Postgres Index Internals: מה קורה בפועל בתוך B-Tree, GIN ו-EXPLAIN ANALYZE
מאת צוות מדיה דיל · 01.09.2026 · טכנולוגיה · 6 דק׳
למה Postgres לפעמים מתעלם מאינדקס קיים, איך לקרוא EXPLAIN ANALYZE נכון, ולמה COUNT(*) איטי גם כשיש אינדקס. מדריך פנימי לאינדקסים ב-Postgres.
רוב המפתחים יודעים שאינדקס "מאיץ שאילתות", אבל מעטים יודעים למה אינדקס לפעמים לא בשימוש, למה COUNT(*) איטי גם עם אינדקס, ואיך לקרוא באמת פלט של EXPLAIN ANALYZE. ההבנה הזו היא מה שמפריד בין "הוספתי אינדקס וזה עזר" ל"אני יודע בדיוק למה השאילתה הזו לוקחת 400 מילישניות".
B-Tree: מבנה העץ שעומד מאחורי רוב האינדקסים
האינדקס ברירת המחדל ב-Postgres, B-Tree, שומר עץ מאוזן שבו כל עלה מצביע לשורה בטבלה, ממוין לפי ערך העמודה המאונדקסת. חיפוש שוויון או טווח (WHERE id = X או WHERE created_at BETWEEN...) עובר O(log n) צעדים בעץ במקום סריקה מלאה — אבל B-Tree לא עוזר לחיפוש שמתחיל באחוז (LIKE '%foo') כי הסדר האלפביתי לא רלוונטי שם. חשוב גם לזכור שהעץ עצמו לא מחזיק את הנתונים המלאים אלא רק מצביעים, כך שגם שאילתה שמנצלת אינדקס עדיין דורשת קפיצה נוספת לטבלה עצמה (Heap Fetch) כדי לשלוף עמודות שלא נמצאות באינדקס.
GIN: אינדקס לערכים מרובי-רכיבים
כשעמודה מכילה מבנה מורכב — מערך, JSONB, או וקטור טקסט לחיפוש מלא — B-Tree לא מספיק, כי אין "ערך יחיד" למיין לפיו. GIN (Generalized Inverted Index) בונה מיפוי הפוך: לכל רכיב אפשרי (כל מילה בטקסט, כל איבר במערך) שומר רשימת שורות שמכילות אותו. זה מה שהופך שאילתות @> על JSONB או to_tsvector לחיפוש מהיר.
למה Postgres לפעמים מתעלם מהאינדקס שלכם
ה-Query Planner ב-Postgres לא תמיד משתמש באינדקס קיים, וזה לרוב לא באג. אם שאילתה מחזירה אחוז גדול מהשורות בטבלה (למשל 30%), סריקה מלאה (Seq Scan) עשויה להיות מהירה יותר בפועל מקפיצות אקראיות לדיסק דרך האינדקס. ההחלטה מבוססת על סטטיסטיקות שהפלנר אוסף — ואם הסטטיסטיקות לא עדכניות, ההחלטה עלולה להיות שגויה.
לקרוא EXPLAIN ANALYZE נכון
EXPLAIN לבדו מראה תוכנית משוערת; EXPLAIN ANALYZE מריץ בפועל ומראה זמן אמיתי לכל שלב. המפתח הוא להשוות בין "rows" המשוער ל"rows" בפועל — פער גדול ביניהם הוא הסימן הראשון לסטטיסטיקות לא מעודכנות. גם שווה לחפש "Buffers: shared hit/read" (עם EXPLAIN (ANALYZE, BUFFERS)) כדי לדעת אם הנתונים הגיעו מה-Cache בזיכרון או מדיסק — הפרש שיכול להיות פי 100 בזמן.
אינדקס Composite: סדר העמודות קובע הכול
אינדקס על (tenant_id, created_at) שונה מהותית מאינדקס על (created_at, tenant_id). כלל האצבע: העמודה הכי סלקטיבית קודם, ורק שאילתה שמסננת גם לפי העמודה הראשונה יכולה לנצל את המשך האינדקס ביעילות. זה קריטי במיוחד בארכיטקטורת Multi-Tenant שבה tenant_id כמעט תמיד בעמודה הראשונה.
המחיר הנסתר: כתיבה איטית יותר
כל אינדקס נוסף מאט כל INSERT, UPDATE ו-DELETE על הטבלה, כי Postgres צריך לעדכן את מבנה העץ בכל שינוי. טבלה עם 15 אינדקסים "ליתר ביטחון" היא לרוב טבלה שכתיבות אליה איטיות באופן לא מוסבר — הכלל הוא לאנדקס לפי שאילתות אמיתיות, לא באופן מונע.
COUNT(*) איטי גם עם אינדקס — למה?
בגלל מודל ה-MVCC של Postgres, אין מונה גלובלי מוכן מראש לשורות — כל COUNT(*) חייב לבדוק אילו שורות נראות (Visible) לטרנזקציה הנוכחית, גם אם יש אינדקס. לספירות מדויקות על טבלאות גדולות עדיף לתחזק מונה נפרד (Materialized View או טבלת סיכום) במקום להסתמך על COUNT בזמן אמת.
אינדקסים ו-Read Replicas: לא תמיד אותה תוכנית
כשמפזרים עומס קריאה בין Read Replicas, כדאי לזכור שסטטיסטיקות ה-Planner יכולות להתעדכן בזמנים שונים בין הרפליקות, מה שעלול לגרום לאותה שאילתה לרוץ בתוכנית ביצוע שונה בהתאם לרפליקה שהיא פוגעת בה — פרט שקל לפספס בדיבאג ביצועים מבוזר, ולכן חשוב לוודא הרצת ANALYZE סדירה גם על הרפליקות ולא רק על מסד הכתיבה הראשי.
שאילתות איטיות שמעכבות את המוצר? נשמח לעזור לכם לנתח ולתקן בוואטסאפ.
תגיות: Postgres · Database Index · B-Tree · EXPLAIN ANALYZE · SQL Performance