# Zaawansowany SQL na rozmowach kwalifikacyjnych dla analityków danych: podzapytania, pivoty i optymalizacja zapytań 2026 > Kompletny przewodnik po zaawansowanych technikach SQL wymaganych na rozmowach kwalifikacyjnych dla analityków danych. Podzapytania skorelowane, pivoty, optymalizacja i strategie indeksowania. - Published: 2026-05-15 - Updated: 2026-05-15 - Author: Anthony Fillion-Maillet - Tags: sql, data-analytics, interview, query-optimization, subqueries - Reading time: 10 min --- Rozmowy kwalifikacyjne na stanowiska analityków danych w 2026 roku wymagają znacznie więcej niż znajomości podstawowych zapytań SELECT i GROUP BY. Rekruterzy w czołowych firmach technologicznych i korporacjach finansowych testują kandydatów z zakresu podzapytań skorelowanych, transformacji pivotowych oraz optymalizacji zapytań — umiejętności, które odróżniają juniorów od seniorów. Ten przewodnik omawia dokładnie te wzorce, które pojawiają się na technicznych etapach rekrutacji, wraz z produkcyjnymi przykładami działającymi na PostgreSQL 17. > **Co naprawdę testują rekruterzy** > > Rozmowy kwalifikacyjne z SQL dla analityków danych w 2026 roku koncentrują się na trzech obszarach: rozbiciu złożonej logiki na czytelne podzapytania lub CTE, transformacji danych wierszowych do raportów pivotowych za pomocą warunkowej agregacji oraz wykazaniu świadomości wydajności zapytań poprzez plany EXPLAIN i indeksowanie. Większość kandydatów odpada na optymalizacji — sama wiedza o jej istnieniu daje znaczną przewagę. ## Podzapytania skorelowane vs zwykłe podzapytania na rozmowach kwalifikacyjnych Podzapytanie skorelowane odwołuje się do kolumny z zapytania zewnętrznego, co wymusza ponowne obliczenie podzapytania dla każdego wiersza w zbiorze wynikowym. Zwykłe (nieskorelowane) podzapytanie wykonuje się jednokrotnie i zwraca stały wynik. Rekruterzy celowo sprawdzają, czy kandydaci rozumieją, kiedy każde podejście jest odpowiednie — i kiedy JOIN działa lepiej niż oba. Zwykłe podzapytanie sprawdza się dobrze przy porównaniach z wartościami progowymi. Zapytanie wewnętrzne wykonuje się raz, a zapytanie zewnętrzne filtruje według tej pojedynczej wartości. ```sql -- regular_subquery_threshold.sql -- Find all orders above the average order value SELECT order_id, customer_id, total_amount FROM orders WHERE total_amount > ( -- Executes once, returns a single scalar value SELECT AVG(total_amount) FROM orders ); ``` Podzapytanie skorelowane jest konieczne, gdy porównanie zależy od kontekstu każdego wiersza. Klasyczne pytanie rekrutacyjne brzmi: "Znajdź pracowników, którzy zarabiają więcej niż średnia pensja w ich dziale." ```sql -- correlated_subquery_department.sql -- Employees earning above their department average SELECT e.employee_id, e.name, e.department, e.salary FROM employees e WHERE e.salary > ( -- Re-evaluated for each employee's department SELECT AVG(e2.salary) FROM employees e2 WHERE e2.department = e.department ); ``` Wersja skorelowana ponownie wykonuje obliczenie AVG dla działu każdego wiersza. Przy dużych tabelach staje się to kosztowne. Zoptymalizowane podejście wykorzystuje CTE lub funkcję okna do jednorazowego obliczenia średnich departamentowych. ```sql -- optimized_with_cte.sql -- Same result, single pass over the data WITH dept_avg AS ( SELECT department, AVG(salary) AS avg_salary FROM employees GROUP BY department ) SELECT e.employee_id, e.name, e.department, e.salary FROM employees e JOIN dept_avg d ON e.department = d.department WHERE e.salary > d.avg_salary; ``` Rekruterzy szukają kandydatów, którzy potrafią napisać wersję skorelowaną, a następnie ją zrefaktoryzować. Zademonstrowanie obu podejść — wraz z wyjaśnieniem kompromisów wydajnościowych — sygnalizuje biegłość SQL na poziomie seniorskim. ## Wzorce podzapytań najczęściej spotykane na rozmowach dla analityków danych Trzy wzorce podzapytań pojawiają się wielokrotnie na rozmowach kwalifikacyjnych: EXISTS do sprawdzania przynależności, skalarne podzapytania w klauzuli SELECT oraz tabele pochodne w klauzuli FROM. **EXISTS vs. IN** — EXISTS kończy przetwarzanie natychmiast po znalezieniu pierwszego dopasowania, co czyni go szybszym niż IN dla dużych zbiorów wynikowych. IN materializuje cały wynik podzapytania przed rozpoczęciem porównywania. ```sql -- exists_vs_in.sql -- Customers who placed at least one order in 2026 (EXISTS — preferred) SELECT c.customer_id, c.name FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id AND o.order_date >= '2026-01-01' ); -- Equivalent with IN (slower on large datasets) SELECT customer_id, name FROM customers WHERE customer_id IN ( SELECT customer_id FROM orders WHERE order_date >= '2026-01-01' ); ``` **Podzapytanie skalarne w SELECT** — przydatne do dodawania obliczonej kolumny bez pełnego JOINa. ```sql -- scalar_subquery_select.sql -- Each product with its category's total revenue SELECT p.product_id, p.product_name, p.category, ( SELECT SUM(oi.quantity * oi.unit_price) FROM order_items oi JOIN products p2 ON oi.product_id = p2.product_id WHERE p2.category = p.category ) AS category_total_revenue FROM products p; ``` Ten wzorzec jest czytelny przy dodawaniu jednej lub dwóch kolumn. Przy większej liczbie CTE z JOINem jest bardziej czytelny i zazwyczaj szybszy. ## Zapytania pivotowe z warunkową agregacją w SQL Zapytania pivotowe przekształcają dane wierszowe w podsumowania kolumnowe — zamieniając miesiące przechowywane jako wiersze w kolumny typu `jan_revenue`, `feb_revenue`. Chociaż niektóre bazy danych oferują natywny operator PIVOT, przenośne podejście wykorzystuje instrukcje CASE wewnątrz funkcji agregujących. To właśnie tę wersję oczekują rekruterzy. Zapytanie pivotowe łączy warunkową agregację z wyrażeniami CASE wewnątrz funkcji agregujących. Każde wyrażenie CASE filtruje dla konkretnej kategorii, a agregat (SUM, COUNT, AVG) redukuje przefiltrowane wiersze do pojedynczej wartości na grupę. ```sql -- pivot_monthly_revenue.sql -- Monthly revenue pivot: one row per product, one column per month SELECT product_id, product_name, SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 1 THEN quantity * unit_price ELSE 0 END) AS jan_revenue, SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 2 THEN quantity * unit_price ELSE 0 END) AS feb_revenue, SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 3 THEN quantity * unit_price ELSE 0 END) AS mar_revenue, SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 4 THEN quantity * unit_price ELSE 0 END) AS apr_revenue, -- Repeat for remaining months SUM(quantity * unit_price) AS total_revenue FROM order_items oi JOIN orders o ON oi.order_id = o.order_id JOIN products p ON oi.product_id = p.product_id WHERE o.order_date >= '2026-01-01' GROUP BY product_id, product_name ORDER BY total_revenue DESC; ``` Ta sama technika warunkowej agregacji obsługuje wielowymiarowe pivoty. Częste pytanie rekrutacyjne prosi kandydatów o zbudowanie raportu pokazującego aktywność użytkowników według dnia tygodnia i statusu. ```sql -- pivot_user_activity.sql -- User activity: sessions by day of week and device type SELECT user_id, COUNT(CASE WHEN device_type = 'mobile' THEN 1 END) AS mobile_sessions, COUNT(CASE WHEN device_type = 'desktop' THEN 1 END) AS desktop_sessions, COUNT(CASE WHEN device_type = 'tablet' THEN 1 END) AS tablet_sessions, ROUND( COUNT(CASE WHEN device_type = 'mobile' THEN 1 END) * 100.0 / COUNT(*), 1 ) AS mobile_pct FROM user_sessions WHERE session_date >= CURRENT_DATE - INTERVAL '30 days' GROUP BY user_id HAVING COUNT(*) >= 5 -- Only users with meaningful activity ORDER BY mobile_pct DESC; ``` Warunkowa agregacja z CASE jest podstawową umiejętnością analityka danych. [Dokumentacja PostgreSQL dotycząca wyrażeń warunkowych](https://www.postgresql.org/docs/current/functions-conditional.html) opisuje dodatkowe wzorce, takie jak klauzule FILTER, które upraszczają tę składnię. ## Dynamiczny pivot z CROSSTAB w PostgreSQL Rozszerzenie `tablefunc` PostgreSQL udostępnia CROSSTAB dla przypadków, gdy kategorie pivotu nie są znane w czasie pisania zapytania. Choć rzadziej spotykane na rozmowach, pojawia się w zadaniach domowych. ```sql -- crosstab_dynamic_pivot.sql -- Enable the extension (once per database) CREATE EXTENSION IF NOT EXISTS tablefunc; -- Revenue by product and quarter using CROSSTAB SELECT * FROM crosstab( $$ SELECT product_name, 'Q' || EXTRACT(QUARTER FROM order_date)::TEXT AS quarter, SUM(quantity * unit_price) AS revenue FROM order_items oi JOIN orders o ON oi.order_id = o.order_id JOIN products p ON oi.product_id = p.product_id WHERE EXTRACT(YEAR FROM o.order_date) = 2026 GROUP BY product_name, quarter ORDER BY product_name, quarter $$, $$ VALUES ('Q1'), ('Q2'), ('Q3'), ('Q4') $$ ) AS pivot_table( product_name TEXT, q1_revenue NUMERIC, q2_revenue NUMERIC, q3_revenue NUMERIC, q4_revenue NUMERIC ); ``` CROSSTAB wymaga, aby zapytanie źródłowe zwracało dokładnie trzy kolumny: identyfikator wiersza, kategorię i wartość. Drugi argument wymienia wszystkie możliwe wartości kategorii. Brakujące kombinacje generują NULL zamiast 0. ## Optymalizacja zapytań SQL: plany EXPLAIN i analiza kosztów Pytania o optymalizację zapytań sprawdzają, czy kandydat potrafi diagnozować wolne zapytania — nie tylko pisać poprawne. Podstawowym narzędziem diagnostycznym jest EXPLAIN ANALYZE, który wykonuje zapytanie i raportuje rzeczywiste czasy wykonania, liczby wierszy oraz operacje wybrane przez planistę. ```sql -- explain_analyze_example.sql -- Analyze a slow query to identify bottlenecks EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT c.customer_id, c.name, COUNT(o.order_id) AS order_count, SUM(o.total_amount) AS lifetime_value FROM customers c JOIN orders o ON c.customer_id = o.customer_id WHERE o.order_date >= '2025-01-01' GROUP BY c.customer_id, c.name HAVING SUM(o.total_amount) > 1000 ORDER BY lifetime_value DESC; ``` Kluczowe elementy wyjścia EXPLAIN, o które pytają rekruterzy: - **Seq Scan** — pełne skanowanie tabeli. Akceptowalne przy małych tabelach; problematyczne przy milionach wierszy. Wskazuje na brakujący indeks. - **Index Scan / Index Only Scan** — zapytanie korzysta z indeksu. Index Only Scan jest szybszy, ponieważ odczytuje dane bezpośrednio z indeksu bez dostępu do tabeli. - **Nested Loop / Hash Join / Merge Join** — strategia złączenia. Hash Join nadaje się do dużych nieposortowanych zbiorów. Merge Join jest optymalny, gdy obie strony są wstępnie posortowane. Nested Loop działa przy małej tabeli zewnętrznej złączonej z indeksowaną tabelą wewnętrzną. - **Rows** — szacowana vs rzeczywista liczba wierszy. Duże rozbieżności sugerują nieaktualne statystyki (należy uruchomić ANALYZE) lub słabo selektywne predykaty. - **Buffers: shared hit / read** — współczynnik trafień w cache. Wysokie wartości read oznaczają, że dane nie są w pamięci. [Dokumentacja PostgreSQL dotycząca EXPLAIN](https://www.postgresql.org/docs/current/sql-explain.html) opisuje wszystkie dostępne formaty wyjściowe i opcje. ## Strategie indeksowania na poziomie rozmowy kwalifikacyjnej Indeksowanie jest najskuteczniejszą techniką optymalizacji i częstym tematem rozmów. Pytanie zazwyczaj przyjmuje formę: "Mając to wolne zapytanie, jakie indeksy poprawiłyby jego wydajność?" Trzy wzorce indeksowania pokrywają większość scenariuszy rekrutacyjnych: **1. Jednokolumnowy indeks na wysoko selektywnych klauzulach WHERE** ```sql -- indexing_strategies.sql -- Index on order_date: filters a small percentage of rows CREATE INDEX idx_orders_date ON orders (order_date); -- Composite index for queries filtering on both columns CREATE INDEX idx_orders_customer_date ON orders (customer_id, order_date); -- Covering index: includes columns needed in SELECT -- Enables Index Only Scan (no table access) CREATE INDEX idx_orders_covering ON orders (customer_id, order_date) INCLUDE (total_amount, status); ``` **2. Kolejność kolumn w indeksie złożonym ma znaczenie.** Kolumna znajdująca się najdalej z lewej musi odpowiadać klauzuli WHERE zapytania. Indeks na `(customer_id, order_date)` przyspiesza zapytania filtrujące po samym `customer_id` lub po `customer_id AND order_date`, ale NIE zapytania filtrujące tylko po `order_date`. **3. Indeksy częściowe** zmniejszają rozmiar indeksu i koszty utrzymania, gdy tylko podzbiór wierszy jest często odpytywany. ```sql -- partial_index.sql -- Only index active orders (smaller, faster index) CREATE INDEX idx_orders_active ON orders (customer_id, order_date) WHERE status = 'active'; -- Only index recent data for dashboard queries CREATE INDEX idx_orders_recent ON orders (order_date) WHERE order_date >= '2026-01-01'; ``` > **Antywzorce indeksowania** > > Należy unikać indeksowania każdej kolumny — każdy indeks spowalnia operacje INSERT, UPDATE i DELETE. Tabela z ponad 10 indeksami przy intensywnych operacjach zapisu odczuje zauważalne pogorszenie wydajności. Należy również unikać indeksowania kolumn o niskiej kardynalności (jak flagi boolean), chyba że stanowią część indeksu złożonego lub częściowego. ## Typowe antywzorce zapytań zabijające wydajność Rekruterzy przedstawiają celowo wolne zapytania i proszą kandydatów o zidentyfikowanie problemu. Pięć poniższych antywzorców odpowiada za większość rzeczywistych problemów z wydajnością. **Funkcje na indeksowanych kolumnach** — zastosowanie funkcji do indeksowanej kolumny uniemożliwia użycie indeksu. ```sql -- anti_patterns.sql -- BAD: function on indexed column disables the index SELECT * FROM orders WHERE EXTRACT(YEAR FROM order_date) = 2026; -- GOOD: range comparison uses the index SELECT * FROM orders WHERE order_date >= '2026-01-01' AND order_date < '2027-01-01'; ``` **SELECT * w zapytaniach produkcyjnych** — pobiera wszystkie kolumny, w tym duże pola TEXT lub BYTEA. Należy określić tylko potrzebne kolumny. **NOT IN z wartościami NULL** — jeśli podzapytanie zwraca jakąkolwiek wartość NULL, NOT IN nie zwraca żadnych wierszy. Należy zastąpić go NOT EXISTS. ```sql -- not_in_null_trap.sql -- BAD: returns empty if any customer_id is NULL in subquery SELECT * FROM customers WHERE customer_id NOT IN ( SELECT customer_id FROM orders ); -- GOOD: NOT EXISTS handles NULLs correctly SELECT * FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id ); ``` **Niejawne rzutowanie typów** — porównywanie kolumny VARCHAR z liczbą całkowitą wymusza rzutowanie na każdym wierszu, pomijając indeks. Zawsze należy jawnie dopasowywać typy. **Brakujący LIMIT w zapytaniach eksploracyjnych** — skanowanie milionów wierszy, gdy potrzebnych jest tylko 10 pierwszych. Zawsze należy dodawać LIMIT podczas tworzenia i w paginowanych zapytaniach API. > **Ćwicz te wzorce** > > Aby budować biegłość w [podzapytaniach i CTE w SQL](/technologies/data-analytics/interview-questions/sql-subqueries-ctes), warto pracować na rzeczywistych scenariuszach rekrutacyjnych. Łączenie refaktoryzacji podzapytań z analizą EXPLAIN rozwija diagnostyczną intuicję, której szukają rekruterzy. ## Podsumowanie - Podzapytania skorelowane ponownie wykonują się dla każdego wiersza — należy je refaktoryzować do CTE lub funkcji okna dla lepszej wydajności na dużych zbiorach danych - EXISTS przewyższa IN wydajnością przy sprawdzaniu przynależności, szczególnie gdy podzapytanie zwraca wiele wierszy lub zawiera wartości NULL - Warunkowa agregacja z CASE jest przenośną techniką pivotową — należy ją opanować przed składnią PIVOT specyficzną dla danej bazy - EXPLAIN ANALYZE ujawnia rzeczywisty plan wykonania; należy skupić się na Seq Scan, dokładności szacunków wierszy i strategii złączeń - Kolejność kolumn w indeksie złożonym musi odpowiadać wzorcom filtrowania zapytań — obowiązuje reguła lewostronnego prefiksu - Indeksy częściowe i pokrywające zmniejszają I/O dla docelowych wzorców zapytań bez narzutu indeksów pełnotabelowych - Należy unikać funkcji na indeksowanych kolumnach, NOT IN z wartościami NULL i SELECT * w zapytaniach produkcyjnych - Warto ćwiczyć łączenie [funkcji okna](/blog/data-analytics/sql-window-functions-ctes-advanced-queries) z optymalizacją podzapytań do obsługi wieloetapowych pytań analitycznych --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/data-analytics/advanced-sql-data-analyst-interviews