SELECT … WHERE … ORDER BY
Podstawowe pobieranie danych z filtrowaniem i sortowaniem. Bez WHERE — zwraca wszystkie wiersze.
INNER JOIN
Łączy tabele — zwraca tylko wiersze z dopasowaniem w obu. Brak pary = wiersz pomijany.
LEFT JOIN
Wszystkie wiersze z tabeli lewej, nawet bez pary w prawej. Brak pary = NULL w kolumnach z prawej.
GROUP BY + HAVING
Grupuje wiersze o tych samych wartościach. HAVING filtruje grupy (WHERE nie zadziała po agregacji).
Podzapytania
Zapytanie wewnątrz zapytania — najczęściej w WHERE … IN (SELECT …). Wynik podzapytania staje się listą do filtrowania.
UPDATE / DELETE
UPDATE zmienia dane, DELETE usuwa rekordy. ZAWSZE z klauzulą WHERE — bez niej operacja dotknie całą tabelę.
| Typ | Co zwraca | Wiersze bez pary | Kiedy użyć? |
|---|---|---|---|
| INNER JOIN | Tylko wiersze z dopasowaniem w obu tabelach | Pomijane | Gdy chcesz tylko kompletne dane |
| LEFT JOIN | Wszystkie z lewej + dopasowane z prawej | NULL w kolumnach z prawej | Gdy chcesz znaleźć „sieroty" lub zachować wszystkich |
| RIGHT JOIN | Wszystkie z prawej + dopasowane z lewej | NULL w kolumnach z lewej | Rzadko — zastąp odwracając kolejność tabel i LEFT JOIN |
Cztery powiązane tabele modelujące pracę przychodni lekarskiej.
-- Stwórz bazę i wybierz ją CREATE DATABASE przychodnia; USE przychodnia; -- Tabela specjalizacji CREATE TABLE specjalizacje ( specjalizacja_id INT PRIMARY KEY AUTO_INCREMENT, nazwa VARCHAR(60) NOT NULL ); -- Tabela lekarzy CREATE TABLE lekarze ( lekarz_id INT PRIMARY KEY AUTO_INCREMENT, imie VARCHAR(40) NOT NULL, nazwisko VARCHAR(60) NOT NULL, specjalizacja_id INT, telefon VARCHAR(15), FOREIGN KEY (specjalizacja_id) REFERENCES specjalizacje(specjalizacja_id) ); -- Tabela pacjentów CREATE TABLE pacjenci ( pacjent_id INT PRIMARY KEY AUTO_INCREMENT, imie VARCHAR(40) NOT NULL, nazwisko VARCHAR(60) NOT NULL, data_urodzenia DATE, miasto VARCHAR(50) ); -- Tabela wizyt CREATE TABLE wizyty ( wizyta_id INT PRIMARY KEY AUTO_INCREMENT, pacjent_id INT, lekarz_id INT, data_wizyty DATE NOT NULL, diagnoza VARCHAR(120), koszt DECIMAL(7,2) NOT NULL, FOREIGN KEY (pacjent_id) REFERENCES pacjenci(pacjent_id), FOREIGN KEY (lekarz_id) REFERENCES lekarze(lekarz_id) ); -- Dane testowe INSERT INTO specjalizacje (nazwa) VALUES ('Kardiolog'), ('Ortopeda'), ('Dermatolog'), ('Internista'); INSERT INTO lekarze (imie, nazwisko, specjalizacja_id, telefon) VALUES ('Anna', 'Kowalska', 1, '600-111-001'), ('Marek', 'Nowak', 1, '600-111-002'), ('Joanna', 'Wiśniewska',2, '600-111-003'), ('Piotr', 'Zając', 3, '600-111-004'), ('Katarzyna','Dąbrowska', 4, '600-111-005'); INSERT INTO pacjenci (imie, nazwisko, data_urodzenia, miasto) VALUES ('Jan', 'Kowalski', '1985-03-12', 'Kielce'), ('Maria', 'Nowak', '1990-07-25', 'Kraków'), ('Adam', 'Wiśniewski','1978-11-30', 'Kielce'), ('Ewa', 'Kaczmarek', '2000-01-15', 'Warszawa'), ('Tomasz', 'Zielski', '1965-09-08', 'Pińczów'), ('Agata', 'Lewandowska','1995-04-20','Kraków'); -- Agata Lewandowska (id=6) nie ma żadnej wizyty — do zadania z LEFT JOIN INSERT INTO wizyty (pacjent_id, lekarz_id, data_wizyty, diagnoza, koszt) VALUES (1, 1, '2025-01-10', 'Nadciśnienie tętnicze', 150.00), (1, 2, '2025-02-14', 'Arytmia serca', 200.00), (2, 3, '2025-01-22', 'Skrzywienie kręgosłupa', 180.00), (3, 1, '2025-03-05', 'Choroba wieńcowa', 220.00), (3, 4, '2025-03-18', 'Trądzik różowaty', 120.00), (4, 5, '2025-04-02', 'Infekcja górnych dróg oddech.', 90.00), (5, 2, '2025-04-10', 'Migotanie przedsionków', 250.00), (5, 3, '2025-04-15', 'Złamanie kości śródstopia', 300.00), (2, 4, '2025-05-01', 'Łuszczyca plackowata', 140.00), (1, 5, '2025-05-12', 'Grypa sezonowa', 80.00);
SELECT imie, nazwisko, miasto FROM pacjenci WHERE miasto IN ('Kielce', 'Kraków') ORDER BY nazwisko ASC;
| imie | nazwisko | miasto |
|---|---|---|
| Jan | Kowalski | Kielce |
| Maria | Nowak | Kraków |
| Adam | Wiśniewski | Kielce |
| Agata | Lewandowska | Kraków |
Zapamiętaj: IN (…) to skrót dla OR — czytelniejszy i łatwiejszy do rozbudowania o kolejne wartości.
SELECT diagnoza, koszt FROM wizyty WHERE koszt > 150 ORDER BY koszt DESC;
| diagnoza | koszt |
|---|---|
| Złamanie kości śródstopia | 300.00 |
| Migotanie przedsionków | 250.00 |
| Choroba wieńcowa | 220.00 |
| Arytmia serca | 200.00 |
| Skrzywienie kręgosłupa | 180.00 |
| Łuszczyca plackowata | 140.00 |
SELECT p.imie AS pacjent_imie, p.nazwisko AS pacjent_nazwisko, w.data_wizyty, l.imie AS lekarz_imie, l.nazwisko AS lekarz_nazwisko FROM wizyty w INNER JOIN pacjenci p ON w.pacjent_id = p.pacjent_id INNER JOIN lekarze l ON w.lekarz_id = l.lekarz_id ORDER BY w.data_wizyty ASC;
| pacjent_imie | pacjent_nazwisko | data_wizyty | lekarz_imie | lekarz_nazwisko |
|---|---|---|---|---|
| Jan | Kowalski | 2025-01-10 | Anna | Kowalska |
| Maria | Nowak | 2025-01-22 | Joanna | Wiśniewska |
| Jan | Kowalski | 2025-02-14 | Marek | Nowak |
| Adam | Wiśniewski | 2025-03-05 | Anna | Kowalska |
| Adam | Wiśniewski | 2025-03-18 | Piotr | Zając |
| Ewa | Kaczmarek | 2025-04-02 | Katarzyna | Dąbrowska |
| Tomasz | Zielski | 2025-04-10 | Marek | Nowak |
| Tomasz | Zielski | 2025-04-15 | Joanna | Wiśniewska |
| Maria | Nowak | 2025-05-01 | Piotr | Zając |
| Jan | Kowalski | 2025-05-12 | Katarzyna | Dąbrowska |
Zapamiętaj: Tabela centralna to wizyty — ona przechowuje FK do obu stron. Zaczynamy JOIN od niej.
SELECT p.nazwisko AS pacjent, w.diagnoza, w.koszt, s.nazwa AS specjalizacja FROM wizyty w JOIN pacjenci p ON w.pacjent_id = p.pacjent_id JOIN lekarze l ON w.lekarz_id = l.lekarz_id JOIN specjalizacje s ON l.specjalizacja_id = s.specjalizacja_id ORDER BY w.koszt DESC;
| pacjent | diagnoza | koszt | specjalizacja |
|---|---|---|---|
| Zielski | Złamanie kości śródstopia | 300.00 | Ortopeda |
| Zielski | Migotanie przedsionków | 250.00 | Kardiolog |
| Wiśniewski | Choroba wieńcowa | 220.00 | Kardiolog |
| Kowalski | Arytmia serca | 200.00 | Kardiolog |
| Nowak | Skrzywienie kręgosłupa | 180.00 | Ortopeda |
| Kowalski | Nadciśnienie tętnicze | 150.00 | Kardiolog |
| Nowak | Łuszczyca plackowata | 140.00 | Dermatolog |
| Wiśniewski | Trądzik różowaty | 120.00 | Dermatolog |
| Kaczmarek | Infekcja górnych dróg oddech. | 90.00 | Internista |
| Kowalski | Grypa sezonowa | 80.00 | Internista |
-- Wszyscy pacjenci z informacją czy mają wizytę SELECT p.imie, p.nazwisko, w.wizyta_id, w.data_wizyty FROM pacjenci p LEFT JOIN wizyty w ON p.pacjent_id = w.pacjent_id ORDER BY p.nazwisko; -- Tylko ci bez żadnej wizyty SELECT p.imie, p.nazwisko, p.miasto FROM pacjenci p LEFT JOIN wizyty w ON p.pacjent_id = w.pacjent_id WHERE w.wizyta_id IS NULL;
| imie | nazwisko | miasto |
|---|---|---|
| Agata | Lewandowska | Kraków |
Klucz: INNER JOIN by ją pominął. LEFT JOIN zachowuje wszystkich pacjentów — NULL w kolumnach wizyty = brak rekordu w tej tabeli.
SELECT l.imie, l.nazwisko, COUNT(w.wizyta_id) AS liczba_wizyt, SUM(w.koszt) AS lacznie_zlotych FROM lekarze l JOIN wizyty w ON l.lekarz_id = w.lekarz_id GROUP BY l.lekarz_id, l.imie, l.nazwisko ORDER BY lacznie_zlotych DESC;
| imie | nazwisko | liczba_wizyt | lacznie_zlotych |
|---|---|---|---|
| Anna | Kowalska | 2 | 370.00 |
| Marek | Nowak | 2 | 450.00 |
| Joanna | Wiśniewska | 2 | 480.00 |
| Piotr | Zając | 2 | 260.00 |
| Katarzyna | Dąbrowska | 2 | 170.00 |
Uwaga: W GROUP BY wymieniamy wszystkie kolumny z SELECT, które nie są funkcją agregującą.
SELECT l.imie, l.nazwisko, SUM(w.koszt) AS przychod FROM lekarze l JOIN wizyty w ON l.lekarz_id = w.lekarz_id GROUP BY l.lekarz_id, l.imie, l.nazwisko HAVING SUM(w.koszt) > 300 ORDER BY przychod DESC;
| imie | nazwisko | przychod |
|---|---|---|
| Joanna | Wiśniewska | 480.00 |
| Marek | Nowak | 450.00 |
| Anna | Kowalska | 370.00 |
| Piotr | Zając | 260.00 |
HAVING vs WHERE: WHERE filtruje przed grupowaniem — nie widzi wyników SUM(). HAVING filtruje po GROUP BY — widzi agregaty.
SELECT s.nazwa AS specjalizacja, ROUND(AVG(w.koszt), 2) AS sredni_koszt, COUNT(w.wizyta_id) AS liczba_wizyt FROM wizyty w JOIN lekarze l ON w.lekarz_id = l.lekarz_id JOIN specjalizacje s ON l.specjalizacja_id = s.specjalizacja_id GROUP BY s.specjalizacja_id, s.nazwa ORDER BY sredni_koszt DESC;
| specjalizacja | sredni_koszt | liczba_wizyt |
|---|---|---|
| Ortopeda | 240.00 | 2 |
| Kardiolog | 205.00 | 4 |
| Dermatolog | 130.00 | 2 |
| Internista | 85.00 | 2 |
SELECT imie, nazwisko FROM pacjenci WHERE pacjent_id IN ( SELECT w.pacjent_id FROM wizyty w JOIN lekarze l ON w.lekarz_id = l.lekarz_id JOIN specjalizacje s ON l.specjalizacja_id = s.specjalizacja_id WHERE s.nazwa = 'Kardiolog' ) ORDER BY nazwisko;
| imie | nazwisko |
|---|---|
| Jan | Kowalski |
| Tomasz | Zielski |
| Adam | Wiśniewski |
Zapamiętaj: Podzapytanie w WHERE IN (…) jest wykonywane raz — jego wynik to lista ID, po której filtrujemy zapytanie zewnętrzne.
-- Krok 1: sprawdź co zmienisz SELECT w.wizyta_id, w.diagnoza, w.koszt FROM wizyty w JOIN lekarze l ON w.lekarz_id = l.lekarz_id JOIN specjalizacje s ON l.specjalizacja_id = s.specjalizacja_id WHERE s.nazwa = 'Internista'; -- Krok 2: dopiero teraz aktualizuj UPDATE wizyty SET koszt = koszt * 1.15 WHERE lekarz_id IN ( SELECT l.lekarz_id FROM lekarze l JOIN specjalizacje s ON l.specjalizacja_id = s.specjalizacja_id WHERE s.nazwa = 'Internista' ); -- Krok 3: weryfikacja SELECT wizyta_id, diagnoza, koszt FROM wizyty WHERE lekarz_id IN (SELECT lekarz_id FROM lekarze WHERE specjalizacja_id = 4);
| wizyta_id | diagnoza | koszt przed | koszt po |
|---|---|---|---|
| 6 | Infekcja górnych dróg oddech. | 90.00 | 103.50 |
| 10 | Grypa sezonowa | 80.00 | 92.00 |
-- Krok 1: sprawdź kogo usuniesz SELECT p.pacjent_id, p.imie, p.nazwisko, p.miasto FROM pacjenci p LEFT JOIN wizyty w ON p.pacjent_id = w.pacjent_id WHERE p.miasto = 'Kraków' AND w.wizyta_id IS NULL; -- Krok 2: dopiero teraz usuń DELETE FROM pacjenci WHERE miasto = 'Kraków' AND pacjent_id NOT IN ( SELECT pacjent_id FROM wizyty );
| Usunięty rekord | Powód |
|---|---|
| Agata Lewandowska (Kraków) | Brak jakiejkolwiek wizyty w tabeli wizyty |
Maria Nowak (Kraków) zostaje — ma wizyty. Klucz obcy i tak by zablokował usunięcie rekordu z powiązanymi wierszami.
SELECT p.imie, p.nazwisko, COUNT(DISTINCT w.lekarz_id) AS rozni_lekarze, SUM(w.koszt) AS lacznie_wydane FROM pacjenci p JOIN wizyty w ON p.pacjent_id = w.pacjent_id GROUP BY p.pacjent_id, p.imie, p.nazwisko HAVING COUNT(DISTINCT w.lekarz_id) > 1 ORDER BY lacznie_wydane DESC;
| imie | nazwisko | rozni_lekarze | lacznie_wydane |
|---|---|---|---|
| Tomasz | Zielski | 2 | 550.00 |
| Jan | Kowalski | 3 | 430.00 |
| Adam | Wiśniewski | 2 | 340.00 |
| Maria | Nowak | 2 | 320.00 |
COUNT(DISTINCT …) liczy unikalne wartości — bez DISTINCT policzyłby każdą wizytę osobno, nawet jeśli do tego samego lekarza.