Menu

אינדקסים חלקיים ב-SQLite: CREATE INDEX ... WHERE

איך אינדקסים חלקיים עובדים ב-SQLite: אינדוקס רק של השורות שאתם באמת שולפים, והדפוסים (מחיקה רכה, ייחודיות חלקית, תת-קבוצות חמות) שבהם הם משתלמים.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

אינדקס חלקי מכסה רק חלק מהשורות

לאינדקס רגיל יש רשומה לכל שורה בטבלה. לאינדקס חלקי יש רשומות רק לשורות שמתאימות לפסוקית WHERE שאתם מספקים ביצירה שלו. אינדקס קטן יותר, פחות דפים לעבור, ופחות עבודה בכל הכנסה ועדכון שלא נוגעים בפרוסה המאונדקסת.

התחביר הוא CREATE INDEX רגיל עם WHERE בסוף:

idx_orders_pending מכיל רשומות רק לשורות שבהן status = 'pending'. הזמנות שנשלחו, בוטלו או הוחזרו לא נמצאות בו בכלל. אם 95% מטבלת ה-orders שלכם היסטוריים ואתם שולפים בעיקר את הפתוחות, זה אינדקס קטן פי 20 לאותה מהירות שאילתה.

מתי המתכנן באמת ישתמש בו

אפשר להשתמש באינדקס חלקי רק כש-SQLite יכולה להוכיח שהשאילתה שלכם מוגבלת לאותן שורות שהאינדקס מכסה. הדרך הנקייה ביותר היא לחזור על פסוקית ה-WHERE של האינדקס בשאילתה:

התוכנית אמורה להזכיר USING INDEX idx_orders_pending. הסירו את status = 'pending' מהשאילתה, והמתכנן חוזר לסריקה מלאה של הטבלה: אין לו דרך לדעת שהשאילתה נשארת בתוך התת-קבוצה המאונדקסת.

כלל האצבע: ה-WHERE של השאילתה חייב לגרור את ה-WHERE של האינדקס. שוויון על אותה עמודה ואותו ערך הוא המקרה הבטוח והברור. אי-שוויונות ו-OR מסובכים יותר, בדקו עם EXPLAIN QUERY PLAN.

למה לטרוח: שלושת היתרונות

שלוש סיבות קונקרטיות לכך שאינדקסים חלקיים משתלמים:

  1. קטן יותר בדיסק. נשמרות רק השורות המתאימות. בעומס עבודה שבו "1% מהטבלה חם", האינדקס הוא בערך 1% מאינדקס מלא.
  2. כתיבות זולות יותר. הכנסות ועדכונים נוגעים באינדקס רק כשהשורה מתאימה לסינון. הכנסה עם status = 'shipped' לטבלה שלמעלה לא נוגעת ב-idx_orders_pending בכלל.
  3. אותה מהירות חיפוש. חיפוש ב-B-tree הוא לוגריתמי בגודל האינדקס. אינדקס קטן יותר נותן חיפושים מעט מהירים יותר, אבל הרווח הגדול הוא בכל מה שמסביב: פחות החטאות מטמון, פחות I/O.

אם עמודה מוטה מאוד, כלומר לרוב השורות יש ערך אחד ואתם מתעניינים רק בערכים הנדירים האחרים, זה מקרה הספר לאינדקס חלקי.

אינדקסים ייחודיים חלקיים (התכונה המנצחת)

אילוצי UNIQUE רגילים חלים על כל שורה. זו בעיה ברגע שמכניסים מחיקה רכה:

-- נכשל: יש שתי שורות עם email = 'a@x.com', למרות שאחת מהן נמחקה.
CREATE UNIQUE INDEX idx_users_email ON users(email);

אינדקס ייחודי חלקי מאפשר לאכוף ייחודיות רק על השורות שחשובות:

שלוש שורות, אותו אימייל, בלי הפרת אילוץ, כי רק השורה עם deleted_at IS NULL משתתפת בבדיקת הייחודיות. נסו להכניס שורה חיה שנייה עם אותו אימייל, ו-SQLite תזרוק UNIQUE constraint failed.

הדפוס הזה מופיע בכל מקום: מינוי פעיל אחד לכל לקוח, כתובת ראשית אחת לכל משתמש, חשבונית פתוחה אחת לכל הזמנה. אינדקסים ייחודיים חלקיים מבטאים אותו ישירות.

אינדוקס סביב NULL

NULL מתנהג בצורה מוזרה עם אינדקסים. מטרה נפוצה היא "להתעלם מה-NULL לגמרי": נניח שיש לכם עמודת external_id דלילה שבה רוב השורות הן NULL, אבל אלה שמולאו חייבות להיות ייחודיות:

שני NULL מתקיימים יחד בשלום, והשורות EXT-001 ו-EXT-002 מובטחות כייחודיות. גם האינדקס קטן יותר, כי שורות NULL לא נשמרות בו בכלל, ולכן חיפושים לפי external_id מהירים גם כשהטבלה גדלה.

