📋 Podstawy SQL

Schemat HR, kluczowe funkcje, podzapytania, JOIN, GROUP BY

⚡ Kluczowe fakty do zapamiętania
  • ROWNUM pułapka: ROWNUM jest przypisywany PRZED sortowaniem. Jeśli napiszesz WHERE ROWNUM <= 5 ORDER BY salary — dostaniesz 5 przypadkowych wierszy, a NIE 5 najlepiej zarabiających. Zawsze wstaw ORDER BY do podzapytania, ROWNUM filtruj na zewnątrz.
  • WHERE vs HAVING: WHERE filtruje wiersze PRZED grupowaniem (nie może używać SUM, AVG itd.). HAVING filtruje grupy PO grupowaniu (może używać agregatów). Kolejność wykonania: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY.
  • NOT IN + NULL = brak wyników! Jeśli podzapytanie w NOT IN zwróci chociaż jeden NULL, całe wyrażenie NOT IN jest FAŁSZYWE dla wszystkich wierszy. Zawsze pisz: WHERE department_id IS NOT NULL w podzapytaniu NOT IN.
  • NVL(commission_pct, 0) = zamień NULL na 0. Bez NVL wyrażenie salary + commission_pct*salary da NULL gdy commission_pct jest NULL — NULL zarazi każde działanie arytmetyczne.
  • LEFT JOIN vs JOIN (INNER JOIN): JOIN pomija wiersze bez dopasowania (działy bez pracowników nie pojawią się). LEFT JOIN zachowuje wszystkie wiersze z lewej tabeli — działy bez pracowników pojawiają się z NULL w kolumnach pracownika.
  • MONTHS_BETWEEN(data1, data2) = liczba miesięcy między datami. Użyj TRUNC aby dostać całkowite miesiące. Użyj EXTRACT(YEAR FROM data) aby wyciągnąć rok. TO_CHAR(data, 'YYYY') = rok jako tekst.

Schemat HR — tabele

TabelaKluczowe kolumny
employeesemployee_id, first_name, last_name, salary, commission_pct, department_id, job_id, manager_id, hire_date
departmentsdepartment_id, department_name, location_id
locationslocation_id, city, country_id
countriescountry_id, country_name, region_id
regionsregion_id, region_name
jobsjob_id, job_title, min_salary, max_salary

ROWNUM vs FETCH FIRST

Dwa sposoby ograniczania liczby wierszy wynikowych. ROWNUM działa wszędzie, FETCH FIRST wymaga Oracle 12c+.

Stary sposób — ROWNUM (działa wszędzie)
-- UWAGA: ORDER BY musi być w podzapytaniu, nie na zewnątrz!
SELECT * FROM (
    SELECT employee_id, first_name, last_name, salary
    FROM hr.employees
    ORDER BY salary DESC
)
WHERE ROWNUM <= 5;
Nowy sposób — FETCH FIRST (Oracle 12c+)
SELECT employee_id, first_name, last_name, salary
FROM hr.employees
ORDER BY salary DESC
FETCH FIRST 5 ROWS ONLY;

-- Top 10% najlepiej zarabiających
SELECT * FROM hr.employees
ORDER BY salary DESC
FETCH FIRST 10 PERCENT ROWS ONLY;
🚨
Pułapka z ROWNUM
Nie pisz WHERE ROWNUM <= 5 ORDER BY salary DESC — ROWNUM jest przypisywany PRZED sortowaniem! Zawsze wstaw ORDER BY do podzapytania wewnętrznego, a ROWNUM filtruj na zewnątrz.
⚖️ Porównanie wynikówROWNUM — złe vs dobre użycie
ZŁE: WHERE ROWNUM <= 3 ORDER BY salary DESC
ROWNUMNAMESALARY
1Fay6000
2Hartstein13000
3Whalen4400
DOBRE: WHERE ROWNUM <= 3 w podzapytaniu z ORDER BY
ROWNUMNAMESALARY
1King24000
2Kochhar17000
3De Haan17000
💡 Lewa: ROWNUM=1,2,3 przypisane PRZED sortowaniem — dostajemy przypadkowych 3 pracowników, potem ich sortujemy. Prawa: podzapytanie najpierw sortuje, zewnętrzne WHERE bierze 3 pierwsze Z posortowanych — naprawdę 3 najwyższe pensje.

Filtrowanie zaawansowane — LIKE, BETWEEN, IN

