SQL na Rozmowie Rekrutacyjnej - Od SELECT do Optymalizacji

Sławomir Plamowski 17 min czytania
backend bazy-danych interview-questions optymalizacja postgresql sql

"Napisz zapytanie łączące trzy tabele z agregacją." To pozornie proste polecenie potrafi zdemaskować nawet kandydatów swobodnie dyskutujących o mikrousługach i Kubernetes. SQL to fundament, na którym stoi większość aplikacji biznesowych, a mimo to wielu developerów traktuje go po macoszemu.

Na rozmowach rekrutacyjnych solidna znajomość SQL odróżnia programistów, którzy naprawdę rozumieją jak działają dane, od tych, którzy tylko przeklejają kod z StackOverflow. W tym artykule znajdziesz najważniejsze pytania SQL wraz z odpowiedziami na poziomie, który zrobi wrażenie na rekruterze.

Odpowiedź w 30 sekund

Gdy rekruter pyta "Czym jest SQL?", oto zwięzła odpowiedź:

SQL to język służący do komunikacji z relacyjnymi bazami danych. Pozwala na pobieranie danych (SELECT), ich modyfikację (INSERT, UPDATE, DELETE) oraz definiowanie struktury bazy (CREATE, ALTER, DROP). Kluczowe koncepcje to tabele powiązane relacjami, klucze główne i obce zapewniające integralność, oraz możliwość łączenia danych z wielu tabel przez JOIN.

Poczekaj na pytania uzupełniające.

Odpowiedź w 2 minuty (jeśli chcą więcej)

Jeśli poproszą o rozwinięcie:

SQL, czyli Structured Query Language, to deklaratywny język - mówisz bazie CO chcesz uzyskać, nie JAK to zrobić. Silnik bazy sam optymalizuje wykonanie zapytania.

Komendy SQL dzielą się na cztery kategorie: DDL (Data Definition Language) do definiowania struktury - CREATE, ALTER, DROP. DML (Data Manipulation Language) do manipulacji danymi - SELECT, INSERT, UPDATE, DELETE. DCL (Data Control Language) do kontroli dostępu - GRANT, REVOKE. TCL (Transaction Control Language) do zarządzania transakcjami - COMMIT, ROLLBACK, SAVEPOINT.

Relacyjne bazy danych przechowują dane w tabelach powiązanych kluczami obcymi. To pozwala uniknąć duplikacji danych i zapewnia integralność. Na przykład zamiast powtarzać dane klienta przy każdym zamówieniu, przechowujesz klienta raz i łączysz go z zamówieniami przez klucz obcy.

JOIN - Serce SQL

Łączenie tabel to fundament pracy z relacyjnymi bazami danych. Kandydaci, którzy sprawnie operują różnymi typami JOIN pokazują, że naprawdę rozumieją model relacyjny.

Typy JOIN wizualnie

flowchart LR subgraph "INNER JOIN" A1[Tabela A] --> C1((Część wspólna)) B1[Tabela B] --> C1 end subgraph "LEFT JOIN" A2[Tabela A] --> C2((Cała A + dopasowane z B)) B2[Tabela B] -.-> C2 end subgraph "FULL OUTER JOIN" A3[Tabela A] --> C3((Wszystkie z obu)) B3[Tabela B] --> C3 end style C1 fill:#e8f5e9 style C2 fill:#e3f2fd style C3 fill:#fff3e0

Przykłady kodu

Załóżmy, że mamy dwie tabele - pracownicy i działy:

-- Tabela pracowników
CREATE TABLE employees (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    department_id INT,
    salary DECIMAL(10,2)
);

-- Tabela działów
CREATE TABLE departments (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    budget DECIMAL(12,2)
);

-- Przykładowe dane
INSERT INTO departments VALUES (1, 'IT', 500000), (2, 'HR', 200000), (3, 'Marketing', 300000);
INSERT INTO employees VALUES
    (1, 'Anna Kowalska', 1, 8000),
    (2, 'Jan Nowak', 1, 7500),
    (3, 'Maria Wiśniewska', 2, 6000),
    (4, 'Piotr Zieliński', NULL, 5500);  -- pracownik bez przypisanego działu

