Język zapytań SQL
Język DDL (tworzenie, modyfikacja, usuwanie tabel i baz), język DML (INSERT, UPDATE, DELETE), język DQL (złożone zapytania SELECT, JOIN, funkcje agregujące, grupowanie), język DCL (uprawnienia, GRANT, REVOKE).
Wstęp: dział, który decyduje o wyniku z części praktycznej
W zadaniu praktycznym INF.03 prawie zawsze jest arkusz z bazą danych i lista poleceń typu „wykonaj zapytanie wypisujące nazwiska uczniów, którzy nie mają żadnej oceny" albo „policz średnią ocen w każdej klasie i pokaż tylko klasy ze średnią powyżej 4". Każde takie zapytanie to osobno punktowana czynność. Nie ma tu miejsca na opisywanie własnymi słowami — albo napiszesz działającą składnię, albo tracisz punkt. Ten artykuł przechodzi przez cztery podjęzyki SQL (DDL, DML, DQL, DCL) na jednym schemacie bazy, żeby każdy przykład dało się od razu porównać z poprzednim.
📋 Spis treści
Struktura i dane
1
Schemat bazy używany w całym artykule
Wszystkie zapytania w tym artykule działają na jednej bazie danych szkoły.
Cztery tabele, dwie relacje jeden-do-wielu wychodzące z tabeli
oceny
i jedna z tabeli
uczniowie.
Wróć do tego schematu za każdym razem, gdy zgubisz się w przykładzie — nazwy
kolumn nigdzie się nie zmieniają.
CREATE DATABASE szkola
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_polish_ci;
USE szkola;
CREATE TABLE klasy (
id_klasy INT AUTO_INCREMENT PRIMARY KEY,
nazwa VARCHAR(5) NOT NULL UNIQUE,
profil VARCHAR(40),
wychowawca VARCHAR(60)
);
CREATE TABLE uczniowie (
id_ucznia INT AUTO_INCREMENT PRIMARY KEY,
imie VARCHAR(30) NOT NULL,
nazwisko VARCHAR(50) NOT NULL,
data_urodzenia DATE,
email VARCHAR(80) UNIQUE,
id_klasy INT,
FOREIGN KEY (id_klasy) REFERENCES klasy(id_klasy)
);
CREATE TABLE przedmioty (
id_przedmiotu INT AUTO_INCREMENT PRIMARY KEY,
nazwa VARCHAR(50) NOT NULL,
liczba_godzin INT DEFAULT 0
);
CREATE TABLE oceny (
id_oceny INT AUTO_INCREMENT PRIMARY KEY,
id_ucznia INT NOT NULL,
id_przedmiotu INT NOT NULL,
ocena DECIMAL(2,1) NOT NULL,
waga INT DEFAULT 1,
data_wpisu DATE,
FOREIGN KEY (id_ucznia) REFERENCES uczniowie(id_ucznia),
FOREIGN KEY (id_przedmiotu) REFERENCES przedmioty(id_przedmiotu)
);
- Jedna klasa ma wielu uczniów — klucz obcy siedzi po stronie „wielu", czyli w tabeli uczniowie.
- Jeden uczeń ma wiele ocen — klucz obcy w tabeli oceny.
- Jeden przedmiot ma wiele ocen — drugi klucz obcy w tabeli oceny.
- Tabela oceny łączy uczniów z przedmiotami, więc każde zapytanie „kto z czego" przechodzi przez nią.
- DDL — Data Definition Language: CREATE, ALTER, DROP, TRUNCATE. Operuje na strukturze.
- DML — Data Manipulation Language: INSERT, UPDATE, DELETE. Operuje na danych.
- DQL — Data Query Language: SELECT. Odczyt danych. Bywa zaliczany do DML.
- DCL — Data Control Language: GRANT, REVOKE. Uprawnienia użytkowników.
Kolejność tworzenia tabel ma znaczenie.
Tabelę uczniowie
z kluczem obcym do klasy
możesz utworzyć dopiero wtedy, gdy tabela klasy już istnieje — inaczej serwer
zgłosi błąd „cannot add foreign key constraint". Przy usuwaniu jest odwrotnie:
najpierw kasujesz tabelę potomną (oceny), potem nadrzędną (uczniowie).
2
DDL — tworzenie i zmiana struktury
DDL to polecenia, po których zmienia się wygląd tabeli, a nie jej zawartość. Na egzaminie pojawiają się w dwóch postaciach: „utwórz tabelę o podanej strukturze" oraz „dodaj do istniejącej tabeli kolumnę X".
| Typ | Co przechowuje | Typowe użycie u nas |
|---|---|---|
| INT | liczba całkowita | identyfikatory, liczba godzin |
| VARCHAR(n) | tekst zmiennej długości, maks. n znaków | imię, nazwisko, e-mail |
| CHAR(n) | tekst stałej długości, dopełniany spacjami | kody o stałej długości, np. PESEL |
| DECIMAL(p,s) | liczba dziesiętna dokładna, p cyfr, s po przecinku | ocena — DECIMAL(2,1) mieści 1.0 do 6.0 |
| FLOAT / DOUBLE | liczba zmiennoprzecinkowa (przybliżona) | pomiary; nigdy do pieniędzy |
| DATE | data w formacie RRRR-MM-DD | data urodzenia, data wpisu |
| DATETIME | data i godzina | moment logowania, znacznik zdarzenia |
| TEXT | długi tekst bez podawania długości | uwagi, opisy |
- PRIMARY KEY — klucz główny. Wartości unikatowe i nigdy NULL. Tabela ma dokładnie jeden.
- FOREIGN KEY — klucz obcy. Wskazuje na klucz główny innej tabeli i pilnuje, żeby nie dało się wstawić oceny dla nieistniejącego ucznia.
- AUTO_INCREMENT — serwer sam nadaje kolejny numer. Działa tylko na kolumnie całkowitej będącej kluczem.
- NOT NULL — kolumna nie może zostać pusta.
- UNIQUE — wartości nie mogą się powtarzać, ale NULL jest dozwolony i może wystąpić wielokrotnie.
- DEFAULT — wartość wstawiana, gdy w INSERT nie podasz tej kolumny.
To ulubione pytanie testowe. Oba wymuszają niepowtarzalność, ale: klucz główny nie dopuszcza NULL i występuje w tabeli tylko raz, natomiast UNIQUE dopuszcza NULL i może być nałożony na wiele kolumn niezależnie. W naszej tabeli uczniowie kluczem głównym jest id_ucznia, a UNIQUE ma email — bo uczeń może jeszcze nie mieć adresu, ale dwóch uczniów nie może mieć tego samego.
Modyfikacja istniejącej tabeli to polecenie ALTER TABLE. Trzy warianty, które wystarczą na egzaminie: dodanie kolumny, zmiana typu kolumny, usunięcie kolumny.
-- 1. dodanie kolumny na numer telefonu
ALTER TABLE uczniowie ADD COLUMN telefon VARCHAR(15);
-- 2. zmiana typu istniejącej kolumny (nazwisko na dłuższe)
ALTER TABLE uczniowie MODIFY COLUMN nazwisko VARCHAR(80) NOT NULL;
-- 3. usunięcie kolumny wraz z zawartymi w niej danymi
ALTER TABLE uczniowie DROP COLUMN telefon;
-- 4. dodanie ograniczenia do istniejącej tabeli
ALTER TABLE przedmioty ADD CONSTRAINT uq_nazwa UNIQUE (nazwa);
-- 5. zmiana nazwy tabeli
ALTER TABLE przedmioty RENAME TO przedmioty_szkolne;
| Polecenie | Podjęzyk | Co znika | Można z WHERE? | AUTO_INCREMENT |
|---|---|---|---|---|
| DROP TABLE | DDL | dane i cała struktura tabeli | nie | nie dotyczy — tabeli nie ma |
| TRUNCATE TABLE | DDL | wszystkie wiersze, struktura zostaje | nie | licznik wraca do 1 |
| DELETE FROM | DML | wskazane wiersze (bez WHERE — wszystkie) | tak | licznik rośnie dalej |
DROP TABLE oceny; -- tabela oceny przestaje istnieć
DROP DATABASE szkola; -- znika cała baza wraz z tabelami
TRUNCATE TABLE oceny; -- tabela zostaje, ale jest pusta, licznik id od 1
DELETE FROM oceny WHERE ocena = 1; -- usuwa tylko jedynki
-- bezpieczne warianty, gdy nie wiadomo, czy obiekt istnieje
DROP TABLE IF EXISTS oceny;
CREATE TABLE IF NOT EXISTS przedmioty (
id_przedmiotu INT AUTO_INCREMENT PRIMARY KEY,
nazwa VARCHAR(50) NOT NULL
);
3
DML — INSERT, UPDATE, DELETE
Trzy polecenia zmieniające zawartość tabel. Zapamiętaj składnię co do przecinka: w INSERT wartości tekstowe i daty idą w apostrofach, liczby bez. Kolumnę z AUTO_INCREMENT po prostu pomijasz.
-- jeden rekord, wskazane kolumny (zalecany zapis)
INSERT INTO klasy (nazwa, profil, wychowawca)
VALUES ('3A', 'programista', 'Anna Kowalska');
-- wiele rekordów jednym poleceniem: kolejne nawiasy po przecinku
INSERT INTO klasy (nazwa, profil, wychowawca) VALUES
('3B', 'grafik', 'Piotr Nowak'),
('4A', 'programista', 'Ewa Zielinska'),
('4B', 'sieciowiec', 'Marek Lis');
-- wstawienie ucznia; id_ucznia nadaje AUTO_INCREMENT, więc go nie podajemy
INSERT INTO uczniowie (imie, nazwisko, data_urodzenia, email, id_klasy)
VALUES ('Jan', 'Kowalczyk', '2008-04-12', 'jan.k@zsp.edu.pl', 1);
-- wersja bez listy kolumn: MUSISZ podać wartości dla wszystkich kolumn
-- w kolejności z definicji tabeli, łącznie z NULL dla id
INSERT INTO przedmioty VALUES (NULL, 'Bazy danych', 4);
UPDATE zmienia istniejące wiersze. Klauzula SET wylicza kolumny do zmiany po przecinku, a WHERE decyduje, których wierszy to dotyczy.
-- przeniesienie jednego ucznia do klasy o id 3
UPDATE uczniowie
SET id_klasy = 3
WHERE id_ucznia = 7;
-- zmiana dwóch kolumn naraz
UPDATE przedmioty
SET nazwa = 'Systemy baz danych', liczba_godzin = 5
WHERE id_przedmiotu = 2;
-- podniesienie wagi wszystkim ocenom z przedmiotu 4 o 1
UPDATE oceny
SET waga = waga + 1
WHERE id_przedmiotu = 4;
-- KATASTROFA: brak WHERE ustawia ocenę 5 KAŻDEMU uczniowi z KAŻDEGO przedmiotu
UPDATE oceny SET ocena = 5;
UPDATE i DELETE bez klauzuli WHERE nie zgłaszają żadnego ostrzeżenia —
po prostu wykonują się na wszystkich wierszach tabeli.
DELETE FROM uczniowie;
czyści całą tabelę i jeśli nie masz kopii ani otwartej transakcji, danych
nie odzyskasz. Nawyk, który warto wyrobić przed egzaminem: napisz najpierw
SELECT * FROM ... WHERE ...
, zobacz, ile wierszy wraca, i dopiero zamień SELECT na UPDATE albo DELETE
z tym samym warunkiem.
-- usunięcie jednego ucznia
DELETE FROM uczniowie WHERE id_ucznia = 12;
-- usunięcie wszystkich ocen niedostatecznych wpisanych przed 1 września
DELETE FROM oceny
WHERE ocena = 1 AND data_wpisu < '2026-09-01';
-- usunięcie ocen ucznia, którego zaraz skreślamy z listy
-- (najpierw potomna tabela oceny, potem uczniowie - inaczej klucz obcy zablokuje)
DELETE FROM oceny WHERE id_ucznia = 12;
DELETE FROM uczniowie WHERE id_ucznia = 12;
4
DQL — SELECT, WHERE, ORDER BY, aliasy
SELECT to jedyne polecenie, które zwraca dane. Cała reszta tego działu to klauzule doczepiane do niego w ustalonej kolejności: SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT. Kolejność zapisu jest sztywna — zamiana ORDER BY z WHERE to błąd składni.
-- wszystkie kolumny wszystkich uczniów
SELECT * FROM uczniowie;
-- tylko wybrane kolumny
SELECT imie, nazwisko, email FROM uczniowie;
-- DISTINCT usuwa duplikaty z wyniku:
-- zamiast czterech wierszy 'programista' dostaniesz jeden
SELECT DISTINCT profil FROM klasy;
-- DISTINCT działa na całym zestawie kolumn, nie na pierwszej z nich
SELECT DISTINCT profil, wychowawca FROM klasy;
| Operator | Znaczenie | Przykład na naszej bazie |
|---|---|---|
| = | równe | id_klasy = 2 |
| <> | różne od (zamiennie: !=) | ocena <> 1 |
| > < >= <= | porównania | liczba_godzin >= 4 |
| BETWEEN a AND b | z przedziału, obustronnie domkniętego | ocena BETWEEN 4 AND 5 |
| IN (…) | należy do listy wartości | id_klasy IN (1, 3, 5) |
| LIKE | dopasowanie wzorca tekstowego | nazwisko LIKE 'Kow%' |
| IS NULL | brak wartości | email IS NULL |
| NOT | zaprzeczenie warunku | NOT (profil = 'grafik') |
Znaki wieloznaczne w LIKE: % zastępuje dowolny ciąg znaków (także pusty), _ (podkreślnik) zastępuje dokładnie jeden znak. Wzorzec 'Kow%' to nazwiska zaczynające się od Kow, '%ski' to kończące się na ski, '%an%' to zawierające an w środku, a 'A_a' pasuje do trzyliterowych imion typu Ala i Ada.
-- uczniowie klasy o id 2
SELECT imie, nazwisko FROM uczniowie WHERE id_klasy = 2;
-- wszystkie oceny inne niż dopuszczająca
SELECT * FROM oceny WHERE ocena <> 2;
-- oceny z przedziału 4-5 WŁĄCZNIE z krańcami
SELECT id_ucznia, ocena FROM oceny WHERE ocena BETWEEN 4 AND 5;
-- to samo zapisane porównaniami - dokładnie ten sam wynik
SELECT id_ucznia, ocena FROM oceny WHERE ocena >= 4 AND ocena <= 5;
-- uczniowie z trzech wskazanych klas
SELECT nazwisko FROM uczniowie WHERE id_klasy IN (1, 3, 5);
-- nazwiska na -ski oraz imiona trzyliterowe zaczynające się na A
SELECT nazwisko FROM uczniowie WHERE nazwisko LIKE '%ski';
SELECT imie FROM uczniowie WHERE imie LIKE 'A_a';
-- uczniowie bez przypisanej klasy - NULL sprawdzamy TYLKO przez IS NULL
SELECT imie, nazwisko FROM uczniowie WHERE id_klasy IS NULL;
SELECT imie, nazwisko FROM uczniowie WHERE email IS NOT NULL;
NULL nie równa się niczemu, nawet sobie.
Warunek WHERE id_klasy = NULL
nie zwróci nigdy żadnego wiersza — porównanie z NULL daje wynik „nieznany",
a nie prawdę. Jedyny poprawny zapis to
IS NULL
i IS NOT NULL.
To jeden z najczęściej odbieranych punktów w zadaniach typu „wypisz uczniów
bez przypisanej klasy".
Warunki łączy się operatorami AND, OR i NOT. Kolejność ich wykonania jest taka jak w logice: najpierw NOT, potem AND, na końcu OR. To znaczy, że AND „wiąże mocniej" niż OR i przy mieszaniu obu trzeba stawiać nawiasy.
-- ZŁE ROZUMIENIE: to zapytanie zwróci uczniów z klasy 1
-- ORAZ wszystkich uczniów z klasy 2 mających e-mail
SELECT * FROM uczniowie
WHERE id_klasy = 1 OR id_klasy = 2 AND email IS NOT NULL;
-- czyli serwer czyta to tak:
SELECT * FROM uczniowie
WHERE id_klasy = 1 OR (id_klasy = 2 AND email IS NOT NULL);
-- ZAMIERZONE: uczniowie z klasy 1 lub 2, którzy mają e-mail
SELECT * FROM uczniowie
WHERE (id_klasy = 1 OR id_klasy = 2) AND email IS NOT NULL;
-- NOT zaprzecza warunkowi bezpośrednio po nim
SELECT * FROM uczniowie WHERE NOT id_klasy = 3;
SELECT * FROM oceny WHERE ocena NOT BETWEEN 2 AND 3;
SELECT * FROM klasy WHERE profil NOT IN ('grafik', 'sieciowiec');
Sortowanie, ograniczanie liczby wierszy, aliasy i kolumny obliczeniowe — cztery rzeczy, które w poleceniu egzaminacyjnym brzmią jak „posortuj malejąco", „pokaż pięciu pierwszych", „nagłówek kolumny ma brzmieć" i „wylicz".
-- sortowanie rosnąco; ASC jest domyślne, więc można je pominąć
SELECT nazwisko, imie FROM uczniowie ORDER BY nazwisko ASC;
-- malejąco po ocenie
SELECT id_ucznia, ocena FROM oceny ORDER BY ocena DESC;
-- po wielu kolumnach: najpierw klasa rosnąco, w ramach klasy nazwisko rosnąco
SELECT id_klasy, nazwisko, imie
FROM uczniowie
ORDER BY id_klasy ASC, nazwisko ASC;
-- każda kolumna ma własny kierunek
SELECT id_klasy, nazwisko FROM uczniowie ORDER BY id_klasy DESC, nazwisko ASC;
-- LIMIT: pięć najwyższych ocen
SELECT id_ucznia, ocena FROM oceny ORDER BY ocena DESC LIMIT 5;
-- LIMIT z przesunięciem: pomiń 10 wierszy, weź kolejne 5 (strona 3 listy)
SELECT nazwisko FROM uczniowie ORDER BY nazwisko LIMIT 10, 5;
SELECT nazwisko FROM uczniowie ORDER BY nazwisko LIMIT 5 OFFSET 10;
-- alias kolumny: nagłówek w wyniku brzmi 'Nazwisko ucznia'
SELECT nazwisko AS 'Nazwisko ucznia', imie AS Imie FROM uczniowie;
-- alias tabeli skraca zapis przy złączeniach
SELECT u.nazwisko, k.nazwa
FROM uczniowie AS u, klasy AS k
WHERE u.id_klasy = k.id_klasy;
-- kolumna obliczeniowa: łączenie tekstu i wyliczenie liczbowe
SELECT CONCAT(imie, ' ', nazwisko) AS pelne_imie FROM uczniowie;
SELECT nazwa, liczba_godzin, liczba_godzin * 30 AS godzin_w_roku
FROM przedmioty;
-- ocena ważona jako kolumna wyliczana
SELECT id_ucznia, ocena, waga, ocena * waga AS punkty FROM oceny;
-- funkcje na tekście i dacie, które warto znać
SELECT UPPER(nazwisko) AS wielkimi,
LOWER(imie) AS malymi,
LENGTH(nazwisko) AS dlugosc,
YEAR(data_urodzenia) AS rocznik
FROM uczniowie;
5
Złączenia: INNER, LEFT, RIGHT JOIN
Dane w bazie relacyjnej są porozrzucane po tabelach — nazwa klasy leży w tabeli klasy, a nazwisko ucznia w tabeli uczniowie. Złączenie zszywa je z powrotem po kluczu obcym. Warunek złączenia zawsze wygląda tak samo: klucz obcy tabeli potomnej równa się kluczowi głównemu tabeli nadrzędnej.
| Złączenie | Które wiersze trafiają do wyniku | Kiedy pojawia się NULL |
|---|---|---|
| INNER JOIN | tylko te pary wierszy, dla których warunek złączenia jest spełniony po obu stronach | nigdy w kolumnach złączanych — wiersz bez pary po prostu wypada |
| LEFT JOIN | wszystkie wiersze tabeli lewej (tej z FROM) plus pasujące z prawej | w kolumnach z prawej tabeli, gdy nie znaleziono pary |
| RIGHT JOIN | wszystkie wiersze tabeli prawej (tej po JOIN) plus pasujące z lewej | w kolumnach z lewej tabeli, gdy nie znaleziono pary |
| CROSS JOIN | iloczyn kartezjański — każdy wiersz z każdym, bez warunku | nie występuje; za to wynik ma m × n wierszy |
Sam zapis JOIN bez przedrostka oznacza INNER JOIN. LEFT JOIN i LEFT OUTER JOIN to dokładnie to samo — słowo OUTER jest opcjonalne. MySQL nie obsługuje FULL OUTER JOIN.
-- INNER JOIN: uczeń wraz z nazwą swojej klasy.
-- Uczeń z id_klasy = NULL NIE pojawi się w wyniku.
SELECT u.imie, u.nazwisko, k.nazwa AS klasa, k.profil
FROM uczniowie AS u
INNER JOIN klasy AS k ON u.id_klasy = k.id_klasy
ORDER BY k.nazwa, u.nazwisko;
-- ten sam efekt zapisany warunkiem w WHERE (starsza składnia)
SELECT u.imie, u.nazwisko, k.nazwa
FROM uczniowie u, klasy k
WHERE u.id_klasy = k.id_klasy;
Teraz najważniejszy przykład całej sekcji. LEFT JOIN pokazuje również te wiersze lewej tabeli, dla których nie ma nic po prawej — i właśnie tam widać wartości NULL. Załóżmy, że uczennica Zofia Wilk nie dostała jeszcze żadnej oceny, a uczeń Adam Baran nie został przypisany do klasy.
SELECT u.nazwisko, p.nazwa AS przedmiot, o.ocena
FROM uczniowie AS u
LEFT JOIN oceny AS o ON u.id_ucznia = o.id_ucznia
LEFT JOIN przedmioty AS p ON o.id_przedmiotu = p.id_przedmiotu
ORDER BY u.nazwisko;
-- Możliwy wynik:
-- nazwisko | przedmiot | ocena
-- ------------+---------------+-------
-- Baran | Bazy danych | 4.0
-- Baran | Sieci | 3.0
-- Kowalczyk | Bazy danych | 5.0
-- Wilk | NULL | NULL <-- uczennica bez ani jednej oceny
--
-- Przy INNER JOIN wiersz z Wilk w ogóle by nie powstał.
-- klasyczne zadanie: wypisz uczniów, którzy NIE MAJĄ żadnej oceny.
-- LEFT JOIN + warunek na NULL po stronie prawej tabeli
SELECT u.imie, u.nazwisko
FROM uczniowie AS u
LEFT JOIN oceny AS o ON u.id_ucznia = o.id_ucznia
WHERE o.id_oceny IS NULL;
-- RIGHT JOIN: wszystkie klasy, także te, do których nikt nie chodzi.
-- Tabela klasy stoi po prawej stronie słowa JOIN, więc to ona jest zachowana.
SELECT u.nazwisko, k.nazwa AS klasa
FROM uczniowie AS u
RIGHT JOIN klasy AS k ON u.id_klasy = k.id_klasy;
-- to samo, ale zapisane jako LEFT JOIN po zamianie tabel miejscami
SELECT u.nazwisko, k.nazwa AS klasa
FROM klasy AS k
LEFT JOIN uczniowie AS u ON u.id_klasy = k.id_klasy;
Złączenie trzech i więcej tabel budujesz przez dopisywanie kolejnych JOIN-ów. Każdy nowy JOIN ma własne ON i podpina się do tego, co zostało złączone wcześniej. Ścieżka jest wymuszona przez klucze obce: od uczniów przez oceny do przedmiotów, a osobno od uczniów do klas.
-- pełna informacja: kto, z jakiej klasy, z jakiego przedmiotu, jaka ocena
SELECT k.nazwa AS klasa,
u.nazwisko AS uczen,
p.nazwa AS przedmiot,
o.ocena,
o.data_wpisu
FROM oceny AS o
INNER JOIN uczniowie AS u ON o.id_ucznia = u.id_ucznia
INNER JOIN przedmioty AS p ON o.id_przedmiotu = p.id_przedmiotu
INNER JOIN klasy AS k ON u.id_klasy = k.id_klasy
WHERE o.ocena >= 5
ORDER BY k.nazwa, u.nazwisko;
-- Zwraca wszystkie oceny bardzo dobre i celujące wraz z nazwiskiem ucznia,
-- nazwą jego klasy i nazwą przedmiotu, uporządkowane klasami.
Kolumna o tej samej nazwie w dwóch tabelach.
Jeśli w obu złączanych tabelach istnieje kolumna
nazwa
, to napisanie samego nazwa w SELECT kończy się błędem
„column is ambiguous". Zawsze poprzedzaj kolumnę nazwą tabeli albo jej
aliasem: k.nazwa, p.nazwa. Przy trzech i więcej tabelach
aliasy to nie ozdobnik, tylko konieczność.
6
Agregacja, GROUP BY, HAVING, podzapytania
Funkcja agregująca zamienia wiele wierszy w jedną wartość. Bez GROUP BY liczy dla całej tabeli, z GROUP BY — osobno dla każdej grupy. Polecenia typu „policz", „podaj średnią", „ile jest" prawie zawsze oznaczają agregację.
- COUNT(*) — liczba wierszy w grupie, licząc te z NULL-ami.
- COUNT(kolumna) — liczba wierszy, w których ta kolumna nie jest NULL. To nie to samo co COUNT(*).
- SUM(kolumna) — suma wartości liczbowych.
- AVG(kolumna) — średnia arytmetyczna; wiersze z NULL są pomijane w liczniku i mianowniku.
- MIN / MAX — wartość najmniejsza i największa; działają też na tekstach i datach.
Każda kolumna wypisana w SELECT musi albo znaleźć się w GROUP BY, albo być opakowana w funkcję agregującą. Zapytanie SELECT nazwisko, COUNT(*) FROM oceny GROUP BY id_ucznia jest logicznie wadliwe — w jednej grupie może być wiele różnych nazwisk i serwer nie wie, które pokazać. Poprawnie grupuje się po tej samej kolumnie, którą się wyświetla.
-- ilu jest uczniów w całej szkole
SELECT COUNT(*) AS liczba_uczniow FROM uczniowie;
-- ilu uczniów ma podany adres e-mail (NULL nie jest liczony)
SELECT COUNT(email) AS z_mailem FROM uczniowie;
-- średnia, najniższa i najwyższa ocena w całej bazie
SELECT AVG(ocena) AS srednia,
MIN(ocena) AS najnizsza,
MAX(ocena) AS najwyzsza,
COUNT(*) AS ile_ocen
FROM oceny;
-- średnia zaokrąglona do dwóch miejsc po przecinku
SELECT ROUND(AVG(ocena), 2) AS srednia FROM oceny;
-- suma godzin wszystkich przedmiotów
SELECT SUM(liczba_godzin) AS godzin_lacznie FROM przedmioty;
-- liczba uczniów w każdej klasie
SELECT k.nazwa AS klasa, COUNT(u.id_ucznia) AS liczba_uczniow
FROM klasy AS k
LEFT JOIN uczniowie AS u ON k.id_klasy = u.id_klasy
GROUP BY k.nazwa
ORDER BY liczba_uczniow DESC;
-- Dzięki LEFT JOIN klasa bez uczniów pojawi się z wynikiem 0.
-- Gdyby użyć COUNT(*), taka klasa pokazałaby błędnie 1.
-- średnia ocena każdego ucznia
SELECT u.nazwisko, u.imie, ROUND(AVG(o.ocena), 2) AS srednia
FROM uczniowie AS u
INNER JOIN oceny AS o ON u.id_ucznia = o.id_ucznia
GROUP BY u.id_ucznia, u.nazwisko, u.imie
ORDER BY srednia DESC;
-- średnia z każdego przedmiotu w każdej klasie (grupowanie po dwóch kolumnach)
SELECT k.nazwa AS klasa, p.nazwa AS przedmiot, ROUND(AVG(o.ocena), 2) AS srednia
FROM oceny AS o
INNER JOIN uczniowie AS u ON o.id_ucznia = u.id_ucznia
INNER JOIN klasy AS k ON u.id_klasy = k.id_klasy
INNER JOIN przedmioty AS p ON o.id_przedmiotu = p.id_przedmiotu
GROUP BY k.nazwa, p.nazwa
ORDER BY k.nazwa, p.nazwa;
| Cecha | WHERE | HAVING |
|---|---|---|
| Etap wykonania | przed grupowaniem | po grupowaniu |
| Co filtruje | pojedyncze wiersze | całe grupy |
| Funkcje agregujące w warunku | niedozwolone (błąd składni) | to jest jego jedyny sens |
| Wymaga GROUP BY | nie | praktycznie zawsze występuje razem z nim |
| Przykład warunku | o.ocena >= 2 | AVG(o.ocena) > 4 |
Reguła w jednym zdaniu: jeśli warunek dotyczy wartości z pojedynczego wiersza — WHERE. Jeśli dotyczy wyniku COUNT, SUM, AVG, MIN lub MAX — HAVING. Obie klauzule mogą wystąpić w tym samym zapytaniu i wtedy WHERE idzie przed GROUP BY, a HAVING po nim.
-- klasy, w których uczy się więcej niż 25 uczniów
SELECT k.nazwa, COUNT(u.id_ucznia) AS ilu
FROM klasy AS k
INNER JOIN uczniowie AS u ON k.id_klasy = u.id_klasy
GROUP BY k.nazwa
HAVING COUNT(u.id_ucznia) > 25;
-- uczniowie ze średnią powyżej 4.5, liczoną TYLKO z ocen wpisanych w 2026 r.
SELECT u.nazwisko, ROUND(AVG(o.ocena), 2) AS srednia
FROM uczniowie AS u
INNER JOIN oceny AS o ON u.id_ucznia = o.id_ucznia
WHERE YEAR(o.data_wpisu) = 2026 -- filtr wierszy PRZED grupowaniem
GROUP BY u.id_ucznia, u.nazwisko
HAVING AVG(o.ocena) > 4.5 -- filtr grup PO grupowaniu
ORDER BY srednia DESC;
-- BŁĄD: agregat w WHERE. Serwer odrzuci to zapytanie.
SELECT u.nazwisko FROM uczniowie u
JOIN oceny o ON u.id_ucznia = o.id_ucznia
WHERE AVG(o.ocena) > 4.5
GROUP BY u.nazwisko;
-- przedmioty, z których wystawiono co najmniej 10 ocen
SELECT p.nazwa, COUNT(*) AS ile_ocen
FROM przedmioty AS p
INNER JOIN oceny AS o ON p.id_przedmiotu = o.id_przedmiotu
GROUP BY p.nazwa
HAVING COUNT(*) >= 10
ORDER BY ile_ocen DESC;
Podzapytanie to SELECT umieszczony wewnątrz innego zapytania, zwykle w nawiasie w klauzuli WHERE. Używa się go wtedy, gdy warunek zależy od wyniku, którego nie znasz z góry — na przykład od średniej całej szkoły.
-- podzapytanie zwracające JEDNĄ wartość - porównanie operatorem
SELECT id_ucznia, ocena
FROM oceny
WHERE ocena > (SELECT AVG(ocena) FROM oceny);
-- Zwraca oceny wyższe od średniej wszystkich ocen w bazie.
-- podzapytanie zwracające LISTĘ wartości - operator IN
SELECT imie, nazwisko
FROM uczniowie
WHERE id_klasy IN (SELECT id_klasy FROM klasy WHERE profil = 'programista');
-- Zwraca uczniów wszystkich klas o profilu programista.
-- podzapytanie skorelowane - odwołuje się do zapytania zewnętrznego
SELECT u.nazwisko
FROM uczniowie AS u
WHERE NOT EXISTS (
SELECT 1 FROM oceny AS o WHERE o.id_ucznia = u.id_ucznia
);
-- Uczniowie bez ani jednej oceny - alternatywa dla LEFT JOIN ... IS NULL.
-- podzapytanie w SELECT jako kolumna wyliczana
SELECT u.nazwisko,
(SELECT COUNT(*) FROM oceny AS o WHERE o.id_ucznia = u.id_ucznia) AS ile_ocen
FROM uczniowie AS u
ORDER BY ile_ocen DESC;
-- uczeń z najwyższą oceną
SELECT id_ucznia, ocena FROM oceny
WHERE ocena = (SELECT MAX(ocena) FROM oceny);
7
Kolejność wykonywania zapytania
Zapytanie zapisujesz w jednej kolejności, a serwer wykonuje je w zupełnie innej. Zrozumienie tej różnicy tłumaczy naraz kilka „dziwnych" błędów: dlaczego alias nie działa w WHERE, dlaczego agregat trafia do HAVING, a nie do WHERE, i dlaczego LIMIT bez ORDER BY daje przypadkowe wiersze.
- FROM i JOIN — serwer buduje jeden roboczy zestaw wierszy ze wszystkich złączonych tabel.
- WHERE — z tego zestawu wyrzuca wiersze niespełniające warunku.
- GROUP BY — pozostałe wiersze zbija w grupy i liczy dla nich funkcje agregujące.
- HAVING — odrzuca całe grupy niespełniające warunku.
- SELECT — dopiero teraz wybiera kolumny, oblicza wyrażenia i nadaje aliasy.
- DISTINCT — usuwa powtarzające się wiersze wyniku.
- ORDER BY — sortuje gotowy wynik.
- LIMIT — obcina go do zadanej liczby wierszy.
Alias powstaje w kroku 5, a WHERE działa w kroku 2. W momencie, w którym serwer sprawdza warunek WHERE, nazwa srednia jeszcze nie istnieje — stąd komunikat „unknown column". Z tego samego powodu aliasu można użyć w ORDER BY (krok 7) i w większości serwerów także w HAVING (krok 4 w MySQL działa na już policzonych agregatach). Obejście w WHERE jest jedno: powtórz całe wyrażenie zamiast aliasu.
-- BŁĄD: alias godzin_w_roku nie istnieje jeszcze na etapie WHERE
SELECT nazwa, liczba_godzin * 30 AS godzin_w_roku
FROM przedmioty
WHERE godzin_w_roku > 100;
-- POPRAWNIE: w WHERE powtarzamy wyrażenie
SELECT nazwa, liczba_godzin * 30 AS godzin_w_roku
FROM przedmioty
WHERE liczba_godzin * 30 > 100
ORDER BY godzin_w_roku DESC; -- tu alias działa, bo ORDER BY jest po SELECT
-- LIMIT bez ORDER BY: dostaniesz 5 wierszy, ale nikt nie gwarantuje KTÓRYCH
SELECT nazwisko FROM uczniowie LIMIT 5;
-- LIMIT z ORDER BY: pięciu uczniów alfabetycznie od końca - wynik powtarzalny
SELECT nazwisko FROM uczniowie ORDER BY nazwisko DESC LIMIT 5;
Weźmy zapytanie o klasy o średniej powyżej 4, liczonej bez ocen niedostatecznych, ograniczone do trzech najlepszych.
SELECT k.nazwa AS klasa, ROUND(AVG(o.ocena), 2) AS srednia
FROM oceny AS o
INNER JOIN uczniowie AS u ON o.id_ucznia = u.id_ucznia
INNER JOIN klasy AS k ON u.id_klasy = k.id_klasy
WHERE o.ocena > 1
GROUP BY k.nazwa
HAVING AVG(o.ocena) > 4
ORDER BY srednia DESC
LIMIT 3;- FROM + JOIN: powstaje tabela robocza, w której każdy wiersz to jedna ocena wraz z danymi ucznia i jego klasy.
- WHERE: znikają wiersze z oceną 1.
- GROUP BY: wiersze zbijają się w tyle grup, ile jest nazw klas; dla każdej grupy liczona jest średnia.
- HAVING: odpadają grupy ze średnią 4 lub niższą.
- SELECT: powstają kolumny klasa i srednia — dopiero tu nazwy aliasów zaczynają istnieć.
- ORDER BY: wynik sortowany malejąco po aliasie srednia.
- LIMIT: zostają trzy pierwsze wiersze.
8
DCL — użytkownicy i uprawnienia
DCL to dwa polecenia: GRANT nadaje uprawnienia, REVOKE je odbiera. Konto
użytkownika w MySQL jest zawsze parą nazwa i host — użytkownik
nauczyciel@localhost
to ktoś inny niż
nauczyciel@'%'
, gdzie znak procenta oznacza dowolny komputer w sieci.
-- utworzenie konta logującego się tylko z tego samego komputera
CREATE USER 'nauczyciel'@'localhost' IDENTIFIED BY 'Tajne_haslo1';
-- konto dostępne z dowolnego hosta w sieci
CREATE USER 'sekretariat'@'%' IDENTIFIED BY 'Inne_haslo2';
-- odczyt całej bazy szkola
GRANT SELECT ON szkola.* TO 'nauczyciel'@'localhost';
-- odczyt i modyfikacja danych, ale bez prawa zmiany struktury
GRANT SELECT, INSERT, UPDATE, DELETE ON szkola.* TO 'sekretariat'@'%';
-- uprawnienie zawężone do jednej tabeli
GRANT SELECT, INSERT ON szkola.oceny TO 'nauczyciel'@'localhost';
-- uprawnienie zawężone do wskazanych kolumn jednej tabeli
GRANT SELECT (imie, nazwisko) ON szkola.uczniowie TO 'nauczyciel'@'localhost';
-- pełnia praw do jednej bazy, z możliwością przekazywania ich dalej
GRANT ALL PRIVILEGES ON szkola.* TO 'admin_szkoly'@'localhost' WITH GRANT OPTION;
-- zastosowanie zmian i podgląd nadanych uprawnień
FLUSH PRIVILEGES;
SHOW GRANTS FOR 'nauczyciel'@'localhost';
-- odebranie pojedynczego uprawnienia
REVOKE DELETE ON szkola.* FROM 'sekretariat'@'%';
-- odebranie wszystkiego
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'sekretariat'@'%';
-- zmiana hasła i usunięcie konta
ALTER USER 'nauczyciel'@'localhost' IDENTIFIED BY 'Nowe_haslo3';
DROP USER 'nauczyciel'@'localhost';
| Poziom | Zapis po ON | Zasięg |
|---|---|---|
| globalny | *.* | wszystkie bazy na serwerze |
| bazy danych | szkola.* | wszystkie tabele jednej bazy |
| tabeli | szkola.oceny | jedna wskazana tabela |
| kolumny | SELECT (imie) ON szkola.uczniowie | wskazane kolumny jednej tabeli |
Obowiązuje zasada najmniejszych uprawnień: konto aplikacji webowej powinno mieć SELECT, INSERT, UPDATE i DELETE na jednej bazie, a nie ALL PRIVILEGES na *.*. Uprawnienia DDL (CREATE, ALTER, DROP) zostawia się administratorowi.
- Warunek na NULL zapisany przez znak równości. WHERE email = NULL nie zwróci nic. Poprawnie: IS NULL lub IS NOT NULL.
- Funkcja agregująca w klauzuli WHERE. WHERE AVG(ocena) > 4 to błąd — warunek na agregat zawsze idzie do HAVING, bo WHERE działa przed grupowaniem.
- Alias z SELECT użyty w WHERE. Alias powstaje dopiero na etapie SELECT. W WHERE trzeba powtórzyć całe wyrażenie; w ORDER BY alias jest już dostępny.
- INNER JOIN tam, gdzie zadanie prosi o „wszystkich, także tych bez…". Wiersze bez pary wypadają. Do wypisania uczniów bez ocen potrzebny jest LEFT JOIN z warunkiem IS NULL po prawej stronie.
- COUNT(*) przy LEFT JOIN. Dla grupy bez dopasowań COUNT(*) policzy sztuczny wiersz z NULL-ami i zwróci 1 zamiast 0. Licz konkretną kolumnę: COUNT(u.id_ucznia).
- UPDATE i DELETE bez WHERE. Zmieniają albo kasują całą tabelę bez ostrzeżenia. Najpierw sprawdź warunek SELECT-em.
- Mylenie TRUNCATE z DROP. TRUNCATE zostawia pustą tabelę i zeruje AUTO_INCREMENT, DROP kasuje także jej strukturę. Żadne z nich nie przyjmuje WHERE.
- Mieszanie AND z OR bez nawiasów. AND wykonuje się przed OR, więc a OR b AND c znaczy a OR (b AND c). Gdy chodzi o coś innego, nawiasy są obowiązkowe.
- BETWEEN traktowany jak przedział otwarty. BETWEEN 4 AND 5 obejmuje również 4 i 5.
SQL-a nie da się nauczyć przez czytanie. Sprawdź, czy potrafisz bez podpowiedzi napisać zapytanie z JOIN-em, GROUP BY i HAVING — test z tego działu pokaże, gdzie jeszcze się gubisz.