Gdy raport ma najpierw znaleźć klientów spełniających określony warunek, a dopiero potem pobrać ich zamówienia, zwykły filtr często przestaje wystarczać. W praktyce określenie mysql subquery oznacza właśnie podzapytanie, czyli zapytanie zagnieżdżone w innym zapytaniu. Pokażę, jak używać go w klauzulach WHERE, SELECT i FROM, kiedy wybrać IN albo EXISTS oraz jak uniknąć typowych błędów i problemów z wydajnością.
Podzapytania pozwalają budować zapytania etapami
- Podzapytanie wykonuje pomocniczą operację wewnątrz głównego zapytania.
- IN sprawdza przynależność do zbioru, a EXISTS jedynie istnienie pasującego wiersza.
- Podzapytanie skalarne musi zwrócić jedną wartość i najwyżej jeden wiersz.
- Zapytanie w klauzuli FROM działa jak tymczasowa tabela i wymaga aliasu.
- Przy większych zbiorach danych zawsze sprawdzam plan wykonania przez EXPLAIN.

Jak działa podzapytanie w MySQL
Podzapytanie to instrukcja SQL umieszczona w nawiasach wewnątrz innej instrukcji. Najpierw dostarcza wynik pomocniczy, a zapytanie zewnętrzne wykorzystuje go do filtrowania, obliczeń albo utworzenia tymczasowego zbioru danych.
SELECT name, email
FROM customers
WHERE id IN (
SELECT customer_id
FROM orders
WHERE total_amount > 1000
);
W tym przykładzie wewnętrzna część znajduje klientów, którzy złożyli zamówienie o wartości większej niż 1000 zł. Zewnętrzne zapytanie pobiera ich dane kontaktowe. To czytelne rozwiązanie, gdy logika filtrowania ma wyraźnie dwa niezależne etapy.
Podzapytanie może znajdować się między innymi w:
-
WHERE, gdy służy do filtrowania rekordów, -
SELECT, gdy oblicza dodatkową wartość dla każdego wiersza, -
FROM, gdy tworzy tymczasowy wynik podobny do tabeli, -
HAVING, gdy filtruje już pogrupowane dane.
Najważniejsza zasada jest prosta. Operator użyty na zewnątrz musi pasować do tego, co zwraca środek. Podzapytanie skalarne zwraca jedną wartość, podzapytanie kolumnowe wiele wartości w jednej kolumnie, a podzapytanie w FROM może zwrócić pełną tabelę wynikową.
Najważniejsze rodzaje podzapytań z przykładami
Podzapytanie skalarne
Podzapytanie skalarne zwraca jedną wartość. Typowym zastosowaniem jest porównanie każdego rekordu ze średnią, maksimum albo minimum obliczonym dla całej tabeli.
SELECT id, product_name, price
FROM products
WHERE price > (
SELECT AVG(price)
FROM products
);
Zapytanie zwróci produkty droższe od średniej ceny. Jeśli wewnętrzna instrukcja zwróci dwa wiersze, MySQL przerwie wykonanie błędem Subquery returns more than 1 row. W takiej sytuacji trzeba użyć agregacji, LIMIT 1 albo operatora obsługującego wiele wartości.
Podzapytanie kolumnowe z IN
Operator IN sprawdza, czy wartość występuje w zbiorze zwróconym przez podzapytanie. Jest wygodny, gdy chcę pobrać rekordy należące do określonej grupy.
SELECT *
FROM employees
WHERE department_id IN (
SELECT id
FROM departments
WHERE city = 'Kraków'
);
Wewnętrzne zapytanie zwraca listę identyfikatorów działów, a zewnętrzne wybiera pracowników przypisanych do tych działów. To rozwiązanie jest łatwe do czytania, ale przy bardzo prostym powiązaniu tabel często równie dobrze sprawdzi się JOIN.
Podzapytanie w SELECT
Podzapytanie może wyliczyć dodatkową kolumnę dla każdego rekordu. Przykład poniżej pokazuje liczbę zamówień przypisanych do klienta.
SELECT
c.id,
c.name,
(
SELECT COUNT(*)
FROM orders o
WHERE o.customer_id = c.id
) AS order_count
FROM customers c;
To przykład podzapytania skorelowanego, ponieważ odwołuje się do aliasu z zapytania zewnętrznego. Jest bardzo intuicyjne, lecz przy dużej liczbie klientów może wymagać szczególnej kontroli planu wykonania. Czasem agregujący LEFT JOIN będzie szybszy i łatwiejszy do optymalizacji.
Podzapytanie w FROM jako tabela pochodna
Zapytanie umieszczone w klauzuli FROM tworzy tak zwaną tabelę pochodną. MySQL wymaga, aby miała ona alias.
SELECT department_id, average_salary
FROM (
SELECT department_id, AVG(salary) AS average_salary
FROM employees
GROUP BY department_id
) AS department_stats
WHERE average_salary > 9000;
Ten wzorzec dobrze sprawdza się wtedy, gdy trzeba wykonać agregację, a później filtrować jej wynik. Nie można bezpośrednio użyć aliasu agregacji w zwykłym WHERE, dlatego najpierw tworzę tabelę pochodną, a dopiero potem filtruję średnie wynagrodzenia.
IN, EXISTS i JOIN nie rozwiązują tego samego problemu
Najwięcej nieporozumień pojawia się przy wyborze między IN i EXISTS. Pierwszy operator porównuje wartość ze zbiorem, drugi sprawdza, czy podzapytanie zwróciło choć jeden wiersz. Dokumentacja MySQL opisuje EXISTS jako test istnienia, dlatego lista kolumn w jego środku nie ma praktycznego znaczenia. Dokumentacja MySQL dotycząca EXISTS pokazuje ten mechanizm na przykładach korelacji między tabelami.
SELECT c.id, c.name
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
W tym przypadku nie interesuje mnie liczba zamówień ani ich suma. Chcę tylko wiedzieć, czy klient ma przynajmniej jedno zamówienie. Dlatego EXISTS często lepiej opisuje intencję niż pobieranie całej listy identyfikatorów.
Porównanie najczęstszych wariantów wygląda tak:
| Rozwiązanie | Najlepsze zastosowanie | Na co uważać |
|---|---|---|
IN |
Sprawdzanie, czy wartość należy do listy | Problemy z logiką NULL przy NOT IN
|
EXISTS |
Sprawdzanie, czy istnieje pasujący rekord | Potrzebna poprawna korelacja z zapytaniem zewnętrznym |
JOIN |
Pobieranie kolumn z kilku tabel | Ryzyko zwielokrotnienia wierszy |
Podzapytanie w FROM
|
Praca na wyniku agregacji lub przefiltrowanej tabeli | Konieczność użycia aliasu i kontroli materializacji |
Szczególnie ostrożnie traktuję konstrukcję NOT IN. Jeśli podzapytanie zwróci choć jedną wartość NULL, wynik porównania może stać się nieokreślony i nie zwrócić oczekiwanych rekordów. W takich sytuacjach bezpieczniejszym wyborem bywa NOT EXISTS.
Podzapytania skorelowane i typowe błędy
Podzapytanie skorelowane odwołuje się do kolumny z zapytania zewnętrznego. Wykonanie logicznie przypomina przejście po każdym rekordzie zewnętrznym i sprawdzenie dopasowanych danych wewnątrz.
SELECT p.id, p.product_name, p.price
FROM products p
WHERE p.price > (
SELECT AVG(p2.price)
FROM products p2
WHERE p2.category_id = p.category_id
);
Ten przykład wybiera produkty droższe od średniej w ich własnej kategorii. Jest bardzo użyteczny analitycznie, ale nie zakładam automatycznie, że będzie najlepszy wydajnościowo. Dla dużych tabel porównuję go z wersją opartą na agregacji i JOIN.
Błąd wielu wierszy
SELECT *
FROM products
WHERE category_id = (
SELECT id
FROM categories
WHERE active = 1
);
Jeśli aktywnych kategorii jest więcej niż jedna, operator = nie pasuje do wyniku. Należy użyć IN albo doprecyzować warunek tak, aby podzapytanie zwracało dokładnie jeden wiersz.
Nieprawidłowa liczba kolumn
Podzapytanie użyte jako pojedyncza wartość nie może zwrócić dwóch kolumn. Taki zapis jest błędny:
SELECT
(SELECT id, name FROM categories LIMIT 1);
Jeżeli potrzebne są dwie wartości, można użyć porównania wierszy, połączyć tabele przez JOIN albo rozbić operację na dwa pola. W zwykłym wyrażeniu skalarne podzapytanie powinno zwracać jedną kolumnę.
Przeczytaj również: MERGE INTO w SQL - jak bezpiecznie synchronizować dane
Modyfikowanie tabeli użytej w podzapytaniu
MySQL ogranicza bezpośrednią modyfikację tabeli, z której jednocześnie pobierane są dane w podzapytaniu. Przykład może zakończyć się błędem ERROR 1093:
UPDATE products
SET price = price * 1.10
WHERE price < (
SELECT AVG(price)
FROM products
);
Przy takich operacjach trzeba rozważyć tabelę pochodną, wspólne wyrażenie tabelowe albo wielotabelowy UPDATE. Ograniczenia i dostępne obejścia opisuje dokumentacja ograniczeń podzapytań MySQL.
Jak zadbać o wydajność podzapytań
Podzapytanie nie jest z definicji wolniejsze od JOIN. Współczesny optymalizator może przekształcić niektóre konstrukcje, między innymi użyć strategii semijoin, materializacji albo zamienić tabelę pochodną na część większego planu. Nie liczę jednak na to bez sprawdzenia.
EXPLAIN
SELECT c.id, c.name
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
W planie wykonania sprawdzam przede wszystkim, czy kolumna używana do korelacji ma indeks. W powyższym przypadku powinien istnieć indeks na orders.customer_id. Bez niego MySQL może wielokrotnie skanować tabelę zamówień.
- Indeksuj kolumny używane w warunkach korelacji.
- Nie używaj
SELECT *, jeśli wystarczy jedna kolumna. - Przenieś filtr do podzapytania, gdy ogranicza liczbę przetwarzanych wierszy.
- Porównaj podzapytanie z wersją opartą na
JOIN. - Sprawdź plan przez
EXPLAIN ANALYZE, gdy problem dotyczy rzeczywistego czasu wykonania.
Dokumentacja MySQL wskazuje, że tabela pochodna może zostać scalona z zapytaniem zewnętrznym albo zmaterializowana jako tymczasowy wynik. Opis optymalizacji podzapytań w MySQL dobrze pokazuje, dlaczego identycznie wyglądające zapytania mogą mieć różne plany wykonania.
Jak wybieram właściwe rozwiązanie w praktyce
Podzapytanie wybieram wtedy, gdy pomocniczy etap ma własny sens i poprawia czytelność logiki. Dobrym przykładem jest znalezienie produktów droższych od średniej albo klientów, dla których istnieje przynajmniej jedno zamówienie.
Gdy potrzebuję jednocześnie wyświetlić kolumny z kilku tabel, częściej wybieram JOIN. Gdy interesuje mnie wyłącznie odpowiedź „czy istnieje”, preferuję EXISTS. Gdy mam listę dopuszczalnych wartości, naturalnym wyborem pozostaje IN.
Najlepsza praktyka jest prosta: najpierw napisz zapytanie, które jasno wyraża wymaganie, a potem sprawdź jego plan. Czytelność ma znaczenie, ale przy milionach rekordów równie ważne są indeksy, selektywność filtrów i liczba wierszy przetwarzanych przez każdą część instrukcji.
Podzapytanie jako narzędzie do porządkowania logiki SQL
Podzapytania w MySQL najlepiej traktować jako sposób na rozłożenie złożonego problemu na mniejsze kroki. IN sprawdza przynależność, EXISTS obecność rekordu, podzapytanie skalarne dostarcza pojedynczej wartości, a tabela pochodna pozwala pracować na wyniku wcześniejszej agregacji.
Nie każdą konstrukcję trzeba jednak zachować w finalnej wersji. Jeśli zapytanie staje się trudne do optymalizacji, porównuję je z JOIN, CTE i tabelą tymczasową, a decyzję podejmuję na podstawie planu wykonania. To właśnie połączenie czytelnej składni i pomiaru wydajności daje w praktyce najlepszy rezultat.