Teraz pokażę różnicę między typami JOIN na przykładzie:

-- INNER JOIN: tylko pracownicy Z przypisanym działem
-- Piotr Zieliński nie pojawi się w wynikach
SELECT e.name, d.name as department
FROM employees e
INNER JOIN departments d ON e.department_id = d.id;

-- Wynik:
-- Anna Kowalska    | IT
-- Jan Nowak        | IT
-- Maria Wiśniewska | HR
-- LEFT JOIN: wszyscy pracownicy, nawet bez działu
-- Piotr pojawi się z NULL w kolumnie department
SELECT e.name, d.name as department
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id;

-- Wynik:
-- Anna Kowalska    | IT
-- Jan Nowak        | IT
-- Maria Wiśniewska | HR
-- Piotr Zieliński  | NULL
-- RIGHT JOIN: wszystkie działy, nawet te bez pracowników
-- Marketing pojawi się z NULL w kolumnie name
SELECT e.name, d.name as department
FROM employees e
RIGHT JOIN departments d ON e.department_id = d.id;

-- Wynik:
-- Anna Kowalska    | IT
-- Jan Nowak        | IT
-- Maria Wiśniewska | HR
-- NULL             | Marketing

Klasyczny problem: znajdź rekordy bez dopasowania

To częste pytanie rekrutacyjne - znajdź wszystkie działy, które nie mają żadnych pracowników:

-- Rozwiązanie z LEFT JOIN i WHERE IS NULL
SELECT d.name as department_without_employees
FROM departments d
LEFT JOIN employees e ON d.id = e.department_id
WHERE e.id IS NULL;

-- Alternatywnie z NOT EXISTS (często wydajniejsze)
SELECT d.name
FROM departments d
WHERE NOT EXISTS (
    SELECT 1 FROM employees e WHERE e.department_id = d.id
);

Wzorzec LEFT JOIN + WHERE IS NULL to jeden z najczęściej sprawdzanych na rozmowach. Kandydaci, którzy od razu wiedzą jak go użyć, pokazują praktyczne doświadczenie.

Funkcje agregujące i GROUP BY

Agregacja danych to chleb powszedni analityki. Rekruterzy często sprawdzają czy kandydat rozumie różnicę między filtrowaniem wierszy (WHERE) a filtrowaniem grup (HAVING).

Odpowiedź w 30 sekund

Funkcje agregujące (COUNT, SUM, AVG, MIN, MAX) operują na zbiorach wierszy i zwracają pojedynczą wartość. GROUP BY grupuje wiersze o tych samych wartościach w określonych kolumnach. WHERE filtruje przed grupowaniem, HAVING po grupowaniu - HAVING może używać funkcji agregujących, WHERE nie może.

Przykłady z wyjaśnieniem

-- Podstawowa agregacja: średnia pensja w każdym dziale
SELECT
    d.name as department,
    COUNT(*) as employee_count,
    AVG(e.salary) as avg_salary,
    SUM(e.salary) as total_salaries
FROM employees e
JOIN departments d ON e.department_id = d.id
GROUP BY d.name;

Tu robi się ciekawie - zobacz różnicę między WHERE i HAVING:

-- WHERE: filtruje PRZED grupowaniem
-- Pomijamy pracowników z pensją poniżej 6000 ZANIM policzymy statystyki
SELECT d.name, COUNT(*) as count, AVG(e.salary) as avg_salary
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE e.salary >= 6000  -- filtruje pojedyncze wiersze
GROUP BY d.name;

-- HAVING: filtruje PO grupowaniu
-- Pokazujemy tylko działy, które mają więcej niż 1 pracownika
SELECT d.name, COUNT(*) as count, AVG(e.salary) as avg_salary
FROM employees e
JOIN departments d ON e.department_id = d.id
GROUP BY d.name
HAVING COUNT(*) > 1;  -- filtruje grupy

