No to przyszedł czas pogadać o ciemnej stronie pracy z CTE — typowych błędach. Nawet najbardziej kozackie zapytanie może się wywalić, jeśli źle użyjesz tych potężnych narzędzi. Ale spokojnie, mamy dla Ciebie całą instrukcję, jak je diagnozować i zapobiegać fuckupom!
1. Błąd: materializacja CTE i jej skutki
Jedna z kluczowych cech PostgreSQL przy pracy z CTE — to ich domyślna materializacja. To znaczy, że wynik CTE jest przetwarzany i tymczasowo trzymany w pamięci (albo na dysku, jeśli danych jest za dużo). Jeśli zapytań jest sporo albo danych masa, może to mocno spowolnić wykonanie.
Przykład:
WITH heavy_data AS (
SELECT * FROM large_table
)
SELECT * FROM heavy_data WHERE column_a > 100;
Na pierwszy rzut oka wygląda, że CTE po prostu filtruje dane. Ale w rzeczywistości heavy_data najpierw ładuje się w całości i materializuje, a dopiero potem jest filtrowane. To może zająć wieki.
Jak tego uniknąć?
Od PostgreSQL 12 można używać CTE jako wyrażenia inline (jak podzapytanie), co rozwiązuje problem materializacji. Wystarczy, że takie CTE używasz tylko raz i nie musisz trzymać wyników pośrednich.
Przykład zoptymalizowanego podejścia:
WITH inline_data AS MATERIALIZED (
SELECT * FROM large_table
)
SELECT * FROM inline_data WHERE column_a > 100;
Tip: jeśli chcesz, żeby materializacja się odbyła, dodaj MATERIALIZED. Jeśli nie — użyj NOT MATERIALIZED.
2. Błąd: rekurencyjne CTE zapętlają się
Rekurencyjne CTE to potężna rzecz, ale używanie ich bez ograniczenia głębokości iteracji może prowadzić do zapętlenia. To nie tylko spowolni wykonanie, ale też zje wszystkie dostępne zasoby.
Przykład:
WITH RECURSIVE endless_loop AS (
SELECT 1 AS value
UNION ALL
SELECT value + 1
FROM endless_loop
)
SELECT * FROM endless_loop;
To zapytanie będzie generować nieskończoną ilość wierszy, bo nie ma warunku zatrzymania rekurencji.
Jak tego uniknąć?
Dodaj jasny warunek stopu przez WHERE. Na przykład:
WITH RECURSIVE limited_loop AS (
SELECT 1 AS value
UNION ALL
SELECT value + 1
FROM limited_loop
WHERE value < 10
)
SELECT * FROM limited_loop;
Tip: jeśli używasz rekurencyjnych CTE do dużych hierarchii, ogranicz głębokość rekurencji przez opcję max_recursion_depth PostgreSQL.
3. Błąd: złe użycie UNION i UNION ALL
Kiedy łączysz bazowe i rekurencyjne zapytania w CTE, zły wybór między UNION a UNION ALL może dać nieoczekiwane efekty. Na przykład UNION usuwa duplikaty, co kosztuje dodatkowe zasoby.
Przykład:
WITH RECURSIVE employee_hierarchy AS (
SELECT employee_id, manager_id
FROM employees
WHERE manager_id IS NULL
UNION -- Tu lepiej użyć UNION ALL
SELECT e.employee_id, e.manager_id
FROM employees e
JOIN employee_hierarchy eh ON e.manager_id = eh.employee_id
)
SELECT * FROM employee_hierarchy;
W tym przykładzie UNION może wywalić ważne wiersze z hierarchii, jeśli się powtórzą. I jeszcze spowolni zapytanie!
Jak naprawić?
Używaj UNION ALL, jeśli nie musisz koniecznie usuwać duplikatów:
UNION ALL
4. Błąd: za dużo CTE w jednym zapytaniu
Chcąc zrobić zapytanie jak najbardziej uporządkowane, niektórzy wrzucają dziesiątki CTE. To nie tylko plącze kod, ale też przeciąża planistę zapytań PostgreSQL.
Przykład:
WITH cte1 AS (...),
cte2 AS (...),
cte3 AS (...),
...
cte20 AS (...)
SELECT ...
FROM cte20;
Wygląda jak koszmar każdego dev-a.
Jak naprawić?
— Podziel zapytanie na kilka prostszych. Zamiast jednego mega-zapytania z dziesiątkami CTE — zrób kilka niezależnych zapytań.
— Druga opcja: dla wyników pośrednich, które musisz użyć kilka razy, zapisuj je w tymczasowych tabelach.
5. Błąd: złożone CTE bez indeksowania
Jeśli CTE działa na dużej ilości danych, ale zapomniałeś dodać indeksy do tabel, zapytania będą się ciągnąć jak flaki z olejem. Indeksy to jak turbo dla Twojej bazy danych.
Przykład:
WITH filtered_data AS (
SELECT * FROM large_table WHERE unindexed_column = 'wartość'
)
SELECT * FROM filtered_data;
Jak naprawić?
Przed użyciem CTE upewnij się, że Twoje tabele są zoptymalizowane:
CREATE INDEX idx_large_table ON large_table(unindexed_column);
6. Błąd: próba użycia CTE do wielokrotnego pobierania danych
CTE jest tworzony, wykonuje się, a potem "zamraża". Jeśli musisz użyć jego wyniku w kilku miejscach, dane nie będą przeliczane ponownie — i to czasem prowadzi do błędów.
Przykład:
WITH data AS (
SELECT x, y FROM some_table
)
SELECT x FROM data
WHERE y > 10;
-- Jeśli jeszcze raz trzeba przeliczyć data, to się nie stanie.
Jak naprawić?
Jeśli potrzebujesz dynamiki albo przeliczenia, może CTE to nie najlepszy wybór. Użyj podzapytań.
7. Błąd: brak komentarzy
CTE mogą być mega narzędziem, ale komu potrzebne skomplikowane SQL, którego nikt, nawet Ty, nie ogarnie po dwóch tygodniach?
Przykład:
WITH data_filtered AS (
SELECT *
FROM large_table
WHERE some_column > 100
)
SELECT * FROM data_filtered;
Za miesiąc nikt nie będzie pamiętał, po co te dane były filtrowane!
Więc komentuj zapytania, szczególnie jeśli używasz złożonych albo rekurencyjnych CTE:
WITH data_filtered AS (
-- Filtrowanie danych po wartości kolumny some_column > 100
SELECT *
FROM large_table
WHERE some_column > 100
)
SELECT * FROM data_filtered;
8. Błąd: nadużywanie CTE zamiast tymczasowych tabel
Czasem tymczasowe tabele są dużo lepsze. Na przykład, jeśli wynik musisz użyć wiele razy w różnych zapytaniach albo pracujesz z ogromnym zbiorem danych.
Przykład:
WITH temp_data AS (
SELECT * FROM large_table
)
SELECT * FROM temp_data WHERE column_a > 100;
SELECT * FROM temp_data WHERE column_b < 50;
Takie zapytanie z CTE wykona się dwa razy, choć dane się nie zmieniają!
Jak naprawić?
Stwórz tymczasową tabelę, jeśli dane używasz wiele razy:
CREATE TEMP TABLE temp_table AS
SELECT * FROM large_table;
SELECT * FROM temp_table WHERE column_a > 100;
SELECT * FROM temp_table WHERE column_b < 50;
Końcowa rada
Jak z każdą potężną funkcją, przy pracy z CTE ważne jest, żeby rozumieć, że nie zawsze są najlepszym narzędziem do zadania. Przemyśl, po co i jak ich używasz. Podejście "im więcej CTE, tym lepiej" może mocno pogorszyć wydajność i czytelność kodu. I oczywiście, nie zapominaj o testach wydajności i optymalizacji zapytań.
GO TO FULL VERSION