JOIN w SQL


SQL JOIN

JOIN(INNER JOIN)

JOIN służy do łączenia danych z wielu tabel w jednym wyniku.

Masz np.:

  • customers (klienci)
  • orders (zamówienia), gdzie orders.customer_id wskazuje na customers.id

JOIN pozwala w jednym wyniku pokazać np. order_id + email klienta.

Warunek dopasowania zapisujesz w ON:

... JOIN customers c ON c.id = o.customer_id

W MySQL/MariaDB samo słowo JOIN oznacza INNER JOIN (czyli INNER jest opcjonalne).

Czyli te zapytania są równoważne:

SELECT ...
FROM a JOIN b ON ...


SELECT ...
FROM a INNER JOIN b ON ...

Czy warto pisać INNER?
Dla nauki i czytelności często tak — łatwiej od razu widzieć, że to złączenie „tylko dopasowane”.

Najważniejsze typy JOIN

INNER JOIN (JOIN) — „część wspólna”

Definicja: Zwraca tylko te wiersze, które mają dopasowanie w obu tabelach.

Przykład: zamówienia z danymi klienta (każde zamówienie ma klienta, więc INNER ma sens)

SELECT
  o.id AS order_id,
  o.status,
  c.email
FROM orders o
JOIN customers c ON c.id = o.customer_id;

Kiedy używać: gdy rekord po obu stronach musi istnieć (np. orders zawsze wskazuje na customers)

OUTER jest opcjonalne przy LEFT/RIGHT

W MySQL/MariaDB w LEFT JOIN i RIGHT JOIN słowo OUTER jest opcjonalne (czyli LEFT JOIN = LEFT OUTER JOIN).

Czy warto pisać OUTER?

  • Nie trzeba.
  • Może poprawiać czytelność początkującym, bo podkreśla „zewnętrzność” (zachowanie niedopasowanych wierszy).

LEFT JOIN (LEFT OUTER JOIN) — „zachowaj lewą tabelę”

Definicja: Zwraca:

  • wszystkie wiersze z lewej tabeli
  • plus dopasowane wiersze z prawej
  • a gdy brak dopasowania → kolumny prawej tabeli są NULL

Przykład: wszystkie zamówienia + płatność jeśli była

SELECT
  o.id AS order_id,
  o.status,
  p.method,
  p.amount
FROM orders o
LEFT JOIN payments p ON p.order_id = o.id;

RIGHT JOIN (RIGHT OUTER JOIN) — „zachowaj prawą tabelę”

To samo co LEFT JOIN, tylko zachowujesz prawą tabelę.

W praktyce często da się go uniknąć: zamieniasz kolejność tabel i robisz LEFT JOIN. MySQL nawet potrafi wewnętrznie upraszczać RIGHT do LEFT.

FULL OUTER JOIN — ważne: MariaDB/MySQL nie wspiera bezpośrednio

W MySQL/MariaDB nie ma natywnego FULL OUTER JOIN. (devart.com)
Robi się obejście przez UNION dwóch zapytań:

SELECT a.id, b.id
FROM a
LEFT JOIN b ON b.a_id = a.id
UNION
SELECT a.id, b.id
FROM a
RIGHT JOIN b ON b.a_id = a.id;

(Uwaga: UNION usuwa duplikaty; UNION ALL ich nie usuwa.)

CROSS JOIN — iloczyn kartezjański (każdy z każdym)

Definicja: Łączy każdy wiersz z A z każdym wierszem z B. W MariaDB CROSS JOIN jest syntaktycznie równoważny INNER JOIN (różnice są „stylowe” i dotyczą tego, czy używasz warunku ON).

SELECT *
FROM customers
CROSS JOIN products;

Składnia JOIN — jak czytać zapytanie

SELECT ...
FROM T1
[INNER] JOIN T2 ON warunek_dopasowania
LEFT  [OUTER] JOIN T3 ON warunek_dopasowania
WHERE warunki_filtrowania
ORDER BY ...
LIMIT ...;

Dobra praktyka:

  • ON = warunki dopasowania tabel
  • WHERE = warunki filtrowania wyników (ale patrz sekcja o błędach przy LEFT JOIN)

ON vs WHERE — najważniejsza rzecz przy LEFT JOIN