Klasyczny błąd

Jednym z najczęstszych błędów jest próba użycia kolumny, która nie jest ani w GROUP BY, ani w funkcji agregującej:

-- ŹLE: e.name nie jest zgrupowane ani zagregowane
SELECT d.name, e.name, AVG(e.salary)
FROM employees e
JOIN departments d ON e.department_id = d.id
GROUP BY d.name;
-- Błąd: column "e.name" must appear in the GROUP BY clause
-- or be used in an aggregate function

-- DOBRZE: wszystkie kolumny są albo w GROUP BY, albo zagregowane
SELECT d.name, STRING_AGG(e.name, ', ') as employees, AVG(e.salary)
FROM employees e
JOIN departments d ON e.department_id = d.id
GROUP BY d.name;

Indeksy - klucz do wydajności

Indeksy to temat, który odróżnia juniorów od seniorów. Juniorzy wiedzą, że "indeksy przyspieszają zapytania". Seniorzy wiedzą KIEDY je stosować, a kiedy nie.

Odpowiedź w 30 sekund

Indeks to struktura danych - najczęściej B-tree - która przyspiesza wyszukiwanie kosztem dodatkowego miejsca na dysku i wolniejszych operacji zapisu. Twórz indeksy na kolumnach używanych w WHERE, JOIN i ORDER BY. Unikaj indeksowania kolumn z małą liczbą unikalnych wartości i tabel z częstymi zapisami.

Jak działa indeks

flowchart TB subgraph "Bez indeksu - Full Table Scan" T1[Tabela 1M wierszy] --> S1[Sprawdź wiersz 1] S1 --> S2[Sprawdź wiersz 2] S2 --> S3[...] S3 --> SN[Sprawdź wiersz 1000000] end subgraph "Z indeksem B-tree" I[Indeks] --> N1[Węzeł główny] N1 --> N2[Węzeł pośredni] N2 --> L[Liść - wskaźnik do wiersza] L --> R[Bezpośredni dostęp do danych] end style T1 fill:#ffcdd2 style I fill:#e8f5e9 style R fill:#e8f5e9

Tworzenie i analiza indeksów

-- Tworzenie indeksu na kolumnie często używanej w WHERE
CREATE INDEX idx_employees_department ON employees(department_id);

-- Indeks złożony - kolejność kolumn ma znaczenie!
-- Ten indeks jest optymalny dla zapytań filtrujących
-- najpierw po department_id, potem po salary
CREATE INDEX idx_emp_dept_salary ON employees(department_id, salary);

-- Indeks unikalny - gwarantuje unikalność wartości
CREATE UNIQUE INDEX idx_employees_email ON employees(email);

Kiedy indeks NIE pomoże

Pokażę na przykładzie sytuacje, w których indeks nie zostanie użyty:

-- 1. Funkcja na kolumnie - indeks na 'created_at' NIE zostanie użyty
SELECT * FROM orders WHERE YEAR(created_at) = 2024;

-- ROZWIĄZANIE: przepisz zapytanie
SELECT * FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';

-- 2. Negacja - indeks zazwyczaj NIE pomoże przy NOT
SELECT * FROM employees WHERE department_id != 5;

-- 3. LIKE z wildcardem na początku
SELECT * FROM employees WHERE name LIKE '%ski';  -- pełny skan tabeli
SELECT * FROM employees WHERE name LIKE 'Kowal%'; -- użyje indeksu

Analiza planu wykonania

-- PostgreSQL: EXPLAIN ANALYZE pokazuje plan i rzeczywiste czasy
EXPLAIN ANALYZE
SELECT e.name, d.name
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE e.salary > 7000;

