EXPLAIN QUERY PLAN מראה איך שאילתה תרוץ
לפני שמכווננים שאילתה איטית, צריך לדעת מה SQLite עושה בפועל. EXPLAIN QUERY PLAN מדפיס סיכום קצר של האסטרטגיה שהמתכנן בחר: באילו טבלאות הוא נוגע, באיזה סדר, ובאילו אינדקסים (אם בכלל) הוא משתמש. השאילתה עצמה לא רצה, אתם מקבלים רק את התוכנית.
פשוט הוסיפו את מילות המפתח לפני כל פקודה:
הפלט נראה בערך כך:
QUERY PLAN
`--SEARCH users USING INDEX sqlite_autoindex_users_1 (email=?)
השורה הבודדת הזו אומרת הרבה: SQLite מבצעת SEARCH (ולא סריקה) על הטבלה users, בעזרת האינדקס הייחודי שנוצר אוטומטית עבור email, כש-email הוא מפתח החיפוש. בדיוק מה שהייתם רוצים לראות.
SCAN מול SEARCH: הדבר הראשון שקוראים
כל שורה בתוכנית מתחילה ב-SCAN או ב-SEARCH. ההבחנה הזו היא האות החשוב ביותר בכל הפלט.
SCAN <table>: SQLite קוראת כל שורה בטבלה (או כל רשומה באינדקס). העלות גדלה עם גודל הטבלה.SEARCH <table> USING ...: SQLite קופצת ישירות לשורות המתאימות דרך אינדקס או מפתח ראשי. העלות גדלה עם גודל התוצאה, לא עם גודל הטבלה.
הנה השוואה זו לצד זו. לעמודה אחת יש אינדקס, ולשנייה אין:
התוכנית הראשונה מדווחת SEARCH orders USING INDEX idx_orders_customer. השנייה מדווחת SCAN orders: אין אינדקס על status, ולכן SQLite קוראת כל שורה. בטבלה קטנה זה בלתי מורגש; בטבלה של מיליון שורות זה ההבדל בין אלפיות שנייה לשניות.
SCAN הוא לא תמיד טעות. בטבלאות עזר קטנטנות, או בשאילתות שבאמת מחזירות את רוב השורות, סריקה היא התוכנית הנכונה. אבל בטבלה גדולה עם מסנן סלקטיבי, SCAN הוא הרמז שלכם להוסיף אינדקס.
איך מוודאים שנעשה שימוש באינדקס
הביטוי שצריך לחפש הוא USING INDEX <name> (או USING COVERING INDEX <name>, על זה בהמשך). אם יצרתם אינדקס בתקווה שהמתכנן ישתמש בו, כך בודקים:
אתם אמורים לראות SEARCH events USING INDEX idx_events_user (user_id=?). אם במקום זה התוכנית אומרת SCAN events, משהו מונע מהמתכנן להשתמש באינדקס. הסיבות הנפוצות: עטיפת העמודה בפונקציה (WHERE lower(user_id) = ...), השוואה בין טיפוסים שונים, או שימוש ב-LIKE '%foo%' עם תו כללי בהתחלה.
בדיקה מהירה של זה:
ה-+ 0 הזה מנטרל את האינדקס, והתוכנית חוזרת ל-SCAN events. כל ביטוי על העמודה המאונדקסת עושה אותו דבר.
אינדקסים מכסים נראים אחרת
כשאינדקס מכיל את כל העמודות שהשאילתה צריכה, SQLite יכולה לענות על השאילתה מהאינדקס בלבד, בלי לגעת בטבלה. התוכנית מדווחת USING COVERING INDEX:
התוכנית: SEARCH products USING COVERING INDEX idx_products_sku_price (sku=?). השאילתה מבקשת את price, והאינדקס כבר שומר את sku ואת price, ולכן SQLite אף פעם לא קוראת את הטבלה עצמה. אינדקס מכסה (covering index) הוא התוכנית המהירה ביותר שאפשר לקבל בחיפוש, וכדאי להכיר אותו כשבוחרים אילו עמודות לאנדקס יחד.
קריאת תוכניות של JOIN
ב-JOIN התוכניות נעשות מעניינות. כל שורה בתוכנית מתאימה לטבלה אחת ב-JOIN, וסדר השורות הוא הסדר שבו SQLite מבקרת בטבלאות. הטבלה הראשונה היא הטבלה החיצונית (outer); בטבלאות הבאות מחפשים פעם אחת לכל שורה של הטבלה החיצונית.
תוכנית טיפוסית:
QUERY PLAN
|--SEARCH c USING INTEGER PRIMARY KEY (rowid=?)
`--SEARCH o USING INDEX idx_orders_customer (customer_id=?)
קראו אותה מלמעלה למטה: SQLite מוצאת את הלקוח היחיד לפי המפתח הראשי, ואז עבור הלקוח הזה מחפשת את ההזמנות המתאימות דרך האינדקס על customer_id. שתי השורות הן SEARCH, בלי סריקות מלאות, וזה מה שאתם רוצים.
אם הייתם רואים SCAN o בשורה השנייה, כל חיפוש של לקוח היה מפעיל מעבר מלא על orders. בטבלה גדולה זה אסון. הפתרון כמעט תמיד הוא אינדקס על עמודת ה-JOIN.
שאילתות מורכבות ותת-שאילתות
תוכניות של UNION, EXCEPT ותת-שאילתות מקוננות. כל ענף מופיע מוזח מתחת לאב שלו:
תראו שתי שורות בנות מתחת לכותרת COMPOUND QUERY, אחת לכל ענף. תת-שאילתות ו-CTE עובדים באופן דומה: כל אחד מקבל צומת תוכנית מוזח משלו, ואת כל אחד קוראים באותה עדשה של SCAN מול SEARCH.
תת-השאילתה הופכת לצומת תוכנית נפרד ("LIST SUBQUERY" או משהו דומה), עם אסטרטגיית גישה משלה. החילו את אותן בדיקות בכל רמה.
EXPLAIN מול EXPLAIN QUERY PLAN
אלה שני דברים שונים, ואנשים מבלבלים ביניהם.
EXPLAIN (בלי QUERY PLAN) שופך את ה-bytecode שהמכונה הווירטואלית של SQLite תריץ: עשרות opcodes ברמה נמוכה כמו OpenRead, SeekRowid, Column, ResultRow. שימושי אם אתם מדבגים את המנוע עצמו. כמעט אף פעם לא שימושי לכוונון.
EXPLAIN QUERY PLAN הוא הסיכום הקריא שאתם באמת רוצים. במקרה של ספק, תמיד פנו ל-EXPLAIN QUERY PLAN.
תהליך עבודה לשאילתות איטיות
כששאילתה איטית, הלולאה נראית כך:
- הריצו עליה
EXPLAIN QUERY PLAN. - עבור כל שורת טבלה, שאלו: האם זה
SCANאוSEARCH? בטבלה גדולה,SCANהוא החשוד. - אם
SCANמסנן לפי עמודה כלשהי, שקלו אינדקס על העמודה הזו. - ב-JOIN, ודאו שהטבלאות בלולאה הפנימית משתמשות ב-
SEARCH USING INDEXעל עמודת ה-JOIN. - הריצו שוב
EXPLAIN QUERY PLANאחרי הוספת האינדקס. התוכנית אמורה להשתנות. אם היא לא השתנתה, המתכנן החליט שהאינדקס לא שווה שימוש, בדרך כלל כי הטבלה קטנה או שהמסנן לא סלקטיבי מספיק.
דוגמה מעשית לשלב 5:
התוכנית השתנתה מ-SCAN ל-SEARCH. זה האות שהאינדקס עושה את העבודה שלו. (בטבלה חדשה וכמעט ריקה המתכנן עשוי עדיין לסרוק, כי אין מספיק נתונים כדי שהאינדקס ישתלם. מלאו את הטבלה או הריצו ANALYZE, ולעתים קרובות הבחירה מתהפכת.)
מה התוכנית לא תגיד לכם
EXPLAIN QUERY PLAN מתאר אסטרטגיה, לא עלות. הוא לא יגיד לכם שהשאילתה לקחה 800 ms או החזירה 50,000 שורות. בשביל זה צריך מדידת זמן (.timer on ב-CLI) וספירת שורות. התוכנית והמדידה משלימות זו את זו: התוכנית אומרת לכם למה שאילתה איטית, והטיימר אומר לכם אם היא באמת איטית.
עוד שתי מגבלות שכדאי להכיר:
- התוכנית יכולה להשתנות ככל שהנתונים גדלים. שאילתה שסרקה בשמחה טבלה של 100 שורות תצטרך אינדקס כשהטבלה תגיע למיליון שורות. בדקו תוכניות מחדש על נתונים בגודל של סביבת הייצור, לא על נתוני הפיתוח שלכם.
- המתכנן משתמש בסטטיסטיקות שנאספות על ידי
ANALYZE. בלעדיהן הוא נופל חזרה לברירות מחדל שלא תמיד טובות. סטטיסטיקות ישנות או חסרות הן סיבה נפוצה לתוכניות מפתיעות.
הצעד הבא: ANALYZE ו-VACUUM
מתכנן השאילתות מקבל החלטות על סמך סטטיסטיקות על הטבלאות והאינדקסים שלכם. אם הסטטיסטיקות האלה חסרות או לא עדכניות, גם סכמה עם אינדקסים מושלמים יכולה להפיק תוכנית גרועה. ANALYZE הוא הדרך לשמור אותן עדכניות, ו-VACUUM היא הפקודה הנלווית לשחרור מקום ולביטול פרגמנטציה בקובץ מסד הנתונים. על זה בהמשך.
שאלות נפוצות
מה עושה EXPLAIN QUERY PLAN ב-SQLite?
הפקודה מבקשת מ-SQLite לתאר איך היא הייתה מריצה שאילתה, בלי להריץ אותה בפועל. הפלט מראה אילו טבלאות נסרקות, באילו אינדקסים נעשה שימוש ובאיזה סדר מתבצעים ה-JOIN. הוסיפו EXPLAIN QUERY PLAN לפני כל SELECT, INSERT, UPDATE או DELETE כדי לראות את התוכנית.
מה ההבדל בין SCAN ל-SEARCH בפלט?
SCAN אומר ש-SQLite קוראת כל שורה בטבלה או באינדקס: זה בסדר בטבלאות קטנות, ויקר בטבלאות גדולות. SEARCH אומר שהיא קופצת ישירות לשורות המתאימות בעזרת אינדקס או מפתח ראשי. בטבלה גדולה כמעט תמיד תרצו לראות SEARCH על העמודות שלפיהן אתם מסננים.
איך בודקים אם השאילתה שלי משתמשת באינדקס?
הריצו EXPLAIN QUERY PLAN על השאילתה וחפשו בפלט USING INDEX <name> או USING COVERING INDEX <name>. אם מופיע רק SCAN <table> בלי אזכור של אינדקס, השאילתה מבצעת סריקה מלאה של הטבלה וסביר שאינדקס יעזור.
מה ההבדל בין EXPLAIN ל-EXPLAIN QUERY PLAN?
EXPLAIN מציג את ה-bytecode ברמה נמוכה שהמכונה הווירטואלית של SQLite מייצרת: שימושי להבנת פנימיות המנוע, ולעתים רחוקות שימושי לכוונון שאילתות. EXPLAIN QUERY PLAN מציג סיכום קריא של הגישה לטבלאות והשימוש באינדקסים. לעבודה על ביצועים כמעט תמיד תרצו את EXPLAIN QUERY PLAN.