sql
-- LIKE: wzorce tekstowe (% = dowolna liczba znaków, _ = jeden znak)
SELECT first_name, last_name FROM hr.employees
WHERE last_name LIKE 'S%';          -- nazwisko zaczyna się na S
WHERE last_name LIKE '_a%';         -- druga litera to 'a'
WHERE last_name LIKE '%son';        -- kończy się na 'son'

-- BETWEEN: zakres włącznie (oba końce)
WHERE salary BETWEEN 5000 AND 9000; -- salary >= 5000 AND salary <= 9000

-- IN: lista wartości
WHERE job_id IN ('IT_PROG', 'SA_REP', 'FI_ACCOUNT');

-- Kombinacja:
SELECT first_name, last_name, salary, job_id
FROM hr.employees
WHERE last_name LIKE 'S%'
  AND salary BETWEEN 5000 AND 9000
  AND job_id IN ('IT_PROG', 'SA_REP', 'FI_ACCOUNT');

Funkcje tekstowe

sql
SELECT
    UPPER(last_name)              AS wielkie,      -- KOWALSKI
    LOWER(last_name)              AS male,         -- kowalski
    INITCAP(first_name)           AS pierwsza,     -- Jan
    SUBSTR(first_name, 1, 1)      AS inicjal,      -- J
    INSTR(LOWER(last_name), 'a')  AS poz_a,        -- pozycja litery 'a'
    LENGTH(last_name)             AS dlugosc,
    LPAD(' ', 4*(level-1))        AS wciecie,      -- dla drzew
    first_name || ' ' || last_name AS pelne_imie
FROM hr.employees;

Funkcje datowe i konwersji

sql
SELECT
    TRUNC(MONTHS_BETWEEN(SYSDATE, hire_date))   AS staz_mies,
    EXTRACT(YEAR FROM hire_date)                AS rok_zatrudnienia,
    TO_CHAR(hire_date, 'yyyy')                  AS rok_tekst,
    TO_CHAR(hire_date, 'DD-MON-YYYY')           AS data_format,
    NVL(commission_pct, 0)                      AS prowizja,
    salary + NVL(commission_pct, 0) * salary   AS pensja_z_premia,
    ROUND(AVG(salary), 2)                       AS zaokraglona_sr
FROM hr.employees;

GROUP BY — agregaty i HAVING

GROUP BY grupuje wiersze wg wartości kolumny. Agregaty (SUM, COUNT, AVG, MIN, MAX) działają wewnątrz każdej grupy. HAVING filtruje grupy po agregacji (WHERE filtruje wiersze przed agregacją).

sql
-- Łączne wynagrodzenie i liczba pracowników w każdym dziale:
SELECT d.department_name,
       NVL(SUM(e.salary), 0)    AS laczne_wynagrodzenie,
       COUNT(e.employee_id)     AS liczba_pracownikow
FROM hr.departments d
LEFT JOIN hr.employees e ON d.department_id = e.department_id
GROUP BY d.department_name;

-- Średnie wynagrodzenie w firmie (bez grupowania):
SELECT ROUND(AVG(salary), 2) AS srednia_firma
FROM hr.employees;

-- Działy ze średnią > 8000 (HAVING po agregacji):
SELECT d.department_name,
       ROUND(AVG(e.salary), 2) AS srednia
FROM hr.employees e
JOIN hr.departments d ON e.department_id = d.department_id
GROUP BY d.department_name
HAVING AVG(e.salary) > 8000;

-- Pracownicy z premią (commission_pct IS NOT NULL) wg stanowiska:
SELECT j.job_title,
       COUNT(e.employee_id) AS liczba_z_premia
FROM hr.employees e
JOIN hr.jobs j ON j.job_id = e.job_id
WHERE e.commission_pct IS NOT NULL      -- WHERE przed GROUP BY
GROUP BY j.job_title;

-- Średnia długość nazwiska wg działu:
SELECT d.department_name,
       TRUNC(AVG(LENGTH(e.last_name))) AS sr_dlugosc_nazwiska
FROM hr.employees e
JOIN hr.departments d ON e.department_id = d.department_id
GROUP BY d.department_name;

-- Liczba podwładnych każdego managera (self JOIN + GROUP BY):
SELECT m.employee_id AS manager_id,
       m.first_name, m.last_name,
       COUNT(e.employee_id) AS liczba_podwladnych