-- Szukaj:
-- "Seq Scan" - pełny skan tabeli (może być problemem na dużych tabelach)
-- "Index Scan" - użycie indeksu (dobrze)
-- "Index Only Scan" - dane pobrane tylko z indeksu (najlepiej)
-- "Nested Loop" vs "Hash Join" vs "Merge Join" - różne strategie łączenia

Transakcje i ACID

Transakcje to fundament niezawodności baz danych. Na rozmowach często pada pytanie o właściwości ACID i poziomy izolacji.

Odpowiedź w 30 sekund

ACID to cztery właściwości transakcji: Atomicity - transakcja wykonuje się w całości lub wcale. Consistency - baza przechodzi z jednego spójnego stanu w drugi. Isolation - współbieżne transakcje nie widzą swoich częściowych zmian. Durability - zatwierdzone zmiany przetrwają awarię systemu.

Odpowiedź w 2 minuty

Weźmy przykład przelewu bankowego - przelewacie 1000 zł z konta A na konto B. To muszą być dwie operacje: odjęcie z A i dodanie do B.

Atomicity gwarantuje, że albo obie się wykonają, albo żadna - nie ma sytuacji, że pieniądze znikną z A, ale nie pojawią się na B.

Consistency oznacza, że suma wszystkich kont przed i po transakcji jest taka sama - nie tworzymy ani nie niszczymy pieniędzy.

Isolation zapewnia, że jeśli jednocześnie wykonuję przelew i ktoś sprawdza saldo, nie zobaczy niepełnego stanu (pieniądze już zdjęte z A, ale jeszcze nie dodane do B).

Durability gwarantuje, że po COMMIT, nawet jeśli serwer się zawiesi sekundę później, transakcja nie zostanie utracona.

Poziomy izolacji

flowchart TB subgraph "Poziomy izolacji - od najsłabszego do najsilniejszego" RU[READ UNCOMMITTED] --> RC[READ COMMITTED] RC --> RR[REPEATABLE READ] RR --> S[SERIALIZABLE] end subgraph "Problemy rozwiązywane" P1[Dirty Read] -.-> RC P2[Non-repeatable Read] -.-> RR P3[Phantom Read] -.-> S end style RU fill:#ffcdd2 style S fill:#e8f5e9
-- Ustawianie poziomu izolacji
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- Przykład transakcji z obsługą błędów
BEGIN;

-- Spróbuj wykonać przelew
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;

-- Sprawdź czy saldo nie jest ujemne
DO $$
BEGIN
    IF (SELECT balance FROM accounts WHERE id = 1) < 0 THEN
        RAISE EXCEPTION 'Niewystarczające środki';
    END IF;
END $$;

COMMIT;  -- lub ROLLBACK jeśli wystąpił błąd

Klasyczny problem: Deadlock

-- Transakcja 1                    -- Transakcja 2
BEGIN;                              BEGIN;
UPDATE accounts                     UPDATE accounts
SET balance = balance - 100         SET balance = balance - 50
WHERE id = 1;                       WHERE id = 2;
-- Czeka na konto 2                 -- Czeka na konto 1
UPDATE accounts                     UPDATE accounts
SET balance = balance + 100         SET balance = balance + 50
WHERE id = 2;                       WHERE id = 1;
-- DEADLOCK!                        -- DEADLOCK!

Rozwiązanie: zawsze blokuj zasoby w tej samej kolejności:

-- Obie transakcje najpierw blokują konto o niższym ID
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

Funkcje okna (Window Functions)

Funkcje okna to zaawansowane narzędzie, które pozwala na obliczenia na zbiorze wierszy powiązanych z bieżącym wierszem, bez redukcji liczby wierszy w wyniku.

Odpowiedź w 30 sekund

Funkcje okna wykonują obliczenia na grupie wierszy (oknie) powiązanych z bieżącym wierszem, ale w przeciwieństwie do GROUP BY, nie redukują liczby wierszy. Każdy wiersz zachowuje swoją tożsamość. Używamy ich do rankingów (ROW_NUMBER, RANK), sum bieżących, porównań z poprzednimi/następnymi wierszami (LAG, LEAD).

