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.

Zaawansowane zapytania SQL i optymalizacja baz danych na rozmowach kwalifikacyjnych dla analityków danych

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.

Gotowy na rozmowy o Data Analytics?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

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 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 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, warto pracować na rzeczywistych scenariuszach rekrutacyjnych. Łączenie refaktoryzacji podzapytań z analizą EXPLAIN rozwija diagnostyczną intuicję, której szukają rekruterzy.

Gotowy na rozmowy o Data Analytics?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

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 z optymalizacją podzapytań do obsługi wieloetapowych pytań analitycznych

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

#sql
#data-analytics
#interview
#query-optimization
#subqueries

Udostępnij

Powiązane artykuły