FROM hr.employees e
JOIN hr.employees m ON e.manager_id = m.employee_id
GROUP BY m.employee_id, m.first_name, m.last_name;
⚠️
WHERE vs HAVING
WHERE — filtruje wiersze PRZED grupowaniem (nie może używać agregatów)
HAVING — filtruje grupy PO grupowaniu (może używać agregatów)
Kolejność: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
⚖️ Porównanie wynikówWHERE commission_pct IS NOT NULL — różne efekty zależnie od miejsca
WHERE commission_pct IS NOT NULL (przed GROUP BY)
JOB_IDDEPTCOUNT(*)
SA_REP8030
SA_MAN805
HAVING COUNT(CASE WHEN commission_pct IS NOT NULL THEN 1 END) > 0
JOB_IDDEPTWSZYSCYZ_PROWIZJĄ
SA_REP803030
SA_MAN8055
IT_PROG6050
💡 WHERE (lewa): odcina wiersze bez prowizji — nie wiesz ile było WSZYSTKICH. HAVING (prawa): widzisz wszystkich pracowników w grupie, filtrowanie dopiero po zliczeniu.

Podzapytania — skalarne i IN/NOT IN

sql
-- Skalarne: pracownicy zarabiający więcej niż średnia firmy
SELECT first_name, last_name, salary
FROM hr.employees
WHERE salary > (SELECT AVG(salary) FROM hr.employees);

-- Działy w tym samym mieście co pracownik 100:
SELECT d.department_name
FROM hr.departments d
JOIN hr.locations l ON d.location_id = l.location_id
WHERE l.city = (
    SELECT l2.city
    FROM hr.employees e2
    JOIN hr.departments d2 ON e2.department_id = d2.department_id
    JOIN hr.locations l2 ON d2.location_id = l2.location_id
    WHERE e2.employee_id = 100
);

Podzapytania skorelowane

Podzapytanie odwołuje się do wartości z zapytania zewnętrznego — wykonuje się osobno dla każdego wiersza.

sql
-- Pracownicy zarabiający więcej niż średnia w swoim dziale
SELECT e.first_name, e.last_name, e.salary, d.department_name
FROM hr.employees e
JOIN hr.departments d ON e.department_id = d.department_id
WHERE e.salary > (
    SELECT AVG(salary)
    FROM hr.employees
    WHERE department_id = e.department_id  -- korelacja!
);

-- Pracownicy zatrudnieni w tym samym roku co Steven King
SELECT first_name, last_name, hire_date
FROM hr.employees
WHERE EXTRACT(YEAR FROM hire_date) = (
    SELECT EXTRACT(YEAR FROM hire_date)
    FROM hr.employees
    WHERE first_name = 'Steven' AND last_name = 'King'
);

-- Alternatywa z TO_CHAR:
WHERE TO_CHAR(hire_date, 'yyyy') = (
    SELECT TO_CHAR(hire_date, 'yyyy')
    FROM hr.employees WHERE first_name = 'Steven' AND last_name = 'King'
);

Self JOIN — hierarchia manager-pracownik

sql
-- Pracownik i jego przełożony (LEFT JOIN = wszyscy, JOIN = tylko ci z szefem)
SELECT e.first_name || ' ' || e.last_name AS pracownik,
       m.first_name || ' ' || m.last_name AS przelozony
FROM hr.employees e
LEFT JOIN hr.employees m ON e.manager_id = m.employee_id;

-- Pracownicy zarabiający więcej niż ich bezpośredni przełożony
SELECT e.first_name, e.last_name, e.salary,
       m.salary AS pensja_szefa
FROM hr.employees e
JOIN hr.employees m ON e.manager_id = m.employee_id
WHERE e.salary > m.salary;

Wielotabelowe JOIN — cały łańcuch HR

sql
-- Pracownicy z Europy
SELECT e.first_name, e.last_name, r.region_name
FROM hr.employees e
JOIN hr.departments d ON e.department_id = d.department_id
JOIN hr.locations l    ON d.location_id  = l.location_id
JOIN hr.countries c    ON l.country_id   = c.country_id
JOIN hr.regions r      ON c.region_id    = r.region_id
WHERE r.region_name = 'Europe';

-- Pracownicy z imieniem + nazwą działu + miastem:
SELECT e.first_name, e.last_name, d.department_name, l.city
FROM hr.employees e
JOIN hr.departments d ON e.department_id = d.department_id
JOIN hr.locations l ON d.location_id = l.location_id;