Dokumentacja MySQL opisuje, że w LEFT JOIN warunek ON decyduje o dopasowaniu wierszy z prawej tabeli, a w razie braku dopasowania generowany jest wiersz z NULL po stronie prawej.

Błąd: „LEFT JOIN, ale znikają wiersze”

Chcesz: „wszystkie zamówienia + płatność jeśli jest, ale tylko płatności kartą”.

#Źle: To usuwa wiersze, gdzie p.method jest NULL → więc zamówienia bez płatności znikają.
SELECT o.id, p.method
FROM orders o
LEFT JOIN payments p ON p.order_id = o.id
WHERE p.method = 'card';

#Dobrze (warunek na prawą tabelę w ON):
SELECT o.id, p.method
FROM orders o
LEFT JOIN payments p
  ON p.order_id = o.id
 AND p.method = 'card';

Najczęstsze błędy i jak je naprawić

Błąd 1: „Znikające wiersze” po LEFT JOIN

Objaw: miały być „wszystkie”, a jest mniej.
Powód: filtr na prawej tabeli w WHERE.
Naprawa: przenieś filtr do ON albo dopisz logicznie OR p.id IS NULL (zależnie od celu).

Błąd 2: „Duplikowanie” wierszy (mnożenie przez relację 1‑do‑wielu)

Jeśli łączysz:

  • orders (1)
  • order_items (wiele)

to jedno zamówienie pojawi się tyle razy, ile ma pozycji.

To jest normalne, ale psuje agregaty, gdy dołączysz coś 1‑do‑1 (np. payments) i potem sumujesz.

Przykład problemu: kwota płatności powtórzona dla każdej pozycji:

 

SELECT o.id, SUM(p.amount)  -- UWAGA: to może zawyżać!
FROM orders o
JOIN payments p ON p.order_id = o.id
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id;

Poprawna idea: najpierw policz agregat pozycji per zamówienie, a płatność dołącz dopiero do zredukowanego wyniku (np. podzapytaniem).

Błąd 3: COUNT(*) vs COUNT(kolumna) przy LEFT JOIN

  • COUNT(*) liczy wiersze wyniku (nawet gdy prawa strona jest NULL)
  • COUNT(o.id) policzy 0 dla klientów bez zamówień
SELECT c.id, COUNT(o.id) AS orders_count
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id;

Błąd 4: mieszanie „starej składni” z przecinkami z JOIN

MySQL zwraca uwagę, że mieszanie , (stare łączenie tabel) z JOIN może prowadzić do mylących błędów i problemów z priorytetami operatorów.
Trzymaj się składni ANSI:

FROM a JOIN b ON ...
#a nie FROM a, b WHERE ...

Krótka ściąga: jak dobrać JOIN (decyzja w 3 pytaniach)

  • Czy chcesz zachować wszystkie wiersze z tabeli A nawet bez dopasowania?
    → LEFT JOIN
  • Czy chcesz tylko wiersze mające dopasowanie po obu stronach?
    → INNER JOIN (JOIN)
  • Czy potrzebujesz „wszystko z obu stron”?
    → w MariaDB/MySQL: UNION (symulacja FULL)

Ćwiczenia

baza do ćwiczeń –pobierz plik txt

1) Wyświetl wszystkie zamówienia oraz dane klienta, który złożył zamówienie.
W wyniku: id zamówienia, status, created_at, e-mail klienta.
Podpowiedź: INNER JOIN orders → customers (po customer_id).

2) Wyświetl tylko zamówienia o statusie 'PAID’ i pokaż dane klienta.
W wyniku: id, status, created_at, e-mail.
Podpowiedź: INNER JOIN + WHERE o.status=’PAID’.

3) Wyświetl wszystkie zamówienia i jeśli zamówienie zostało opłacone, pokaż dane płatności.
W wyniku: id, status, method, paid_at, amount (dla nieopłaconych NULL).
Podpowiedź: LEFT JOIN orders → payments (po order_id).

4) Wyświetl zamówienia, które nie mają żadnej płatności.
W wyniku: id, status, created_at.
Podpowiedź: LEFT JOIN payments + WHERE payments.order_id IS NULL.