ROW_NUMBER, RANK i DENSE_RANK

-- Załóżmy że mamy wyniki sprzedaży
CREATE TABLE sales (
    salesperson VARCHAR(50),
    region VARCHAR(50),
    amount DECIMAL(10,2)
);

INSERT INTO sales VALUES
    ('Anna', 'Północ', 10000),
    ('Jan', 'Północ', 10000),
    ('Maria', 'Północ', 8000),
    ('Piotr', 'Południe', 12000),
    ('Ewa', 'Południe', 9000);

-- Różnica między funkcjami rankingowymi
SELECT
    salesperson,
    region,
    amount,
    ROW_NUMBER() OVER (ORDER BY amount DESC) as row_num,  -- unikalne numery
    RANK() OVER (ORDER BY amount DESC) as rank,           -- ta sama ranga dla remisów, luki
    DENSE_RANK() OVER (ORDER BY amount DESC) as dense     -- ta sama ranga, bez luk
FROM sales;

-- Wynik:
-- salesperson | region   | amount | row_num | rank | dense
-- Piotr       | Południe | 12000  | 1       | 1    | 1
-- Anna        | Północ   | 10000  | 2       | 2    | 2
-- Jan         | Północ   | 10000  | 3       | 2    | 2    <- remis!
-- Ewa         | Południe | 9000   | 4       | 4    | 3    <- rank=4 (luka), dense=3 (bez luki)
-- Maria       | Północ   | 8000   | 5       | 5    | 4

PARTITION BY - okna w grupach

-- Ranking sprzedawców W RAMACH każdego regionu
SELECT
    salesperson,
    region,
    amount,
    RANK() OVER (PARTITION BY region ORDER BY amount DESC) as region_rank
FROM sales;

-- Wynik:
-- salesperson | region   | amount | region_rank
-- Anna        | Północ   | 10000  | 1
-- Jan         | Północ   | 10000  | 1
-- Maria       | Północ   | 8000   | 3
-- Piotr       | Południe | 12000  | 1
-- Ewa         | Południe | 9000   | 2

Sumy bieżące i porównania z poprzednimi wierszami

-- Suma bieżąca sprzedaży
SELECT
    salesperson,
    amount,
    SUM(amount) OVER (ORDER BY amount) as running_total
FROM sales;

-- Porównanie z poprzednim wierszem (LAG) i następnym (LEAD)
SELECT
    salesperson,
    amount,
    LAG(amount, 1) OVER (ORDER BY amount) as previous_amount,
    LEAD(amount, 1) OVER (ORDER BY amount) as next_amount,
    amount - LAG(amount, 1) OVER (ORDER BY amount) as diff_from_previous
FROM sales;

Klasyczny problem: Top N w każdej grupie

Znajdź 2 najlepszych sprzedawców w każdym regionie:

WITH ranked_sales AS (
    SELECT
        salesperson,
        region,
        amount,
        ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) as rn
    FROM sales
)
SELECT salesperson, region, amount
FROM ranked_sales
WHERE rn <= 2;

Podzapytania i CTE

Common Table Expressions (CTE) to elegancki sposób na strukturyzowanie złożonych zapytań. Rekruterzy sprawdzają czy kandydat potrafi pisać czytelny, możliwy do utrzymania kod SQL.

Odpowiedź w 30 sekund

CTE (WITH) to nazwane podzapytanie definiowane przed głównym zapytaniem. Poprawia czytelność, pozwala na reużycie logiki w ramach zapytania, i umożliwia zapytania rekurencyjne. W przeciwieństwie do podzapytań inline, CTE jest czytelniejsze i łatwiejsze do debugowania.

Porównanie: Podzapytanie vs CTE

-- Podzapytanie inline - trudniejsze do czytania przy złożonych zapytaniach
SELECT e.name, e.salary, dept_avg.avg_salary
FROM employees e
JOIN (
    SELECT department_id, AVG(salary) as avg_salary
    FROM employees
    GROUP BY department_id
) dept_avg ON e.department_id = dept_avg.department_id
WHERE e.salary > dept_avg.avg_salary;

