Przejdź do treści
INF.03 · Dział IV

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.

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)
);
Jak czytać relacje
  • 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ą.
Cztery podjęzyki SQL
  • DDLData Definition Language: CREATE, ALTER, DROP, TRUNCATE. Operuje na strukturze.
  • DMLData Manipulation Language: INSERT, UPDATE, DELETE. Operuje na danych.
  • DQLData Query Language: SELECT. Odczyt danych. Bywa zaliczany do DML.
  • DCLData 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".

Typy kolumn, które trzeba znać z pamięci
TypCo przechowujeTypowe użycie u nas
INTliczba całkowitaidentyfikatory, liczba godzin
VARCHAR(n)tekst zmiennej długości, maks. n znakówimię, nazwisko, e-mail
CHAR(n)tekst stałej długości, dopełniany spacjamikody o stałej długości, np. PESEL
DECIMAL(p,s)liczba dziesiętna dokładna, p cyfr, s po przecinkuocena — DECIMAL(2,1) mieści 1.0 do 6.0
FLOAT / DOUBLEliczba zmiennoprzecinkowa (przybliżona)pomiary; nigdy do pieniędzy
DATEdata w formacie RRRR-MM-DDdata urodzenia, data wpisu
DATETIMEdata i godzinamoment logowania, znacznik zdarzenia
TEXTdługi tekst bez podawania długościuwagi, opisy
Więzy (ograniczenia)
  • 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.
PRIMARY KEY kontra UNIQUE

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;
DROP kontra TRUNCATE kontra DELETE — klasyk egzaminacyjny
PoleceniePodjęzykCo znikaMożna z WHERE?AUTO_INCREMENT
DROP TABLEDDLdane i cała struktura tabelinienie dotyczy — tabeli nie ma
TRUNCATE TABLEDDLwszystkie wiersze, struktura zostajenielicznik wraca do 1
DELETE FROMDMLwskazane wiersze (bez WHERE — wszystkie)taklicznik 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;
Dlaczego brak WHERE to katastrofa

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;
Operatory klauzuli WHERE
OperatorZnaczeniePrzykład na naszej bazie
=równeid_klasy = 2
<>różne od (zamiennie: !=)ocena <> 1
> < >= <=porównanialiczba_godzin >= 4
BETWEEN a AND bz przedziału, obustronnie domkniętegoocena BETWEEN 4 AND 5
IN (…)należy do listy wartościid_klasy IN (1, 3, 5)
LIKEdopasowanie wzorca tekstowegonazwisko LIKE 'Kow%'
IS NULLbrak wartościemail IS NULL
NOTzaprzeczenie warunkuNOT (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.

Typy złączeń i co trafia do wyniku
ZłączenieKtóre wiersze trafiają do wynikuKiedy pojawia się NULL
INNER JOINtylko te pary wierszy, dla których warunek złączenia jest spełniony po obu stronachnigdy w kolumnach złączanych — wiersz bez pary po prostu wypada
LEFT JOINwszystkie wiersze tabeli lewej (tej z FROM) plus pasujące z prawejw kolumnach z prawej tabeli, gdy nie znaleziono pary
RIGHT JOINwszystkie wiersze tabeli prawej (tej po JOIN) plus pasujące z lewejw kolumnach z lewej tabeli, gdy nie znaleziono pary
CROSS JOINiloczyn kartezjański — każdy wiersz z każdym, bez warunkunie 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ę.

Pięć funkcji agregujących
  • 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.
Zasada GROUP BY

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;
WHERE kontra HAVING — najczęstsza pomyłka
CechaWHEREHAVING
Etap wykonaniaprzed grupowaniempo grupowaniu
Co filtrujepojedyncze wierszecałe grupy
Funkcje agregujące w warunkuniedozwolone (błąd składni)to jest jego jedyny sens
Wymaga GROUP BYniepraktycznie zawsze występuje razem z nim
Przykład warunkuo.ocena >= 2AVG(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.

Kolejność logiczna — do zapamiętania
  1. FROM i JOIN — serwer buduje jeden roboczy zestaw wierszy ze wszystkich złączonych tabel.
  2. WHERE — z tego zestawu wyrzuca wiersze niespełniające warunku.
  3. GROUP BY — pozostałe wiersze zbija w grupy i liczy dla nich funkcje agregujące.
  4. HAVING — odrzuca całe grupy niespełniające warunku.
  5. SELECT — dopiero teraz wybiera kolumny, oblicza wyrażenia i nadaje aliasy.
  6. DISTINCT — usuwa powtarzające się wiersze wyniku.
  7. ORDER BY — sortuje gotowy wynik.
  8. LIMIT — obcina go do zadanej liczby wierszy.
Dlaczego aliasu z SELECT nie użyjesz w WHERE

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;
Prześledzenie jednego zapytania krok po kroku

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;
  1. FROM + JOIN: powstaje tabela robocza, w której każdy wiersz to jedna ocena wraz z danymi ucznia i jego klasy.
  2. WHERE: znikają wiersze z oceną 1.
  3. GROUP BY: wiersze zbijają się w tyle grup, ile jest nazw klas; dla każdej grupy liczona jest średnia.
  4. HAVING: odpadają grupy ze średnią 4 lub niższą.
  5. SELECT: powstają kolumny klasa i srednia — dopiero tu nazwy aliasów zaczynają istnieć.
  6. ORDER BY: wynik sortowany malejąco po aliasie srednia.
  7. 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';
Poziomy uprawnień — od najszerszego do najwęższego
PoziomZapis po ONZasięg
globalny*.*wszystkie bazy na serwerze
bazy danychszkola.*wszystkie tabele jednej bazy
tabeliszkola.ocenyjedna wskazana tabela
kolumnySELECT (imie) ON szkola.uczniowiewskazane 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.

Najczęstsze pułapki egzaminacyjne
  • 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.
Sprawdź, ile z tego zostało w głowie

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.