-- Działy bez pracowników
SELECT d.department_name, l.city
FROM hr.departments d
JOIN hr.locations l ON d.location_id = l.location_id
WHERE d.department_id NOT IN (
    SELECT department_id FROM hr.employees
    WHERE department_id IS NOT NULL  -- ważne! patrz pułapka poniżej
);
🚨
NOT IN + NULL = puste wyniki!
Jeśli podzapytanie w NOT IN zwróci chociaż jeden NULL, całe wyrażenie zwróci FALSE dla każdego wiersza. Zawsze filtruj: WHERE department_id IS NOT NULL w podzapytaniu.

Pensja poza widełkami stanowiska

sql
SELECT e.first_name, e.last_name, e.salary, j.min_salary, j.max_salary
FROM hr.employees e
JOIN hr.jobs j ON e.job_id = j.job_id
WHERE e.salary < j.min_salary OR e.salary > j.max_salary;

Pytania egzaminacyjne

🔤
Pytanie ABCD

Dlaczego to zapytanie NIE zwróci 5 najwyżej zarabiających pracowników? WHERE ROWNUM <= 5 ORDER BY salary DESC

🔤
Pytanie ABCD

Co się stanie przy: WHERE department_id NOT IN (SELECT department_id FROM employees) jeśli jakiś pracownik ma department_id = NULL?

🔤
Pytanie ABCD

Różnica między WHERE a HAVING?

🔍 Czy to się wykona?
SELECT department_id, AVG(salary)
FROM hr.employees
WHERE AVG(salary) > 8000
GROUP BY department_id;
🔍 Czy to się wykona?
SELECT first_name, last_name, salary
FROM hr.employees
WHERE salary > (SELECT AVG(salary) FROM hr.employees);
🔎 Znajdź błąd — wskaż niepoprawną linię
1SELECT department_id, SUM(salary)
2FROM hr.employees
3HAVING SUM(salary) > 50000
4WHERE department_id IS NOT NULL
5GROUP BY department_id;
🔎 Znajdź błąd — wskaż niepoprawną linię
1SELECT department_id, AVG(salary) AS avg_sal
2FROM hr.employees
3WHERE department_id NOT IN (
4 SELECT department_id FROM hr.departments
5)
6GROUP BY department_id;

Ćwiczenia — lab01

💪

Ćwiczenie 1 — Inicjały i indeks litery 'a'

Wyświetl nazwiska pracowników wielkimi literami, imiona z pierwszą dużą literą, inicjały w formacie 'J.K.' oraz indeks pierwszej litery 'a' (lub 'A') w nazwisku.

💪

Ćwiczenie 2 — Pensja z premią i staż

Wyświetl pracowników z pensją uwzględniającą premię (salary + commission_pct*salary) oraz stażem pracy w pełnych miesiącach.

💪

Ćwiczenie 3 — Filtrowanie: LIKE + BETWEEN + IN

Wyświetl pracowników, których nazwisko zaczyna się na 'S', pensja wynosi 5000–9000 i pracują na jednym ze stanowisk: IT_PROG, SA_REP, FI_ACCOUNT.

💪

Ćwiczenie 4 — Łączna pensja i liczba pracowników wg działu

Wyświetl każdy dział (nawet bez pracowników) z łącznym wynagrodzeniem i liczbą pracowników. Użyj LEFT JOIN żeby działy bez pracowników też się pokazały.

💪

Ćwiczenie 5 — Pracownicy z premią wg stanowiska

Wyświetl liczbę pracowników posiadających premię (commission_pct IS NOT NULL) w podziale na stanowiska (job_title). Użyj JOIN z hr.jobs.

💪

Ćwiczenie 6 — Średnia długość nazwiska wg działu

Oblicz średnią długość nazwisk pracowników w każdym dziale (zaokrągloną w dół do całości). Pokaż nazwę działu i wynik.

💪

Ćwiczenie 7 — Liczba podwładnych każdego managera

Wyświetl każdego managera (id, imię, nazwisko) i liczbę jego bezpośrednich podwładnych. Użyj self JOIN (employees e JOIN employees m).

💪

Ćwiczenie 8 — Pracownicy powyżej średniej firmy

Wyświetl pracowników (imię, nazwisko, pensja), którzy zarabiają więcej niż średnia pensja w całej firmie.

💪

Ćwiczenie 9 — Działy ze średnią > 8000

Wyświetl nazwy działów, w których średnia pensja jest wyższa niż 8000. Dołącz wartość średniej zaokrągloną do 2 miejsc.

💪

Ćwiczenie 10 — Działy w tym samym mieście co pracownik 100

Znajdź wszystkie działy zlokalizowane w tym samym mieście, w którym pracuje pracownik o identyfikatorze 100.