-- To samo z CTE - bardziej czytelne
WITH department_averages AS (
    SELECT department_id, AVG(salary) as avg_salary
    FROM employees
    GROUP BY department_id
)
SELECT e.name, e.salary, da.avg_salary
FROM employees e
JOIN department_averages da ON e.department_id = da.department_id
WHERE e.salary > da.avg_salary;

Rekurencyjne CTE

Rekurencyjne CTE to potężne narzędzie do pracy z danymi hierarchicznymi:

-- Struktura organizacyjna - kto komu podlega
CREATE TABLE org_structure (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    manager_id INT REFERENCES org_structure(id)
);

INSERT INTO org_structure VALUES
    (1, 'CEO Jan', NULL),
    (2, 'CTO Anna', 1),
    (3, 'CFO Piotr', 1),
    (4, 'Dev Lead Maria', 2),
    (5, 'Dev Senior Tomek', 4),
    (6, 'Dev Junior Ewa', 4);

-- Znajdź całą hierarchię pod CTO
WITH RECURSIVE hierarchy AS (
    -- Punkt startowy (anchor)
    SELECT id, name, manager_id, 1 as level
    FROM org_structure
    WHERE name = 'CTO Anna'

    UNION ALL

    -- Część rekurencyjna
    SELECT o.id, o.name, o.manager_id, h.level + 1
    FROM org_structure o
    JOIN hierarchy h ON o.manager_id = h.id
)
SELECT REPEAT('  ', level - 1) || name as org_chart, level
FROM hierarchy
ORDER BY level;

-- Wynik:
-- org_chart          | level
-- CTO Anna           | 1
--   Dev Lead Maria   | 2
--     Dev Senior Tomek | 3
--     Dev Junior Ewa   | 3

SQL Injection - bezpieczeństwo

To temat, który musi znać każdy programista pracujący z bazami danych. Brak wiedzy o SQL injection to poważna czerwona flaga.

Odpowiedź w 30 sekund

SQL injection to atak polegający na wstrzyknięciu złośliwego kodu SQL przez dane wejściowe. Obrona: zawsze używaj parametryzowanych zapytań, nigdy nie konkatenuj danych użytkownika z SQL, waliduj dane wejściowe po stronie serwera, stosuj zasadę najmniejszych uprawnień dla kont bazodanowych.

Przykład ataku i obrony

-- ŹRÓDŁO PROBLEMU: konkatenacja stringów
-- PHP/pseudokod - NIE RÓB TEGO!
$query = "SELECT * FROM users WHERE username = '" + $userInput + "'";

-- Jeśli użytkownik wpisze: admin' OR '1'='1
-- Zapytanie staje się:
SELECT * FROM users WHERE username = 'admin' OR '1'='1';
-- Zwraca WSZYSTKICH użytkowników!

-- Jeszcze gorzej, użytkownik może wpisać: admin'; DROP TABLE users; --
-- Zapytanie:
SELECT * FROM users WHERE username = 'admin'; DROP TABLE users; --';
-- Usunięcie całej tabeli!
-- ROZWIĄZANIE: Parametryzowane zapytania (prepared statements)

-- PostgreSQL/Node.js
const result = await client.query(
    'SELECT * FROM users WHERE username = $1',
    [userInput]  // parametr przekazany oddzielnie
);

-- Python/psycopg2
cursor.execute(
    "SELECT * FROM users WHERE username = %s",
    (user_input,)  # krotka z parametrami
)

-- Java/JDBC
PreparedStatement stmt = conn.prepareStatement(
    "SELECT * FROM users WHERE username = ?"
);
stmt.setString(1, userInput);
ResultSet rs = stmt.executeQuery();

