Ćwiczenie SQL / krok po kroku

Ćwiczenie SQL · analiza sprzedaży

SQL — ćwiczenie z rozwiązaniem: analiza sprzedaży w kawiarni

Jak policzyć wartość sprzedaży, gdy tabela zawiera również zwroty i anulowane transakcje? W tym ćwiczeniu przeanalizujesz sprzedaż w kawiarni: przygotujesz dane, napiszesz zapytanie SQL i sprawdzisz, co wynika z obliczeń.

Rozwiążemy zadanie krok po kroku. Zobaczysz, jak wykorzystać WHERE, CASE, SUM i GROUP BY, żeby porównać wartość sprzedaży kawy, deserów i kanapek. Wyjaśnimy, jak uwzględnić zwroty, dlaczego pomijamy anulowania i jak sprawdzić poprawność wyniku. Następnie porównamy sprzedaż na miejscu i na wynos. Oba pytania mają gotowe rozwiązania. W trzecim zadaniu samodzielnie obliczysz, jaką część wartości sprzedaży stanowią zwroty.

Dane, skrypty SQL i instrukcję PDF pobierzesz poniżej. Wybierz wersję dla swojej bazy danych i wykonuj kolejne kroki razem z artykułem. Skrypty możesz też skopiować bezpośrednio ze strony. Pracujemy na 24 fikcyjnych zdarzeniach z sześciu dni. Do wykonania ćwiczenia wystarczy znajomość podstaw SQL.

Materiały do tego artykułu · bezpłatnie

Pobierz dane. Przećwicz analizę.

W paczce znajdziesz plik CSV, instrukcję PDF i skrypty dla czterech baz danych. Skrypt przygotowania tworzy tabelę i wypełnia ją danymi z ćwiczenia, więc nie musisz samodzielnie importować CSV.

01 / Dane CSV02 / Skrypty przygotowania danych03 / Dwa rozwiązania + Twoje zadanie04 / Instrukcja PDF w trzech krokach

Z której bazy danych korzystasz? Wybierz ją poniżej. Przykłady kodu i pliki SQL do pobrania zmienią się na wersję dla wybranej bazy.

01 / Pytania i zasady

Pytania do analizy i zasady obliczeń

Osoba prowadząca kawiarnię chce porównać sprzedaż kawy, deserów i kanapek oraz sprzedaż na miejscu i na wynos. W tabeli zapisano zrealizowaną sprzedaż, zwroty i anulowane transakcje. Na tych danych odpowiemy na trzy pytania:

  1. Która kategoria produktów osiągnęła najwyższą wartość sprzedaży po odjęciu zwrotów? Porównamy kawę, desery i kanapki.
  2. Który kanał osiągnął wyższą wartość sprzedaży po zwrotach? Porównamy sprzedaż na miejscu i na wynos.
  3. W której kategorii zwroty stanowią największą część zrealizowanej sprzedaży? Obliczysz udział procentowy wartości zwrotów.

Pierwsze dwa pytania przejdziemy razem, z kodem i wynikami do sprawdzenia. Trzecie rozwiążesz samodzielnie, korzystając z podpowiedzi. Najpierw przyjrzyjmy się temu, jak liczyć wartość sprzedaży po zwrotach.

Najpierw ustalmy zasady obliczeń. Nie możemy po prostu dodać wszystkich kwot z tabeli, ponieważ sposób uwzględnienia każdej z nich zależy od rodzaju zdarzenia:

Zasada obliczeń

Sprzedaż po zwrotach = suma kwot zrealizowanej sprzedaży − suma kwot zwrotów. Wszystkie kwoty zapisane w tabeli są dodatnie, także te dotyczące zwrotów. Dlatego dopiero w zapytaniu ustalimy, które kwoty dodać, a które odjąć. Obliczamy wartość sprzedaży, nie zysk: nie odejmujemy kosztów produktów ani nie przeliczamy kwot na wartości bez VAT.

02 / Przygotuj dane

Sprawdź, co oznacza każdy wiersz

Każdy wiersz opisuje jedno zdarzenie dotyczące jednej kategorii produktów. Nie jest to cały paragon. Sprzedaż i późniejszy zwrot zapisujemy w osobnych wierszach, a zapis pierwotnej sprzedaży pozostaje w tabeli. Przykład: wiersz o identyfikatorze 1 to sprzedaż kawy za 18 zł, a wiersz 20 to późniejszy zwrot tych 18 zł. W kolumnie original_event_id zwrotu znajduje się wartość 1, która wskazuje pierwotną sprzedaż. Po uwzględnieniu obu zdarzeń otrzymujemy 18 − 18 = 0 zł. Te wiersze opisują różne zdarzenia, więc żaden z nich nie jest duplikatem do usunięcia.