5) Wyświetl wszystkie zamówienia i jeśli zamówienie zostało wysłane, pokaż dane wysyłki.
W wyniku: id, status, courier, shipped_at, delivered_at.
Podpowiedź: LEFT JOIN orders → shipments.

6) Wyświetl zamówienia, które nie mają żadnej wysyłki.
W wyniku: id, status, created_at.
Podpowiedź: LEFT JOIN shipments + WHERE shipments.order_id IS NULL.

7) Wyświetl wszystkie zamówienia i jeśli użyto kuponu, pokaż kod i procent rabatu.
W wyniku: id, status, code, discount_pct (lub NULL).
Podpowiedź: LEFT JOIN orders → coupons (po coupon_id).

8) Wyświetl zamówienia, w których nie użyto kuponu.
W wyniku: id, status, created_at.
Podpowiedź: WHERE orders.coupon_id IS NULL.

9) Wyświetl wszystkie pozycje zamówień (co kupiono).
W wyniku: id zamówienia, nazwa produktu, qty.
Podpowiedź: JOIN orders → order_items → products.

10) Wyświetl pozycje zamówień razem z kategorią produktu.
W wyniku: order_id, product_name, category_name, qty.
Podpowiedź: JOIN 4 tabel: orders → items → products → categories.

11) Dla każdego zamówienia policz, ile pozycji zawiera.
W wyniku: order_id, items_count.
Podpowiedź: LEFT JOIN order_items + GROUP BY o.id + COUNT(oi.id).

12) Dla każdego zamówienia oblicz sumę netto z pozycji.
W wyniku: order_id, sum_net.
Podpowiedź: SUM(qty*price_net_at_time) + GROUP BY order_id.

13) Dla każdego zamówienia oblicz sumę brutto (z VAT) z pozycji.
W wyniku: order_id, sum_gross.
Podpowiedź: SUM(qtyprice_net_at_time(1+vat_rate_at_time)).

14) Pokaż tylko zamówienia, których suma brutto > 30.
W wyniku: order_id, sum_gross.
Podpowiedź: HAVING SUM(…) > 30 (nie WHERE).

15) Wyświetl zamówienia z e-mailem klienta, od najnowszego.
W wyniku: order_id, created_at, status, email.
Podpowiedź: JOIN customers + ORDER BY o.created_at DESC.

16) Wyświetl wszystkich klientów i policz, ile zamówień ma każdy (także 0).
W wyniku: customer_id, email, orders_count.
Podpowiedź: LEFT JOIN customers → orders + COUNT(o.id).

17) Wyświetl tylko klientów bez zamówień.
W wyniku: customer_id, email.
Podpowiedź: LEFT JOIN orders + WHERE o.id IS NULL.

18) Wyświetl wszystkich klientów i sumę brutto ich zamówień (także 0).
W wyniku: email, total_gross.
Podpowiedź: LEFT JOIN customers→orders→order_items + SUM(…) + COALESCE.

19) Wyświetl klienta z największą liczbą zamówień.
W wyniku: email, orders_count.
Podpowiedź: zad. 16 + ORDER BY orders_count DESC + LIMIT 1.

20) Wyświetl klienta z największą sumą brutto.
W wyniku: email, total_gross.
Podpowiedź: zad. 18 + ORDER BY total_gross DESC + LIMIT 1.

21) Wyświetl produkty razem z kategorią.
W wyniku: product_id, product_name, category_name, price_net, vat_rate.
Podpowiedź: JOIN products → categories.

22) Wyświetl kategorie i policz produkty w każdej (także 0).
W wyniku: category_name, products_count.
Podpowiedź: LEFT JOIN categories → products + COUNT(pr.id).

23) Wyświetl kategorię z największą liczbą produktów.
W wyniku: category_name, products_count.
Podpowiedź: zad. 22 + ORDER BY products_count DESC + LIMIT 1.

24) Wyświetl średnią cenę netto produktów w każdej kategorii.
W wyniku: category_name, avg_price_net.
Podpowiedź: AVG(price_net) + GROUP BY category.

25) Wyświetl produkty, które były zamówione przynajmniej raz (bez duplikatów).
W wyniku: product_id, product_name.
Podpowiedź: JOIN order_items + DISTINCT.