Parametryzowane zapytania działają, bo wartość parametru jest traktowana jako DANE, nie jako część kodu SQL. Nawet jeśli zawiera znaki specjalne SQL, zostanie poprawnie zeskejpowana.

Na co rekruterzy naprawdę zwracają uwagę

Po przeprowadzeniu setek rozmów, mogę powiedzieć że sprawdzam:

Zrozumienie modelu relacyjnego - czy kandydat wie dlaczego dane są rozbite na wiele tabel, co to są klucze obce i jak działają relacje. Ktoś kto pisze SELECT * z jednej wielkiej tabeli nie rozumie podstaw.

Praktyczne doświadczenie z JOINami - czy bez wahania wybierze właściwy typ JOIN, czy rozumie różnicę między INNER i LEFT, czy potrafi napisać zapytanie łączące 3-4 tabele.

Świadomość wydajności - czy wie że indeksy istnieją, kiedy je stosować, czy potrafi przeczytać EXPLAIN. Nie wymagam optymalizacji na poziomie DBA, ale podstawowa świadomość jest konieczna.

Znajomość transakcji - ACID, poziomy izolacji, kiedy użyć COMMIT/ROLLBACK. To fundamenty pracy z danymi w produkcji.

Bezpieczeństwo - SQL injection i parametryzowane zapytania. Brak tej wiedzy to dyskwalifikacja dla pozycji backend.

Praktyka na koniec

Przetestuj się przed rozmową:

1. Napisz zapytanie, które znajdzie wszystkich pracowników zarabiających więcej niż średnia w ich dziale.

2. Jak znajdziesz duplikaty w tabeli na podstawie określonych kolumn?

3. Masz tabelę zamówień z kolumnami: order_id, customer_id, order_date, amount. Napisz zapytanie pokazujące dla każdego klienta sumę zamówień i różnicę względem poprzedniego miesiąca.

4. Co jest nie tak z tym zapytaniem?

SELECT department, MAX(salary), name
FROM employees
GROUP BY department;

Odpowiedzi:

1.

WITH dept_avg AS (
    SELECT department_id, AVG(salary) as avg_salary
    FROM employees
    GROUP BY department_id
)
SELECT e.name, e.salary, e.department_id, da.avg_salary
FROM employees e
JOIN dept_avg da ON e.department_id = da.department_id
WHERE e.salary > da.avg_salary;

2.

SELECT email, COUNT(*) as count
FROM users
GROUP BY email
HAVING COUNT(*) > 1;

3.

WITH monthly_totals AS (
    SELECT
        customer_id,
        DATE_TRUNC('month', order_date) as month,
        SUM(amount) as total
    FROM orders
    GROUP BY customer_id, DATE_TRUNC('month', order_date)
)
SELECT
    customer_id,
    month,
    total,
    total - LAG(total) OVER (PARTITION BY customer_id ORDER BY month) as diff_vs_prev_month
FROM monthly_totals;

4. Kolumna name nie jest ani w GROUP BY, ani zagregowana. Dla grupy wielu pracowników w dziale, baza nie wie którego name wybrać.


Zobacz też


Chcesz więcej pytań rekrutacyjnych z SQL?

Ten artykuł to tylko wycinek materiału z naszego kompletnego przewodnika po rozmowach rekrutacyjnych z baz danych. Uzyskaj dostęp do 100+ pytań obejmujących:

  • Zaawansowane optymalizacje zapytań
  • Projektowanie schematów i normalizację
  • Replikację i sharding
  • Procedury składowane i triggery
  • Różnice między silnikami baz danych

Kup pełny dostęp do wszystkich pytań →

Lub wypróbuj nasz darmowy podgląd pytań, żeby zobaczyć więcej pytań w tym stylu.


Napisane przez zespół Flipcards, na podstawie ponad 15 lat doświadczenia w IT i setek przeprowadzonych rozmów technicznych w firmach takich jak BNY Mellon, UBS i wiodących fintechach.

Chcesz więcej pytań rekrutacyjnych?

To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.