W tym ćwiczeniu status completed oznacza zrealizowaną sprzedaż, refunded osobny zapis zwrotu, a cancelled anulowaną transakcję. W innym systemie te same nazwy mogą mieć inne znaczenie. Zanim zastosujesz takie obliczenia do własnych danych, sprawdź opis statusów i sposób zapisywania zwrotów.

KolumnaZnaczenie
event_idUnikalny identyfikator zdarzenia
event_dateData zdarzenia; dane przykładowe z 1–6 kwietnia 2026
categoryKategoria produktu: kawa, desery lub kanapki
channelKanał sprzedaży: lokal (na miejscu) lub na_wynos (na wynos)
statuscompleted, refunded lub cancelled
amount_plnKwota sprzedaży, zwrotu lub anulowania w złotych, zapisana jako liczba dodatnia
original_event_idIdentyfikator sprzedaży, której dotyczy zwrot; dla pozostałych zdarzeń NULL

Jak uruchomić przykład

  1. Otwórz narzędzie do wykonywania zapytań SQL i połącz się z bazą, w której chcesz zrobić ćwiczenie. Następnie wybierz tę bazę przyciskami na górze artykułu.
  2. Pobierz skrypt przygotowania danych lub rozwiń kod poniżej. Uruchom cały skrypt, a nie tylko instrukcję tworzącą tabelę.
  3. Sprawdź wynik ostatniego zapytania w skrypcie. Liczba wierszy powinna wynosić 24.
  4. Wszystkie kolejne zapytania wykonuj w tym samym połączeniu z bazą. Korzystamy z tabeli tymczasowej, która znika po zakończeniu sesji. Jeśli się rozłączysz, po ponownym połączeniu uruchom skrypt przygotowania danych jeszcze raz.

Plik CSV i skrypt zawierają te same zdarzenia. CSV możesz otworzyć w arkuszu, żeby obejrzeć dane lub ręcznie sprawdzić obliczenia. Do pracy w SQL użyj skryptu przygotowania: tworzy tabelę z odpowiednimi kolumnami i od razu wypełnia ją danymi.

Skrypt do skopiowania: tabela i dane
-- Uruchom cały skrypt, a następnie rozwiązanie w tym samym połączeniu.
-- Zastępuje wyłącznie tabelę tymczasową ćwiczenia w bieżącej sesji.
IF OBJECT_ID('tempdb..#cafe_sales') IS NOT NULL DROP TABLE #cafe_sales;
CREATE TABLE #cafe_sales (
    event_id INTEGER NOT NULL PRIMARY KEY,
    event_date DATE NOT NULL,
    category VARCHAR(20) NOT NULL,
    channel VARCHAR(20) NOT NULL,
    status VARCHAR(20) NOT NULL CHECK (status IN ('completed','refunded','cancelled')),
    amount_pln DECIMAL(10,2) NOT NULL CHECK (amount_pln > 0),
    original_event_id INTEGER NULL
);
INSERT INTO #cafe_sales (event_id,event_date,category,channel,status,amount_pln,original_event_id)
VALUES
(1, '2026-04-01', 'kawa', 'lokal', 'completed', 18.00, NULL),
(2, '2026-04-01', 'kawa', 'na_wynos', 'completed', 16.50, NULL),
(3, '2026-04-01', 'desery', 'lokal', 'completed', 24.00, NULL),
(4, '2026-04-01', 'kanapki', 'na_wynos', 'completed', 28.00, NULL),
(5, '2026-04-02', 'kawa', 'lokal', 'completed', 36.00, NULL),
(6, '2026-04-02', 'desery', 'na_wynos', 'completed', 22.00, NULL),
(7, '2026-04-02', 'kanapki', 'lokal', 'completed', 30.00, NULL),
(8, '2026-04-02', 'kawa', 'na_wynos', 'cancelled', 19.00, NULL),
(9, '2026-04-03', 'kawa', 'na_wynos', 'completed', 21.00, NULL),
(10, '2026-04-03', 'desery', 'lokal', 'completed', 48.00, NULL),
(11, '2026-04-03', 'kanapki', 'na_wynos', 'completed', 56.00, NULL),
(12, '2026-04-03', 'desery', 'na_wynos', 'refunded', 22.00, 6),
(13, '2026-04-04', 'kawa', 'lokal', 'completed', 20.00, NULL),
(14, '2026-04-04', 'kanapki', 'lokal', 'completed', 32.00, NULL),
(15, '2026-04-04', 'desery', 'na_wynos', 'completed', 26.00, NULL),
(16, '2026-04-04', 'kanapki', 'na_wynos', 'refunded', 28.00, 4),
(17, '2026-04-05', 'kawa', 'na_wynos', 'completed', 33.00, NULL),
(18, '2026-04-05', 'desery', 'lokal', 'cancelled', 25.00, NULL),
(19, '2026-04-05', 'kanapki', 'lokal', 'completed', 60.00, NULL),
(20, '2026-04-05', 'kawa', 'lokal', 'refunded', 18.00, 1),
(21, '2026-04-06', 'desery', 'lokal', 'completed', 27.00, NULL),
(22, '2026-04-06', 'kawa', 'na_wynos', 'completed', 17.00, NULL),
(23, '2026-04-06', 'kanapki', 'na_wynos', 'cancelled', 31.00, NULL),
(24, '2026-04-06', 'desery', 'na_wynos', 'completed', 44.00, NULL);
SELECT COUNT(*) AS liczba_zdarzen FROM #cafe_sales;