26) Wyświetl produkty, które nigdy nie były zamówione.
W wyniku: product_id, product_name.
Podpowiedź: LEFT JOIN products → order_items + WHERE oi.product_id IS NULL.

27) Wyświetl 3 najczęściej kupowane produkty (po sumie sztuk).
W wyniku: product_name, qty_total.
Podpowiedź: SUM(oi.qty) GROUP BY product, ORDER BY DESC LIMIT 3.

28) Wyświetl 3 produkty o największej wartości sprzedaży netto.
W wyniku: product_name, sales_net.
Podpowiedź: SUM(qty*price_net_at_time) GROUP BY product, LIMIT 3.

29) Wyświetl kategorię o największej sprzedaży netto.
W wyniku: category_name, sales_net.
Podpowiedź: categories→products→order_items + SUM(…) + LIMIT 1.

30) Dla każdej kategorii wyświetl listę sprzedanych produktów w jednej kolumnie.
W wyniku: category_name, sold_products.
Podpowiedź: GROUP_CONCAT(DISTINCT product_name ORDER BY …).

31) Wyświetl zamówienia oraz dane płatności i wysyłki, jeśli istnieją.
W wyniku: order_id, status, method/amount, courier/shipped/delivered.
Podpowiedź: orders LEFT JOIN payments LEFT JOIN shipments.

32) Wyświetl zamówienia opłacone, ale niewysłane.
W wyniku: order_id, method, paid_at.
Podpowiedź: JOIN payments (musi istnieć) + LEFT JOIN shipments + WHERE shipments.order_id IS NULL.

33) Wyświetl zamówienia wysłane, ale niedostarczone.
W wyniku: order_id, courier, shipped_at, delivered_at.
Podpowiedź: JOIN shipments + WHERE delivered_at IS NULL.

34) Wyświetl anulowane zamówienia oraz ich pozycje.
W wyniku: order_id, product_name, qty.
Podpowiedź: JOIN orders→items→products + WHERE o.status=’CANCELLED’.

35) Dla każdego zamówienia pokaż jednocześnie liczbę pozycji i sumę brutto.
W wyniku: order_id, items_count, sum_gross.
Podpowiedź: GROUP BY order_id + COUNT + SUM w jednym SELECT.

36) Wyświetl zamówienia opłacone metodą 'card’ oraz e-mail klienta.
W wyniku: order_id, email, paid_at, amount.
Podpowiedź: JOIN payments + WHERE p.method=’card’ + JOIN customers.

37) Policz liczbę płatności dla każdej metody.
W wyniku: method, n.
Podpowiedź: GROUP BY payments.method.

38) Policz sumę kwot płatności dla każdej metody.
W wyniku: method, total_amount.
Podpowiedź: SUM(amount) GROUP BY method.

39) Wyświetl zamówienia, gdzie payment.amount różni się od wyliczonego brutto z pozycji.
W wyniku: order_id, computed_gross, paid_amount, diff.
Podpowiedź: JOIN payments + JOIN order_items + GROUP BY order_id + HAVING ABS(diff) > 0.01.

40) Wyświetl zamówienia i dodaj informację, czy użyto kuponu.
W wyniku: order_id, status, coupon_used, code, discount_pct.
Podpowiedź: LEFT JOIN coupons + IF(o.coupon_id IS NULL,’NIE’,’TAK’).

41) Raport dzienny: liczba zamówień per dzień.
W wyniku: data, orders_count.
Podpowiedź: GROUP BY DATE(o.created_at).

42) Raport dzienny: suma brutto per dzień.
W wyniku: data, gross_total.
Podpowiedź: JOIN order_items + GROUP BY DATE(o.created_at) + SUM(brutto).

43) Raport: klient + miesiąc (YYYY-MM) + liczba zamówień.
W wyniku: email, ym, orders_count.
Podpowiedź: DATE_FORMAT(o.created_at,’%Y-%m’) + GROUP BY email, ym.

44) Raport: kategoria + miesiąc + sprzedaż netto.
W wyniku: category, ym, sales_net.
Podpowiedź: orders→items→products→categories + GROUP BY category, ym.

45) Dla każdego zamówienia pokaż listę produktów w jednej kolumnie.
W wyniku: order_id, products_list.
Podpowiedź: GROUP_CONCAT(product_name) + GROUP BY order_id.

