📋 Podstawy SQL
Schemat HR, kluczowe funkcje, podzapytania, JOIN, GROUP BY
- →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
| Tabela | Kluczowe kolumny |
|---|---|
employees | employee_id, first_name, last_name, salary, commission_pct, department_id, job_id, manager_id, hire_date |
departments | department_id, department_name, location_id |
locations | location_id, city, country_id |
countries | country_id, country_name, region_id |
regions | region_id, region_name |
jobs | job_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+.
-- 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;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;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.| ROWNUM | NAME | SALARY |
|---|---|---|
| 1 | Fay | 6000 |
| 2 | Hartstein | 13000 |
| 3 | Whalen | 4400 |
| ROWNUM | NAME | SALARY |
|---|---|---|
| 1 | King | 24000 |
| 2 | Kochhar | 17000 |
| 3 | De Haan | 17000 |
Filtrowanie zaawansowane — LIKE, BETWEEN, IN
-- 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
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
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ą).
-- Łą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;HAVING — filtruje grupy PO grupowaniu (może używać agregatów)
Kolejność: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
| JOB_ID | DEPT | COUNT(*) |
|---|---|---|
| SA_REP | 80 | 30 |
| SA_MAN | 80 | 5 |
| JOB_ID | DEPT | WSZYSCY | Z_PROWIZJĄ |
|---|---|---|---|
| SA_REP | 80 | 30 | 30 |
| SA_MAN | 80 | 5 | 5 |
| IT_PROG | 60 | 5 | 0 |
Podzapytania — skalarne i IN/NOT IN
-- 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.
-- 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
-- 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
-- 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 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
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
Dlaczego to zapytanie NIE zwróci 5 najwyżej zarabiających pracowników? WHERE ROWNUM <= 5 ORDER BY salary DESC
Co się stanie przy: WHERE department_id NOT IN (SELECT department_id FROM employees) jeśli jakiś pracownik ma department_id = NULL?
Różnica między WHERE a HAVING?
SELECT department_id, AVG(salary)
FROM hr.employees
WHERE AVG(salary) > 8000
GROUP BY department_id;SELECT first_name, last_name, salary
FROM hr.employees
WHERE salary > (SELECT AVG(salary) FROM hr.employees);Ć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.