Sprawdź statusy przed obliczeniami

SELECT status, COUNT(*) AS event_count
FROM #cafe_sales
GROUP BY status
ORDER BY status;

Otrzymasz trzy grupy: 18 transakcji sprzedaży, 3 zwroty i 3 anulowania. Nie możemy więc zsumować wszystkich kwot w ten sam sposób. Dodalibyśmy kwoty anulowanych transakcji, a zwroty zwiększałyby wynik zamiast go zmniejszać. Sprawdź też, czy identyfikatory zdarzeń są unikalne i czy każdy zwrot wskazuje właściwą sprzedaż w kolumnie original_event_id. W tym zbiorze wszystkie kwoty i statusy są uzupełnione, więc nie musimy ustalać dodatkowych zasad dla brakujących wartości.

03 / Pytanie 1 · z rozwiązaniem

Która kategoria ma najwyższą wartość sprzedaży po zwrotach?

Spróbuj najpierw napisać zapytanie samodzielnie. Zgrupuj wiersze według kategorii, dodaj kwoty sprzedaży, odejmij kwoty zwrotów i pomiń anulowania. Następnie porównaj swój kod i wynik z rozwiązaniem poniżej.

Pokaż rozwiązanie i wynik kontrolny
-- Najpierw uruchom 01_setup_sql_server.sql. Zachowaj to samo połączenie.
-- Pytanie 1: która kategoria ma największą sprzedaż po zwrotach?
-- Sprzedaż dodajemy, zwroty odejmujemy, anulowania pomijamy.
-- Wynik nie jest zyskiem ani kwotą bez VAT.
SELECT category,
       SUM(CASE WHEN status = 'refunded' THEN -amount_pln
                ELSE amount_pln END) AS revenue_after_returns_pln
FROM #cafe_sales
WHERE status IN ('completed', 'refunded')
GROUP BY category
ORDER BY revenue_after_returns_pln DESC, category;

Warunek WHERE uwzględnia tylko zrealizowaną sprzedaż i zwroty. Wyrażenie CASE zmienia znak kwot zwrotów na ujemny. Funkcja SUM sumuje kwoty, a GROUP BY grupuje wiersze tak, żeby każda kategoria miała osobny wynik. Jeśli dwie kategorie osiągną tę samą kwotę, drugie kryterium w ORDER BY uporządkuje je według nazwy.

Jak czytać to zapytanie?

  1. Wybierz zdarzenia do analizy. Warunek WHERE status IN (...) pomija anulowane transakcje. Pozostawia tylko wiersze dotyczące zrealizowanej sprzedaży i zwrotów.
  2. Nadaj kwocie właściwy znak. Wyrażenie CASE sprawdza status każdego wiersza. Dla zwrotu przyjmuje kwotę ujemną, a dla sprzedaży zachowuje kwotę dodatnią. W danych źródłowych obie kwoty są dodatnie. Bez sprawdzenia statusu nie wiemy więc, czy daną kwotę dodać, czy odjąć.
  3. Zsumuj kwoty dla każdej kategorii. GROUP BY category dzieli wiersze na trzy grupy: kawa, desery i kanapki. Funkcja SUM oblicza sumę kwot osobno dla każdej grupy.
  4. Uporządkuj wynik. ORDER BY ... DESC wyświetla kategorię z najwyższą kwotą jako pierwszą. Przy równych kwotach o kolejności decyduje nazwa kategorii.

Pobierz rozwiązanie dla wybranej bazy

KategoriaSprzedaż po zwrotach (PLN)
kanapki178,00
desery169,00
kawa143,50

Suma kontrolna: 490,50 PLN dla całego zbioru.

Dlaczego samo SUM nie wystarczy?

Jeśli zsumujesz wszystkie kwoty bez sprawdzenia statusów, zawyżysz wynik. Uwzględnisz kwoty anulowanych transakcji, a zwroty dodasz zamiast je odjąć. Takie zapytanie może wykonać się bez błędu, ale nie odpowie poprawnie na nasze pytanie.

Sprawdź obliczenia w osobnym kroku. Dodaj kwoty ze statusem completed, a następnie odejmij od nich sumę kwot ze statusem refunded. Anulowania pomiń. Otrzymana kwota powinna być równa sumie wyników dla wszystkich kategorii. Zbiór jest mały, więc możesz wykonać tę kontrolę ręcznie lub w arkuszu.

Dokumentacja SQL — przydatne linki

Informacje o wyrażeniu CASE i funkcji SUM w SQL Server znajdziesz w dokumentacji Microsoft: CASE i SUM. Dokumentacja pozostałych baz: PostgreSQL, MySQL i SQLite. Sposób uwzględniania sprzedaży, zwrotów i anulowań wynika z założeń tego ćwiczenia, a nie z reguł języka SQL.

04 / Pytanie 2 · z rozwiązaniem

Na miejscu czy na wynos — gdzie wartość sprzedaży jest wyższa?

Wiemy już, która kategoria osiągnęła najwyższą kwotę. Teraz spójrzmy na te same zdarzenia z innej strony: porównajmy sprzedaż na miejscu i na wynos. Czy wyższy wynik uzyskuje kanał lokal, czy na_wynos?

Twoje zadanie: oblicz wartość sprzedaży po odjęciu zwrotów dla obu kanałów i uporządkuj wyniki malejąco. Zachowaj zasady z pierwszego pytania: dodaj sprzedaż, odejmij zwroty i pomiń anulowania.

Najpierw spróbuj dostosować wcześniejsze zapytanie. Tym razem każdy wiersz wyniku ma opisywać jeden kanał sprzedaży. Dlatego zamiast category użyj channel w SELECT, GROUP BY i dodatkowym kryterium sortowania.

Pokaż rozwiązanie drugiego pytania
-- Pytanie 2: który kanał osiągnął wyższą wartość sprzedaży po zwrotach?
-- Zasady obliczeń pozostają takie same. Grupujemy według channel.
SELECT channel,
       SUM(CASE WHEN status = 'refunded' THEN -amount_pln
                ELSE amount_pln END) AS revenue_after_returns_pln
FROM #cafe_sales
WHERE status IN ('completed', 'refunded')
GROUP BY channel
ORDER BY revenue_after_returns_pln DESC, channel;

Sposób liczenia kwot pozostaje taki sam. Zmienia się to, według czego grupujemy zdarzenia: wszystkie sprzedaże i zwroty na miejscu trafiają do jednej grupy, a zdarzenia na wynos do drugiej. Otrzymujemy dwa wyniki:

Kanał sprzedażySprzedaż po zwrotach (PLN)
lokal — na miejscu277,00
na_wynos — na wynos213,50

Wniosek: w tym zestawie sprzedaż na miejscu osiągnęła wyższą wartość po zwrotach: 277,00 PLN wobec 213,50 PLN na wynos. Różnica wynosi 63,50 PLN. To porównanie wartości sprzedaży, więc nie rozstrzyga, który kanał jest bardziej rentowny.

Kontrola wyniku: 277,00 + 213,50 = 490,50 PLN. To ta sama suma co w pierwszym pytaniu. Grupujemy te same zdarzenia według innej kolumny, dlatego zmienia się podział kwoty, a nie jej łączna wartość.

05 / Wnioski i ograniczenia

Co wynika z obliczeń?

Pytanie 1: kategorie produktów

W tym fikcyjnym zbiorze najwyższą wartość sprzedaży po odjęciu zwrotów osiągnęły kanapki: 178,00 PLN. Kolejne miejsca zajęły desery z wynikiem 169,00 PLN i kawa z wynikiem 143,50 PLN.

Pytanie 2: kanały sprzedaży

Sprzedaż na miejscu osiągnęła 277,00 PLN po zwrotach, a sprzedaż na wynos 213,50 PLN. W analizowanym okresie kanał na miejscu osiągnął więc wartość wyższą o 63,50 PLN.

Suma wyników w obu podziałach wynosi 490,50 PLN. Kategorie i kanały pokazują inne spojrzenie na te same zdarzenia, dlatego ich łączna wartość sprzedaży powinna być taka sama.

Co możemy powiedzieć, a czego jeszcze nie wiemy?

Wyniki dotyczą wyłącznie przykładowych zdarzeń z sześciu dni. Nie pozwalają ocenić sprzedaży w innych kawiarniach ani sytuacji na całym rynku. Najwyższa wartość sprzedaży nie oznacza największego zysku — ani dla kategorii produktów, ani dla kanału sprzedaży.

Nie znamy kosztów produktów, marż, kosztów obsługi obu kanałów ani sprzedaży w dłuższym okresie. Dlatego same wyniki nie uzasadniają rozszerzenia oferty kanapek ani ograniczenia sprzedaży na wynos. Do takich decyzji potrzebujemy dodatkowych danych.

W opisie własnej analizy oddziel to, co wynika z obliczeń, od proponowanych działań. Wyjaśnij, jak liczysz sprzedaż, jak sprawdzasz wynik i jakich informacji brakuje do podjęcia decyzji. Dzięki temu czytelnik może ocenić poprawność obliczeń i zrozumieć ich ograniczenia.

06 / Pytanie 3 · samodzielnie

W której kategorii zwroty stanowią największą część sprzedaży?

Sama suma zwrotów nie wystarczy do porównania kategorii o różnej wartości sprzedaży. Kategoria o wysokiej sprzedaży może mieć więcej zwrotów w złotych, a jednocześnie niższy udział zwrotów niż mniejsza kategoria. Żeby to porównać, potrzebujemy wyniku procentowego.

Twoje zadanie: dla każdej kategorii oblicz wartość zrealizowanej sprzedaży przed odjęciem zwrotów, wartość zwrotów i ich udział w sprzedaży. Pomiń anulowania. Uporządkuj kategorie od najwyższego udziału zwrotów i napisz krótki wniosek.

Jak obliczyć udział zwrotów?

Udział zwrotów (%) = wartość zwrotów / wartość zrealizowanej sprzedaży × 100.

Licznik to suma dodatnich kwot refunded, a mianownik to suma kwot completed, przed odjęciem zwrotów. Porównujemy wartości w złotych, a nie liczbę transakcji.

Wynik powinien zawierać cztery kolumny: category, completed_sales_pln, refunds_pln i refund_share_pct. Zaokrąglij procent do dwóch miejsc po przecinku. Przy równych udziałach posortuj kategorie według nazwy.

Potrzebujesz podpowiedzi?
  1. Użyj dwóch wyrażeń SUM(CASE ...): jednego dla sprzedaży i drugiego dla zwrotów. Gdy status nie pasuje, zwróć 0.
  2. Przy obliczaniu procentu użyj np. 100.0, żeby uniknąć dzielenia całkowitego. NULLIF(..., 0) pozwoli zabezpieczyć mianownik przed zerem.
  3. W SQLite sumuj kwoty w groszach. Sumy sprzedaży i zwrotów przelicz na złote dopiero w wyniku. W ilorazie do procentu możesz użyć groszy zarówno w liczniku, jak i mianowniku.
  4. Sprawdź sumy dla całego zestawu: zrealizowana sprzedaż to 558,50 PLN, a zwroty 68,00 PLN. Ich różnica powinna dać 490,50 PLN.

Na koniec odpowiedz własnymi słowami: która kategoria ma najwyższy udział zwrotów i czego jeszcze potrzebujesz, żeby zrozumieć przyczynę? Pamiętaj, że analizujemy tylko sześć dni, a dane nie zawierają powodów zwrotu.

W pliku rozwiązania znajdziesz oba omówione zapytania oraz podpowiedzi do trzeciego. Gotową odpowiedź na ostatnie pytanie przygotowujesz samodzielnie.