Menu

Rekurencyjne CTE w SQLite: WITH RECURSIVE dla drzew i serii

Jak działają rekurencyjne CTE w SQLite: układ zapytania startowego i rekurencyjnego, przechodzenie drzew rodzic-dziecko, generowanie serii i unikanie nieskończonych pętli.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Rekurencja w SQL brzmi dziwnie, dopóki jej nie zobaczysz

Większość zapytań zwraca wiersze z danych, które już istnieją. Rekurencyjne CTE działa inaczej: buduje wiersze, podając własny wynik z powrotem jako wejście, krok po kroku, aż nie będzie już nic nowego do dodania. Tak przechodzi się drzewo o nieznanej głębokości albo generuje liczby od 1 do 100 bez tabeli z liczbami.

Kształt jest zawsze taki sam:

WITH RECURSIVE name(columns) AS (
    -- zapytanie startowe: początkowe wiersze
    SELECT ...
    UNION ALL
    -- rekurencja: wiersze wyprowadzone z poprzedniego kroku
    SELECT ... FROM name WHERE ...
)
SELECT * FROM name;

Zapytanie startowe na górze, UNION ALL, zapytanie rekurencyjne pod spodem. SQLite uruchamia zapytanie startowe raz, a potem wciąż uruchamia część rekurencyjną, za każdym razem na wierszach z poprzedniej rundy, dopóki nie przestanie zwracać nowych wierszy. Wtedy się zatrzymuje.

Liczenie od 1 do 10

Najprostsze rekurencyjne CTE generuje ciąg. Żadne tabele nie są potrzebne:

Prześledźmy to:

  1. Zapytanie startowe tworzy jeden wiersz: n = 1.
  2. Krok rekurencyjny bierze ten wiersz, oblicza n + 1 = 2, a 2 < 10 jest prawdą, więc wiersz zostaje.
  3. Następna iteracja bierze n = 2 i tworzy n = 3. I tak dalej.
  4. Gdy n dochodzi do 10, 10 < 10 jest fałszem, krok rekurencyjny nie zwraca żadnych wierszy i SQLite się zatrzymuje.

WHERE n < 10 to warunek zatrzymania. Bez niego zapytanie działa w nieskończoność.

Generowanie serii dat

Ten sam pomysł, przydatny w prawdziwych raportach: uzupełnij każdy dzień w zakresie, także dni, w których nic się nie działo:

Zwykle robi się LEFT JOIN takiej serii z tabelą zdarzeń, żeby poprawnie policzyć dni bez zdarzeń. Zwykłe GROUP BY date całkowicie pomija puste dni, a seria dat daje wiersz dla każdego dnia bez wyjątku.

Przechodzenie drzewa rodzic-dziecko

Klasyczne zastosowanie. Oto tabela pracowników, w której każdy wiersz wskazuje swojego kierownika:

Zapytanie startowe wybiera korzeń (osobę bez kierownika). Krok rekurencyjny złącza tabelę pracowników z CTE i znajduje wszystkich, których manager_id pasuje do id już obecnego w CTE. Każda iteracja schodzi o poziom głębiej. depth to tylko licznik, który dodajemy, by wciąć wynik.

To działa dla drzew o dowolnej głębokości. Dwa poziomy czy dziesięć, zapytanie się nie zmienia.

Znajdowanie wszystkich przodków konkretnego wiersza

Odwróć kierunek. Zamiast schodzić od korzenia, idź w górę od konkretnego pracownika, żeby znaleźć cały łańcuch jego przełożonych:

Zapytanie startowe to pracownik, od którego zaczynasz. Każdy krok rekurencyjny przeskakuje do rodzica. SQLite zatrzymuje się, gdy dojdzie do korzenia: manager_id IS NULL, więc złączenie nic nie znajduje.

Ten wzorzec przydaje się do okruszków nawigacji (breadcrumbs), komentarzy w wątkach, ścieżek kategorii i wszędzie tam, gdzie trzeba "dojść na samą górę".

Warunki zatrzymania i nieskończone pętle

Najczęstszy błąd to zapomniany warunek zatrzymania albo taki, który nigdy nie zadziała. Porównaj:

-- Działa w nieskończoność:
WITH RECURSIVE bad(n) AS (
    SELECT 1
    UNION ALL
    SELECT n + 1 FROM bad
)
SELECT n FROM bad;

Nie ma tu klauzuli WHERE, która kiedykolwiek zwróciłaby zero wierszy. SQLite chętnie spróbuje doliczyć do nieskończoności.