46) Zrób FULL OUTER JOIN między orders i payments (obejście UNION).
W wyniku: order_id, payment_id, amount.
Podpowiedź: (orders LEFT JOIN payments) UNION (orders RIGHT JOIN payments).

47) Wyświetl wszystkie kupony i policz, ile razy każdy został użyty (także 0).
W wyniku: code, discount_pct, used_count.
Podpowiedź: LEFT JOIN coupons → orders + COUNT(o.id).

48) Wyświetl kupony nigdy nieużyte.
W wyniku: code, discount_pct.
Podpowiedź: LEFT JOIN orders + WHERE o.id IS NULL.

49) Dla zamówień z kuponem policz brutto przed i po rabacie.
W wyniku: order_id, code, gross_before, gross_after.
Podpowiedź: JOIN coupons + SUM(brutto) + * (1 – discount_pct/100).

50) Dla każdego klienta policz sumę „zaoszczędzoną” dzięki kuponom.
W wyniku: email, saved_total.
Podpowiedź: dla pozycji licz brutto * (discount_pct/100), zsumuj per klient.

 

Odpowiedzi

— 1) Zamówienia + dane klienta (INNER JOIN)
SELECT
o.id AS order_id,
o.status,
o.created_at,
c.email
FROM orders o
JOIN customers c ON c.id = o.customer_id
ORDER BY o.id;
— Uwaga: JOIN = INNER JOIN.

— 2) Zamówienia 'PAID’ + klient
SELECT
o.id AS order_id,
o.status,
o.created_at,
c.email
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'PAID’
ORDER BY o.created_at;

— 3) Wszystkie zamówienia + płatność jeśli jest
SELECT
o.id AS order_id,
o.status,
p.method,
p.paid_at,
p.amount
FROM orders o
LEFT JOIN payments p ON p.order_id = o.id
ORDER BY o.id;
— Uwaga: LEFT JOIN zostawia zamówienia bez płatności (p.* = NULL).

— 4) Zamówienia bez płatności
SELECT
o.id AS order_id,
o.status,
o.created_at
FROM orders o
LEFT JOIN payments p ON p.order_id = o.id
WHERE p.order_id IS NULL
ORDER BY o.id;

— 5) Wszystkie zamówienia + wysyłka jeśli jest
SELECT
o.id AS order_id,
o.status,
s.courier,
s.shipped_at,
s.delivered_at
FROM orders o
LEFT JOIN shipments s ON s.order_id = o.id
ORDER BY o.id;

— 6) Zamówienia bez wysyłki
SELECT
o.id AS order_id,
o.status,
o.created_at
FROM orders o
LEFT JOIN shipments s ON s.order_id = o.id
WHERE s.order_id IS NULL
ORDER BY o.id;

— 7) Wszystkie zamówienia + kupon (jeśli użyto)
SELECT
o.id AS order_id,
o.status,
cp.code,
cp.discount_pct
FROM orders o
LEFT JOIN coupons cp ON cp.id = o.coupon_id
ORDER BY o.id;

— 8) Zamówienia bez kuponu
SELECT
o.id AS order_id,
o.status,
o.created_at
FROM orders o
WHERE o.coupon_id IS NULL
ORDER BY o.id;

— 9) Wszystkie pozycje zamówień: zamówienie + produkt + qty
SELECT
o.id AS order_id,
pr.name AS product_name,
oi.qty
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products pr ON pr.id = oi.product_id
ORDER BY o.id, product_name;

— 10) Pozycje + kategoria
SELECT
o.id AS order_id,
pr.name AS product_name,
cat.name AS category_name,
oi.qty
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products pr ON pr.id = oi.product_id
JOIN categories cat ON cat.id = pr.category_id
ORDER BY o.id, category_name, product_name;

— 11) Liczba pozycji na zamówienie
SELECT
o.id AS order_id,
COUNT(oi.id) AS items_count
FROM orders o
LEFT JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id
ORDER BY o.id;
— Uwaga: COUNT(oi.id) (a nie COUNT(*)) daje 0 jeśli brak pozycji.

— 12) Suma netto na zamówienie
SELECT
o.id AS order_id,
ROUND(SUM(oi.qty * oi.price_net_at_time), 2) AS sum_net
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id
ORDER BY o.id;

— 13) Suma brutto na zamówienie
SELECT
o.id AS order_id,
ROUND(SUM(oi.qty * oi.price_net_at_time * (1 + oi.vat_rate_at_time)), 2) AS sum_gross
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id
ORDER BY o.id;

— 14) Zamówienia z sumą brutto > 30
SELECT
o.id AS order_id,
ROUND(SUM(oi.qty * oi.price_net_at_time * (1 + oi.vat_rate_at_time)), 2) AS sum_gross
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id
HAVING SUM(oi.qty * oi.price_net_at_time * (1 + oi.vat_rate_at_time)) > 30
ORDER BY sum_gross DESC;

— 15) Zamówienia + e-mail klienta, od najnowszego
SELECT
o.id AS order_id,
o.created_at,
o.status,
c.email
FROM orders o
JOIN customers c ON c.id = o.customer_id
ORDER BY o.created_at DESC;

— 16) Wszyscy klienci + liczba zamówień (także 0)
SELECT
c.id AS customer_id,
c.email,
COUNT(o.id) AS orders_count
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.email
ORDER BY c.id;

— 17) Klienci bez zamówień
SELECT
c.id AS customer_id,
c.email
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL
ORDER BY c.id;

— 18) Wszyscy klienci + suma brutto zamówień (także 0)
SELECT
c.email,
COALESCE(
ROUND(SUM(oi.qty * oi.price_net_at_time * (1 + oi.vat_rate_at_time)), 2),
0
) AS total_gross
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
LEFT JOIN order_items oi ON oi.order_id = o.id
GROUP BY c.email
ORDER BY total_gross DESC, c.email;

— 19) Klient z największą liczbą zamówień
SELECT
c.email,
COUNT(o.id) AS orders_count
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.email
ORDER BY orders_count DESC
LIMIT 1;

— 20) Klient z największą sumą brutto
SELECT
c.email,
COALESCE(
ROUND(SUM(oi.qty * oi.price_net_at_time * (1 + oi.vat_rate_at_time)), 2),
0
) AS total_gross
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
LEFT JOIN order_items oi ON oi.order_id = o.id
GROUP BY c.email
ORDER BY total_gross DESC
LIMIT 1;

— 21) Produkty + kategorie
SELECT
pr.id AS product_id,
pr.name AS product_name,
cat.name AS category_name,
pr.price_net,
pr.vat_rate
FROM products pr
JOIN categories cat ON cat.id = pr.category_id
ORDER BY category_name, product_name;

— 22) Kategorie + liczba produktów (także 0)
SELECT
cat.name AS category_name,
COUNT(pr.id) AS products_count
FROM categories cat
LEFT JOIN products pr ON pr.category_id = cat.id
GROUP BY cat.name
ORDER BY category_name;

— 23) Kategoria z największą liczbą produktów
SELECT
cat.name AS category_name,
COUNT(pr.id) AS products_count
FROM categories cat
LEFT JOIN products pr ON pr.category_id = cat.id
GROUP BY cat.name
ORDER BY products_count DESC
LIMIT 1;

— 24) Średnia cena netto produktów w kategorii
SELECT
cat.name AS category_name,
ROUND(AVG(pr.price_net), 2) AS avg_price_net
FROM categories cat
JOIN products pr ON pr.category_id = cat.id
GROUP BY cat.name
ORDER BY avg_price_net DESC, category_name;

— 25) Produkty zamówione przynajmniej raz (bez duplikatów)
SELECT DISTINCT
pr.id AS product_id,
pr.name AS product_name
FROM products pr
JOIN order_items oi ON oi.product_id = pr.id
ORDER BY product_id;

— 26) Produkty nigdy niezamówione
SELECT
pr.id AS product_id,
pr.name AS product_name
FROM products pr
LEFT JOIN order_items oi ON oi.product_id = pr.id
WHERE oi.product_id IS NULL
ORDER BY pr.id;

— 27) TOP 3 najczęściej kupowane produkty (SUM ilości)
SELECT
pr.name AS product_name,
SUM(oi.qty) AS qty_total
FROM products pr
JOIN order_items oi ON oi.product_id = pr.id
GROUP BY pr.id, pr.name
ORDER BY qty_total DESC, product_name
LIMIT 3;

