Menu

CTE רקורסיבי ב-SQLite: WITH RECURSIVE לעצים ולסדרות

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

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

רקורסיה ב-SQL נשמעת מוזר, עד שרואים אותה

רוב השאילתות מחזירות שורות מנתונים שכבר קיימים. CTE רקורסיבי שונה: הוא בונה שורות על ידי הזנת הפלט של עצמו בחזרה כקלט, צעד אחר צעד, עד שאין מה להוסיף. כך עוברים על עץ בעומק לא ידוע, או מייצרים את המספרים 1 עד 100 בלי טבלת מספרים.

המבנה תמיד זהה:

WITH RECURSIVE name(columns) AS (
    -- עוגן: שורות ההתחלה
    SELECT ...
    UNION ALL
    -- רקורסיבי: שורות שנגזרות מהשלב הקודם
    SELECT ... FROM name WHERE ...
)
SELECT * FROM name;

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

ספירה מ-1 עד 10

ה-CTE הרקורסיבי הפשוט ביותר מייצר סדרה. אין צורך בטבלאות:

עקבו אחרי הריצה:

  1. העוגן מייצר שורה אחת: n = 1.
  2. הצעד הרקורסיבי לוקח את השורה הזו, מחשב n + 1 = 2, ו-2 < 10 אמת, אז הוא שומר את השורה.
  3. האיטרציה הבאה לוקחת n = 2 ומייצרת n = 3. וכן הלאה.
  4. כש-n מגיע ל-10, 10 < 10 שקר, הצעד הרקורסיבי לא מחזיר שורות, ו-SQLite עוצרת.

WHERE n < 10 הוא תנאי העצירה. בלעדיו השאילתה רצה לנצח.

יצירת סדרת תאריכים

אותו רעיון, שימושי בדוחות אמיתיים: למלא כל יום בטווח, גם ימים שבהם לא קרה כלום:

בדרך כלל עושים LEFT JOIN בין זה לבין טבלת אירועים כדי לספור נכון ימים בלי אירועים. GROUP BY date רגיל מדלג לגמרי על ימים ריקים, וסדרת התאריכים נותנת שורה לכל יום, בכל מקרה.

מעבר על עץ של הורה וילד

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

העוגן בוחר את השורש (האדם שאין לו מנהל). הצעד הרקורסיבי מחבר את טבלת העובדים בחזרה ל-CTE, ומוצא את כל מי שה-manager_id שלו מתאים ל-id שכבר נמצא ב-CTE. כל איטרציה יורדת רמה אחת עמוק יותר. depth הוא רק מונה שהוספנו כדי להזיח את הפלט.

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

מציאת כל האבות של שורה מסוימת

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

העוגן הוא העובד שממנו מתחילים. כל צעד רקורסיבי קופץ להורה. SQLite עוצרת כשהיא מגיעה לשורש: manager_id IS NULL, ולכן ה-JOIN לא מוצא כלום.

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

תנאי עצירה ולולאות אינסופיות

הבאג הנפוץ ביותר הוא לשכוח את תנאי העצירה או לכתוב תנאי שאף פעם לא מתקיים. השוו:

-- רצה לנצח:
WITH RECURSIVE bad(n) AS (
    SELECT 1
    UNION ALL
    SELECT n + 1 FROM bad
)
SELECT n FROM bad;

אין פסוקית WHERE שתחזיר אי פעם אפס שורות. SQLite תנסה בשמחה לספור עד אינסוף.

שני הרגלים הגנתיים:

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

ה-CTE עצמו בלתי מוגבל, אבל LIMIT 5 עוצר את השאילתה החיצונית מוקדם. SQLite חכמה מספיק כדי לא להמשיך ברקורסיה מעבר למה ש-LIMIT צריך. שימושי לחקירה, אבל לא תחליף לתנאי עצירה אמיתי בקוד ייצור.

מעגלים בגרפים

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

path היא מחרוזת של צמתים שכבר ביקרתם בהם, מופרדים בפסיקים. לפני הוספת צומת חדש, פסוקית ה-WHERE בודקת שהוא לא נמצא בה. בלי ההגנה הזו, המעגל 1 → 2 → 3 → 1 היה נמשך לנצח.

אין ב-SQL "קבוצת ביקורים" מובנית: בונים אותה בעצמכם, בדרך כלל כמחרוזת או על ידי JOIN מול ה-CTE עד כה.

CTE רקורסיבי מול Self Join

אם צריך רק רמה אחת או שתיים, self join פשוט ומהיר יותר:

זה מטפל בשאלה "מי המנהל הישיר של כל אחד". אבל אם צריך "כל מי שכפוף בסופו של דבר ל-Ada, לא משנה באיזה עומק", כשהעומק לא ידוע, רק CTE רקורסיבי מטפל בזה בצורה נקייה. בחרו את הכלי לפי העומק שאתם צריכים:

  • עומק קבוע וקטן: self join, אולי שניים או שלושה כאלה.
  • עומק לא ידוע או שרירותי: WITH RECURSIVE.

מודל מנטלי

CTE רקורסיבי הוא לולאה שנכתבת באופן הצהרתי:

  • העוגן הוא הערך ההתחלתי של הלולאה.
  • השאילתה הרקורסיבית היא גוף הלולאה: היא מייצרת את קבוצת השורות הבאה מהנוכחיות.
  • תנאי העצירה הוא בדיקת היציאה של הלולאה: כשהיא מחזירה אפס שורות, הלולאה מסתיימת.
  • UNION ALL צובר הכול לקבוצת התוצאות הסופית.

ברגע שהמיפוי הזה יושב לכם בראש, התחביר מפסיק להרגיש מוזר. אתם כותבים לולאת for ב-SQL.

הבא: אינדקסים

CTE רקורסיבי עובר על הרבה שורות, וה-JOIN שבתוך הצעד הרקורסיבי רץ בכל איטרציה. אם עמודת ה-JOIN לא מאונדקסת, הביצועים צונחים מהר. אינדקסים הם הפרק הבא, ו-manager_id הוא בדיוק סוג העמודה שמרוויח מאינדקס.

שאלות נפוצות

מה זה CTE רקורסיבי ב-SQLite?

CTE רקורסיבי הוא שאילתת WITH RECURSIVE שבונה קבוצת תוצאות על ידי פנייה חוזרת לעצמה. יש לה שני חלקים שמחוברים ב-UNION ALL: שאילתת עוגן שמייצרת את שורות ההתחלה, ושאילתה רקורסיבית שמייצרת שורות נוספות מהשלב הקודם. SQLite ממשיכה להריץ את החלק הרקורסיבי עד שהוא לא מחזיר שורות חדשות.

מתי כדאי להשתמש ב-WITH RECURSIVE ב-SQLite?

השתמשו בו כשצריך לעבור על עץ או גרף (עובדים ומנהלים, קטגוריות ותת-קטגוריות, תגובות בשרשור) או לייצר סדרה (כל תאריך בטווח, המספרים 1 עד 100). JOIN רגיל מטפל ברמה אחת או שתיים, ו-CTE רקורסיבי מטפל בכל עומק בלי לדעת אותו מראש.

איך נמנעים מלולאות אינסופיות ב-CTE רקורסיבי ב-SQLite?

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

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

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

להתחיל