Dwa nawyki obronne:

  1. Zawsze miej w części rekurencyjnej klauzulę WHERE, która ogranicza wzrost.
  2. W trakcie pracy dodawaj LIMIT do zewnętrznego SELECT jako siatkę bezpieczeństwa: jeśli pomylisz się co do warunku zatrzymania, zapytanie i tak się zakończy.

Samo CTE nie ma ograniczenia, ale LIMIT 5 wcześnie zatrzymuje zewnętrzne zapytanie. SQLite jest na tyle sprytny, że nie kontynuuje rekurencji ponad to, czego potrzebuje LIMIT. Przydatne przy eksperymentach, ale w kodzie produkcyjnym nie zastępuje prawdziwego warunku zatrzymania.

Cykle w grafach

Drzewa nie mogą mieć cykli. Ogólne grafy mogą, a naiwne rekurencyjne CTE zapętli się na zawsze, jeśli w danych jest cykl. Rozwiązanie: śledź przebytą do tej pory ścieżkę i nie odwiedzaj węzłów ponownie:

path to tekst z już odwiedzonymi węzłami rozdzielonymi przecinkami. Przed dodaniem nowego węzła klauzula WHERE sprawdza, czy go tam nie ma. Bez tego zabezpieczenia cykl 1 → 2 → 3 → 1 kręciłby się w nieskończoność.

SQL nie ma wbudowanego "zbioru odwiedzonych": budujesz go sam, zwykle jako tekst albo przez złączenie z dotychczasowym CTE.

Rekurencyjne CTE a self join

Jeśli potrzebujesz tylko jednego lub dwóch poziomów, self join jest prostszy i szybszy:

To odpowiada na pytanie "kto jest bezpośrednim kierownikiem każdej osoby". Ale jeśli potrzebujesz "wszystkich, którzy podlegają Adzie, bez względu na głębokość", a głębokość jest nieznana, czysto poradzi sobie z tym tylko rekurencyjne CTE. Dobierz narzędzie do potrzebnej głębokości:

  • Stała, niewielka głębokość: self join, może dwa lub trzy.
  • Nieznana lub dowolna głębokość: WITH RECURSIVE.

Model myślowy

Rekurencyjne CTE to pętla zapisana deklaratywnie:

  • Zapytanie startowe to wartość początkowa pętli.
  • Zapytanie rekurencyjne to ciało pętli: tworzy następną porcję wierszy z bieżących.
  • Warunek zatrzymania to test wyjścia z pętli: gdy zwraca zero wierszy, pętla się kończy.
  • UNION ALL gromadzi wszystko w końcowym wyniku.

Gdy to sobie poukładasz w głowie, składnia przestaje wydawać się dziwna. Piszesz pętlę for w SQL.

Dalej: indeksy

Rekurencyjne CTE przechodzą przez wiele wierszy, a złączenie w kroku rekurencyjnym wykonuje się w każdej iteracji. Jeśli kolumna złączenia nie ma indeksu, wydajność szybko leci w dół. Indeksy to następny rozdział, a manager_id to dokładnie taka kolumna, której indeks się przyda.

Najczęściej zadawane pytania

Czym jest rekurencyjne CTE w SQLite?

Rekurencyjne CTE to zapytanie WITH RECURSIVE, które buduje wynik, wielokrotnie odwołując się do samego siebie. Ma dwie części połączone przez UNION ALL: zapytanie startowe (anchor), które tworzy początkowe wiersze, i zapytanie rekurencyjne, które tworzy kolejne wiersze z poprzedniego kroku. SQLite uruchamia część rekurencyjną, dopóki nie przestanie zwracać nowych wierszy.

Kiedy używać WITH RECURSIVE w SQLite?

Sięgnij po nie, gdy musisz przejść drzewo lub graf (pracownicy i kierownicy, kategorie i podkategorie, komentarze w wątkach) albo wygenerować ciąg (każda data w zakresie, liczby od 1 do 100). Zwykłe złączenia radzą sobie z jednym lub dwoma poziomami, a rekurencyjne CTE obsługuje dowolną głębokość bez znajomości jej z góry.

Jak uniknąć nieskończonej pętli w rekurencyjnym CTE w SQLite?

Upewnij się, że zapytanie rekurencyjne ma warunek zatrzymania: klauzulę WHERE, która w końcu zwróci zero wierszy, albo licznik z górnym limitem. W grafach z cyklami zapisuj odwiedzoną ścieżkę w kolumnie i wykluczaj wiersze, które już w niej są. Jako siatkę bezpieczeństwa dodaj LIMIT do zewnętrznego zapytania, żeby niekontrolowana rekurencja nie zapełniła pamięci.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