Rozmowa o pracę z SQL szybko pokazuje, czy kandydat rozumie dane, czy tylko pamięta kilka gotowych zapytań. Zebrałem tu najczęstsze pytania rekrutacyjne z SQL wraz z krótkimi odpowiedziami, przykładami kodu i zadaniami, które sprawdzają się na stanowiskach analityka danych, backend developera, BI developera oraz SQL developera.
Najważniejsze obszary przygotowania do rozmowy z SQL
- JOIN, WHERE i GROUP BY to fundament większości zadań praktycznych.
- NULL zachowuje się inaczej niż zwykła wartość i często prowadzi do błędnych wyników.
- Funkcje okna pomagają rozwiązywać zadania z rankingami, numeracją i porównywaniem rekordów.
- Indeksy i plan wykonania są ważniejsze niż mechaniczne przepisywanie zapytań.
- Transakcje i poziomy izolacji pojawiają się szczególnie często na rozmowach technicznych.

Co naprawdę sprawdza rozmowa z SQL
Rekruter zwykle nie oczekuje recytowania całej składni języka. Chce sprawdzić, czy potrafisz przełożyć problem biznesowy na zapytanie, przewidzieć jego wynik i zauważyć sytuacje brzegowe, takie jak brak dopasowania, duplikaty albo wartości NULL.
Zakres zależy od stanowiska. Analityk danych może dostać zadanie z filtrowania, agregacji i funkcji okna, a backend developer pytania o transakcje, indeksy i spójność danych. Na poziomie seniorskim dochodzi analiza planu wykonania, blokady, optymalizacja oraz uzasadnianie kompromisów.
| Stanowisko | Najczęściej sprawdzane tematy |
|---|---|
| Analityk danych | SELECT, JOIN, GROUP BY, agregacje, funkcje okna, jakość danych |
| Backend developer | transakcje, klucze, indeksy, zapytania aplikacyjne, blokady |
| BI developer | łączenie wielu tabel, wydajność, ETL, CTE, raportowanie |
| SQL developer | procedury, widoki, indeksy, plan zapytania, modelowanie danych |
Moim zdaniem największy błąd kandydatów polega na uczeniu się samych definicji. Znacznie lepiej przygotować mały schemat złożony z 3-5 tabel i rozwiązywać na nim zadania zbliżone do tych, które mogą pojawić się w pracy.
Podstawowe pytania, które wracają niemal zawsze
Jaka jest różnica między kluczem głównym a obcym
Klucz główny jednoznacznie identyfikuje rekord w tabeli. Nie może być pusty ani powtarzać się dla dwóch wierszy. Klucz obcy wskazuje rekord w innej tabeli i pomaga utrzymać spójność relacji.
Przykładowo kolumna department_id w tabeli employees może wskazywać na id w tabeli departments. Jeden dział może mieć wielu pracowników, ale pracownik powinien należeć do istniejącego działu, chyba że model danych dopuszcza brak przypisania.
Czym różnią się WHERE i HAVING
WHERE filtruje pojedyncze wiersze przed grupowaniem, natomiast HAVING filtruje grupy utworzone przez GROUP BY. To rozróżnienie jest proste, ale bardzo często pojawia się w zadaniach rekrutacyjnych.
SELECT department_id, COUNT(*) AS employee_count
FROM employees
WHERE active = TRUE
GROUP BY department_id
HAVING COUNT(*) >= 5;
Najpierw wybierani są aktywni pracownicy, później są grupowani według działów, a na końcu zostają tylko te działy, w których jest co najmniej 5 aktywnych osób. Przeniesienie warunku z WHERE do HAVING mogłoby zmienić wynik i pogorszyć wydajność.
Jak działa NULL
NULL nie oznacza zera, pustego tekstu ani wartości fałszywej. Oznacza brak znanej wartości. Dlatego zapis column = NULL nie działa tak, jak wiele osób zakłada.
SELECT *
FROM employees
WHERE manager_id IS NULL;
Do sprawdzania braku wartości używamy IS NULL albo IS NOT NULL. Trzeba też pamiętać, że większość funkcji agregujących, na przykład AVG i SUM, pomija wartości NULL, podczas gdy COUNT(*) liczy wszystkie wiersze.
COUNT(*) czy COUNT(kolumna)
COUNT(*) zwraca liczbę wierszy, a COUNT(column) liczy tylko te rekordy, w których wskazana kolumna nie jest NULL. Różnica ma znaczenie przy raportach i jest dobrym testem rozumienia jakości danych.
SELECT
COUNT(*) AS all_rows,
COUNT(manager_id) AS rows_with_manager
FROM employees;
Jeżeli tabela ma 100 pracowników, ale 12 z nich nie ma przypisanego przełożonego, wyniki będą wynosić odpowiednio 100 i 88. Przy liczeniu unikalnych wartości można użyć także COUNT(DISTINCT department_id).
UNION a UNION ALL
UNION łączy wyniki i usuwa duplikaty, natomiast UNION ALL zachowuje wszystkie wiersze. Ten drugi wariant jest zwykle szybszy, ponieważ baza nie musi sortować ani porównywać całego wyniku.
Warunkiem połączenia jest zgodna liczba kolumn oraz kompatybilne typy danych. Jeśli duplikaty mają znaczenie biznesowe albo wiemy, że ich nie ma, wybieram UNION ALL, zamiast płacić za niepotrzebne deduplikowanie.
JOIN, agregacje i NULL w zadaniach praktycznych
INNER JOIN i LEFT JOIN
INNER JOIN zwraca tylko rekordy mające dopasowanie po obu stronach. LEFT JOIN zachowuje wszystkie rekordy z lewej tabeli, nawet jeśli po prawej stronie nie ma odpowiadającego wiersza.
SELECT d.name, e.name
FROM departments d
LEFT JOIN employees e
ON e.department_id = d.id;
Takie zapytanie pokaże również działy bez pracowników. Gdybyśmy użyli INNER JOIN, te działy zniknęłyby z wyniku. To klasyczny przykład, w którym wybór rodzaju JOIN zmienia odpowiedź na pytanie biznesowe.
Dlaczego warunek w WHERE może zepsuć LEFT JOIN
Załóżmy, że chcemy znaleźć wszystkich klientów oraz ich zamówienia z 2026 roku. Jeśli filtr na datę umieścimy w WHERE, klienci bez zamówienia zostaną usunięci z wyniku.
SELECT c.id, c.name, o.id AS order_id
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.id
AND o.created_at >= '2026-01-01';
Filtr w warunku ON zachowuje klientów bez zamówień. W praktyce właśnie takie drobne różnice odróżniają zapytanie poprawne od zapytania, które tylko wygląda poprawnie.
Jak znaleźć rekordy bez powiązania
Do znalezienia działów bez pracowników można użyć LEFT JOIN i sprawdzenia wartości NULL po prawej stronie.
SELECT d.id, d.name
FROM departments d
LEFT JOIN employees e
ON e.department_id = d.id
WHERE e.id IS NULL;
Alternatywą jest NOT EXISTS. Często wybieram tę formę, gdy chcę jasno zapisać warunek „nie istnieje powiązany rekord” i uniknąć nieoczywistych problemów z NULL występujących przy NOT IN.
SELECT d.id, d.name
FROM departments d
WHERE NOT EXISTS (
SELECT 1
FROM employees e
WHERE e.department_id = d.id
);
Jak działa GROUP BY
Każda kolumna w SELECT, która nie jest funkcją agregującą, powinna znaleźć się w GROUP BY. Funkcje takie jak COUNT, SUM, AVG, MIN i MAX obliczają wynik dla każdej grupy.
SELECT customer_id, SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id
HAVING SUM(amount) > 1000;
To zapytanie zwraca klientów, których suma zamówień przekroczyła 1000. Na rozmowie dobrze dopowiedzieć, czy kwota ma obejmować anulowane zamówienia, jaki jest zakres dat i co zrobić z wartościami NULL. Sama składnia nie rozwiązuje nieprecyzyjnego problemu biznesowego.
Funkcje okna i zadania, które często pojawiają się przy tablicy
Funkcja okna wykonuje obliczenia na powiązanym zbiorze wierszy, ale nie scala ich w jeden rekord tak jak GROUP BY. Dzięki temu możemy obok pojedynczego wyniku pokazać na przykład pozycję pracownika w dziale.
Druga najwyższa pensja
Najbezpieczniej najpierw usunąć powtórzone wartości, a dopiero potem wybrać drugą pozycję. W przeciwnym razie dwie osoby z najwyższą identyczną pensją mogą zmienić interpretację zadania.
SELECT salary
FROM (
SELECT DISTINCT salary,
DENSE_RANK() OVER (ORDER BY salary DESC) AS position
FROM employees
) ranked
WHERE position = 2;
DENSE_RANK nadaje tę samą pozycję równym wartościom i nie tworzy luki po remisie. RANK również uwzględnia remisy, ale kolejną pozycję może przesunąć, a ROW_NUMBER zawsze nadaje unikalny numer.
Trzy najlepiej zarabiające osoby w każdym dziale
SELECT id, name, department_id, salary
FROM (
SELECT e.*,
ROW_NUMBER() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS row_number
FROM employees e
) ranked
WHERE row_number <= 3;
PARTITION BY dzieli dane na niezależne grupy, a ORDER BY ustala kolejność w każdej z nich. Jeżeli osoby z taką samą pensją powinny zajmować wspólne miejsce, lepszym wyborem będzie DENSE_RANK zamiast ROW_NUMBER.
Najświeższy rekord dla każdego klienta
SELECT id, customer_id, created_at, amount
FROM (
SELECT o.*,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY created_at DESC, id DESC
) AS row_number
FROM orders o
) latest
WHERE row_number = 1;
Dodanie id DESC jako drugiego kryterium rozstrzyga remis, gdy dwa zamówienia mają identyczny czas utworzenia. To szczegół, który poprawia deterministyczność wyniku i pokazuje, że kandydat myśli o danych, a nie tylko o syntaktycznym przejściu testu.
CTE czy podzapytanie
CTE, czyli konstrukcja WITH, pozwala nazwać etap obliczeń i później odwołać się do niego w głównym zapytaniu. Zwykle poprawia czytelność, ale nie gwarantuje automatycznie lepszej wydajności. Sposób optymalizacji zależy od silnika bazy i konkretnego zapytania.
WITH monthly_sales AS (
SELECT customer_id, SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id
)
SELECT *
FROM monthly_sales
WHERE total_amount > 1000;
Na rozmowie nie wystarczy powiedzieć, że CTE jest „szybsze”. Trafniejsza odpowiedź brzmi, że służy głównie do porządkowania logiki, a szybkość trzeba sprawdzić planem wykonania i pomiarem.
Indeksy i wydajność bez zgadywania
Po co tworzy się indeks
Indeks jest dodatkową strukturą, która pomaga szybciej odnaleźć rekordy według określonych kolumn. Największą korzyść daje przy często używanych filtrach, połączeniach i sortowaniu, ale nie jest darmowy.
Każdy indeks zajmuje miejsce i może spowalniać INSERT, UPDATE oraz DELETE, ponieważ musi zostać zaktualizowany. Nie tworzę więc indeksu na każdej kolumnie „na wszelki wypadek”. Najpierw sprawdzam rzeczywiste zapytania i plan ich wykonania.
Co pokazuje EXPLAIN
EXPLAIN pokazuje, jak silnik planuje wykonać zapytanie. W zależności od bazy można zobaczyć między innymi sposób skanowania tabeli, użyte indeksy, kolejność JOIN-ów, szacowaną liczbę wierszy i koszt operacji.
Jeśli tabela ma kilka tysięcy rekordów, pełne skanowanie może być całkowicie akceptowalne. Przy milionach wierszy ten sam plan może stać się problemem. Dlatego unikam odpowiedzi „indeks zawsze przyspiesza” i mówię o selektywności danych, rozmiarze tabeli oraz konkretnym planie.
Typowe przyczyny wolnych zapytań
- pobieranie wszystkich kolumn przez
SELECT *, choć potrzebne są tylko dwie, - brak indeksu na kolumnach używanych w JOIN lub często stosowanym filtrze,
- funkcja na indeksowanej kolumnie, na przykład
YEAR(created_at)w warunku, - łączenie tabel przed odfiltrowaniem dużej części danych,
- niepotrzebne sortowanie i deduplikowanie przez
UNION, - zwracanie ogromnego wyniku, gdy aplikacja potrzebuje tylko pierwszych rekordów.
Trzeba jednak zachować ostrożność. Przepisanie zapytania bez sprawdzenia planu może niczego nie poprawić, a czasem pogorszyć sytuację. Wydajność oceniam na danych zbliżonych do produkcyjnych, bo mała tabela testowa potrafi ukryć realny problem.
Transakcje, spójność i różnice między bazami
Co oznacza ACID
ACID opisuje właściwości transakcji. Atomicity oznacza wykonanie całości albo wycofanie całości, Consistency pilnuje spójności, Isolation ogranicza wpływ równoległych operacji, a Durability zapewnia trwałość zatwierdzonych zmian.
Przykładem jest przelew. Zmniejszenie salda na jednym koncie i zwiększenie go na drugim powinno znaleźć się w jednej transakcji. Jeśli druga operacja się nie powiedzie, pierwsza również powinna zostać wycofana.
BEGIN;
UPDATE accounts
SET balance = balance - 100
WHERE id = 1;
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
COMMIT;
W aplikacji trzeba jeszcze obsłużyć błąd i wykonać ROLLBACK. Sama obecność słowa BEGIN nie gwarantuje poprawności, jeśli program zatwierdza transakcję mimo nieudanego zapytania.
Poziomy izolacji i zakleszczenia
Poziom izolacji określa, jak dużo zmian z innych transakcji może zobaczyć bieżąca transakcja. Wyższa izolacja zwykle zwiększa bezpieczeństwo odczytu, ale może też ograniczyć współbieżność i zwiększyć ryzyko blokad.
| Zjawisko | Znaczenie |
|---|---|
| Dirty read | odczyt danych, które inna transakcja jeszcze może wycofać |
| Non-repeatable read | ten sam rekord zwraca różne wartości w jednej transakcji |
| Phantom read | ponowne wykonanie warunku zwraca dodatkowe lub znikające wiersze |
| Deadlock | dwie transakcje wzajemnie czekają na zwolnienie blokad |
W pytaniu o deadlock podkreślam, że rozwiązaniem nie jest tylko „zwiększenie timeoutu”. Pomagają krótkie transakcje, stała kolejność aktualizacji tabel, właściwe indeksy i mechanizm ponawiania operacji po wykryciu zakleszczenia.
Przeczytaj również: Zabezpieczenie bazy danych - 6 warstw ochrony, które musisz znać
DELETE, TRUNCATE i DROP
DELETE usuwa wybrane wiersze i może korzystać z warunku WHERE. TRUNCATE usuwa zawartość całej tabeli w sposób zależny od silnika, a DROP usuwa tabelę wraz z jej definicją.
Nie przedstawiam tych poleceń jako całkowicie wymiennych. Ich zachowanie dotyczące transakcji, wyzwalaczy, blokad i liczników autoinkrementacji różni się między PostgreSQL, MySQL i SQL Server. Na rozmowie warto zaznaczyć zależność od konkretnego silnika, zamiast udzielać absolutnej odpowiedzi.
Jak odpowiadać, żeby pokazać praktyczne rozumienie
Dobra odpowiedź techniczna nie musi być długa. Najczęściej stosuję prosty schemat, w którym najpierw podaję definicję, potem mały przykład, a na końcu wspominam o ograniczeniu albo wyjątku.
- Doprecyzuj założenia, na przykład sposób traktowania duplikatów i NULL.
- Napisz najprostszą poprawną wersję zapytania.
- Sprawdź wynik na małym przykładzie, najlepiej także dla pustego zbioru.
- Omów wydajność, jeśli dane mogą być duże.
- Wskaż różnice dialektu, gdy korzystasz z funkcji charakterystycznej dla konkretnej bazy.
Podczas zadania na żywo mówię na głos, co robię. Jeżeli widzę tabelę zamówień i pytanie o „ostatnie zamówienie klienta”, od razu pytam, co zrobić przy remisie i czy klient bez zamówień ma pojawić się w wyniku. Takie pytania pokazują dojrzałość analityczną lepiej niż szybkie napisanie przypadkowego JOIN-a.
Przed rozmową ćwiczę szczególnie pięć typów problemów: duplikaty, rekordy bez dopasowania, drugi największy wynik, top N w grupie oraz ostatni rekord według daty. Te zadania dobrze pokrywają JOIN-y, agregacje, funkcje okna i warunki brzegowe.
Nie uczę się też odpowiedzi zależnych od jednego silnika jako uniwersalnych prawd. LIMIT, TOP, FETCH FIRST, składnia dat czy obsługa autoinkrementacji mogą się różnić. Jeśli nie znam dialektu, zaznaczam założenie i proponuję wariant dla używanej bazy.
Co przećwiczyć dzień przed rozmową z SQL
Najlepsza powtórka nie polega na przeczytaniu kolejnej listy definicji. Przygotuj mały schemat z tabelami klientów, zamówień, produktów i pracowników, a potem rozwiązuj zadania bez podpowiedzi przez 60-90 minut.
- połącz co najmniej trzy tabele i wyjaśnij, dlaczego wybrałeś dany JOIN,
- napisz raport z
GROUP BYi warunkiem wHAVING, - znajdź duplikaty oraz rekordy bez powiązania,
- wyznacz trzy najlepsze wyniki w każdej grupie za pomocą funkcji okna,
- porównaj prostą wersję zapytania z planem
EXPLAIN, - opowiedz, jak zabezpieczyć kilka zmian wykonywanych razem w transakcji.
Jeżeli potrafisz nie tylko napisać zapytanie, ale też wyjaśnić jego wynik, ograniczenia i koszt wykonania, jesteś przygotowany na większość rozmów na poziomie juniora i solidną część pytań mid-level. Największą przewagę daje spokojne rozumowanie na danych, a nie znajomość rzadko używanej funkcji na pamięć.