— 28) TOP 3 produkty o największej sprzedaży netto
SELECT
pr.name AS product_name,
ROUND(SUM(oi.qty * oi.price_net_at_time), 2) AS sales_net
FROM products pr
JOIN order_items oi ON oi.product_id = pr.id
GROUP BY pr.id, pr.name
ORDER BY sales_net DESC, product_name
LIMIT 3;

— 29) Kategoria o największej sprzedaży netto
SELECT
cat.name AS category_name,
ROUND(SUM(oi.qty * oi.price_net_at_time), 2) AS sales_net
FROM categories cat
JOIN products pr ON pr.category_id = cat.id
JOIN order_items oi ON oi.product_id = pr.id
GROUP BY cat.id, cat.name
ORDER BY sales_net DESC
LIMIT 1;

— 30) Kategoria + lista sprzedanych produktów
SELECT
cat.name AS category_name,
GROUP_CONCAT(DISTINCT pr.name ORDER BY pr.name SEPARATOR ’, ’) AS sold_products
FROM categories cat
JOIN products pr ON pr.category_id = cat.id
JOIN order_items oi ON oi.product_id = pr.id
GROUP BY cat.id, cat.name
ORDER BY category_name;

— 31) Zamówienia + płatność + wysyłka
SELECT
o.id AS order_id,
o.status,
o.created_at,
p.method,
p.amount,
s.courier,
s.shipped_at,
s.delivered_at
FROM orders o
LEFT JOIN payments p ON p.order_id = o.id
LEFT JOIN shipments s ON s.order_id = o.id
ORDER BY o.id;

— 32) Opłacone, ale niewysłane
SELECT
o.id AS order_id,
p.method,
p.paid_at
FROM orders o
JOIN payments p ON p.order_id = o.id
LEFT JOIN shipments s ON s.order_id = o.id
WHERE s.order_id IS NULL
ORDER BY o.id;

— 33) Wysłane, ale niedostarczone
SELECT
o.id AS order_id,
s.courier,
s.shipped_at,
s.delivered_at
FROM orders o
JOIN shipments s ON s.order_id = o.id
WHERE s.delivered_at IS NULL
ORDER BY o.id;

— 34) Anulowane zamówienia + pozycje
SELECT
o.id AS order_id,
pr.name AS product_name,
oi.qty
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products pr ON pr.id = oi.product_id
WHERE o.status = 'CANCELLED’
ORDER BY o.id, product_name;

— 35) Zamówienia: items_count i suma brutto
SELECT
o.id AS order_id,
COUNT(oi.id) AS items_count,
ROUND(SUM(oi.qty * oi.price_net_at_time * (1 + oi.vat_rate_at_time)), 2) AS sum_gross
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id
ORDER BY o.id;

— 36) Zamówienia opłacone 'card’ + klient
SELECT
o.id AS order_id,
c.email,
p.paid_at,
p.amount
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN payments p ON p.order_id = o.id
WHERE p.method = 'card’
ORDER BY p.paid_at;

— 37) Liczba płatności na metodę
SELECT
p.method,
COUNT(*) AS n
FROM payments p
GROUP BY p.method
ORDER BY n DESC, p.method;

— 38) Suma kwot płatności na metodę
SELECT
p.method,
ROUND(SUM(p.amount), 2) AS total_amount
FROM payments p
GROUP BY p.method
ORDER BY total_amount DESC, p.method;

— 39) Zamówienia, gdzie payment.amount różni się od wyliczonego brutto
SELECT
o.id AS order_id,
ROUND(SUM(oi.qty * oi.price_net_at_time * (1 + oi.vat_rate_at_time)), 2) AS computed_gross,
p.amount AS paid_amount,
ROUND(p.amount – SUM(oi.qty * oi.price_net_at_time * (1 + oi.vat_rate_at_time)), 2) AS diff
FROM orders o
JOIN payments p ON p.order_id = o.id
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id, p.amount
HAVING ABS(p.amount – SUM(oi.qty * oi.price_net_at_time * (1 + oi.vat_rate_at_time))) > 0.01
ORDER BY ABS(diff) DESC;

— 40) Zamówienia + informacja czy użyto kuponu
SELECT
o.id AS order_id,
o.status,
IF(o.coupon_id IS NULL, 'NIE’, 'TAK’) AS coupon_used,
cp.code,
cp.discount_pct
FROM orders o
LEFT JOIN coupons cp ON cp.id = o.coupon_id
ORDER BY o.id;

— 41) Raport dzienny: liczba zamówień
SELECT
DATE(o.created_at) AS d,
COUNT(*) AS orders_count
FROM orders o
GROUP BY DATE(o.created_at)
ORDER BY d;

— 42) Raport dzienny: suma brutto zamówień
SELECT
DATE(o.created_at) AS d,
ROUND(SUM(oi.qty * oi.price_net_at_time * (1 + oi.vat_rate_at_time)), 2) AS gross_total
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY DATE(o.created_at)
ORDER BY d;

— 43) Klient + miesiąc + liczba zamówień
SELECT
c.email,
DATE_FORMAT(o.created_at, '%Y-%m’) AS ym,
COUNT(*) AS orders_count
FROM orders o
JOIN customers c ON c.id = o.customer_id
GROUP BY c.email, DATE_FORMAT(o.created_at, '%Y-%m’)
ORDER BY c.email, ym;

— 44) Kategoria + miesiąc + sprzedaż netto
SELECT
cat.name AS category_name,
DATE_FORMAT(o.created_at, '%Y-%m’) AS ym,
ROUND(SUM(oi.qty * oi.price_net_at_time), 2) AS sales_net
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products pr ON pr.id = oi.product_id
JOIN categories cat ON cat.id = pr.category_id
GROUP BY cat.name, DATE_FORMAT(o.created_at, '%Y-%m’)
ORDER BY category_name, ym;

— 45) Zamówienie + lista produktów
SELECT
o.id AS order_id,
GROUP_CONCAT(pr.name ORDER BY pr.name SEPARATOR ’, ’) AS products_list
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products pr ON pr.id = oi.product_id
GROUP BY o.id
ORDER BY o.id;

— 46) FULL OUTER JOIN (obejście UNION): orders ↔ payments
SELECT
o.id AS order_id,
p.id AS payment_id,
p.amount
FROM orders o
LEFT JOIN payments p ON p.order_id = o.id
UNION
SELECT
o.id AS order_id,
p.id AS payment_id,
p.amount
FROM orders o
RIGHT JOIN payments p ON p.order_id = o.id
ORDER BY order_id;

— 47) Wszystkie kupony + ile razy użyte (także 0)
SELECT
cp.code,
cp.discount_pct,
COUNT(o.id) AS used_count
FROM coupons cp
LEFT JOIN orders o ON o.coupon_id = cp.id
GROUP BY cp.code, cp.discount_pct
ORDER BY used_count DESC, cp.code;

— 48) Kupony nigdy nieużyte
SELECT
cp.code,
cp.discount_pct
FROM coupons cp
LEFT JOIN orders o ON o.coupon_id = cp.id
WHERE o.id IS NULL
ORDER BY cp.code;

— 49) Zamówienia z kuponem: brutto przed i po rabacie
SELECT
o.id AS order_id,
cp.code,
ROUND(SUM(oi.qty * oi.price_net_at_time * (1 + oi.vat_rate_at_time)), 2) AS gross_before,
ROUND(SUM(oi.qty * oi.price_net_at_time * (1 + oi.vat_rate_at_time)) * (1 – cp.discount_pct/100), 2) AS gross_after
FROM orders o
JOIN coupons cp ON cp.id = o.coupon_id
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id, cp.code, cp.discount_pct
ORDER BY o.id;

— 50) Klient: suma „zaoszczędzone” na kuponach
SELECT
c.email,
ROUND(SUM((oi.qty * oi.price_net_at_time * (1 + oi.vat_rate_at_time)) * (cp.discount_pct/100)), 2) AS saved_total
FROM customers c
JOIN orders o ON o.customer_id = c.id
JOIN coupons cp ON cp.id = o.coupon_id
JOIN order_items oi ON oi.order_id = o.id
GROUP BY c.email
ORDER BY saved_total DESC, c.email;