למה הסינון יכול להתייחס

פסוקית ה-WHERE של אינדקס חלקי מוגבלת. היא יכולה להתייחס ל:

  • עמודות של הטבלה שמאונדקסת.
  • קבועים מילוליים.
  • קבוצה קטנה של פונקציות מובנות דטרמיניסטיות.

היא לא יכולה להתייחס ל:

  • טבלאות אחרות.
  • תת-שאילתות.
  • פונקציות לא דטרמיניסטיות כמו random() או CURRENT_TIMESTAMP.
  • פרמטרים או משתנים.

זה הגיוני: SQLite צריכה להעריך את הסינון בכל הכנסה ועדכון של שורה, והתוצאה חייבת להיות יציבה. לכן זה עובד:

אבל WHERE created_at > date('now') לא יעבוד: date('now') משתנה עם הזמן, כך שקבוצת השורות המאונדקסות הייתה זזה מתחת לרגליים של SQLite.

תהליך בדיקת שפיות

כשאתם מוסיפים אינדקס חלקי, עברו על שלוש בדיקות:

שאילתה 1 אמורה להשתמש ב-idx_jobs_runnable. שאילתות 2 ו-3 אמורות לחזור לסריקה (או לאינדקס אחר, אם יש לכם כזה). אם המתכנן בוחר באינדקס החלקי לשאילתה שלא ציפיתם, קראו שוב את הסינון: ייתכן שהוא רחב יותר ממה שאתם חושבים.

מתי לא להשתמש בו

אינדקסים חלקיים הם כלי חד. סיבות לוותר:

  • הסינון מתאים לרוב הטבלה. אם "פעיל" הוא 90% מהשורות, אינדקס חלקי הוא אינדקס רגיל עם צעדים מיותרים. פשוט אנדקסו את העמודה.
  • השאילתות שלכם לא כוללות את הסינון מילולית. אם הקוד שלכם משתמש ב-ORM שבונה WHERE status IN (?, ?, ?) או מחשב את הסינון דינמית, המתכנן לעתים קרובות לא יזהה את ההתאמה. בדקו עם EXPLAIN QUERY PLAN, אל תניחו.
  • התת-קבוצה החמה משתנה עם הזמן. אינדקס חלקי על "הזמנות מ-30 הימים האחרונים" נשמע מפתה אבל אי אפשר לבטא אותו: הסינון חייב להיות דטרמיניסטי. תצטרכו לבנות את האינדקס מחדש, או לבחור סכמה אחרת (טבלת recent_orders נפרדת או עמודה בוליאנית archived שמעדכנים כל לילה).

כשהסינון יציב ומתאים לפרוסה קטנה של טבלה גדולה, אינדקסים חלקיים הם מהכוונונים המשתלמים ביותר שאפשר לעשות ב-SQLite.

הבא: קריאת תוכניות שאילתה

רוב העמוד הזה נשען על EXPLAIN QUERY PLAN כדי לוודא שאינדקס באמת היה בשימוש. הכלי הזה ראוי לעמוד משלו: איך לקרוא את הפלט שלו, מה המילים השמורות אומרות, ואיך להבחין בין חיפוש שמח באינדקס לבין סריקה מלאה ערמומית. זה מה שבא עכשיו.

שאלות נפוצות

מהו אינדקס חלקי ב-SQLite?

אינדקס חלקי מאנדקס רק שורות שמתאימות לפסוקית WHERE שניתנה בזמן היצירה. כותבים CREATE INDEX name ON table(col) WHERE condition, ו-SQLite שומרת רשומות רק עבור שורות שבהן התנאי מתקיים. אינדקס קטן יותר, כתיבה מהירה יותר, ואותה מהירות חיפוש לשאילתות שמתאימות לסינון.

מתי כדאי להשתמש באינדקס חלקי במקום באינדקס מלא?

כששולפים שוב ושוב פרוסה קטנה של טבלה גדולה: הזמנות ממתינות, משתמשים פעילים, משימות שלא עובדו. אינדוקס של הפרוסה הזו בלבד שומר על אינדקס זעיר ומאפשר לכתיבות לשורות אחרות לדלג עליו לגמרי. אם השאילתות שלכם לא כוללות את אותו תנאי WHERE שיש באינדקס, המתכנן לא יכול להשתמש בו.

האם אינדקס חלקי יכול לאכוף ייחודיות?

כן. CREATE UNIQUE INDEX ... WHERE ... אוכף ייחודיות רק על שורות שמתאימות לסינון. השימוש הקלאסי הוא 'רשומה פעילה אחת לכל משתמש': שורות שנמחקו מחיקה רכה לא נכללות, כך שאפשר להחזיק כמה רשומות מחוקות עם אותו מפתח אבל רק אחת חיה.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל