KemSoft · SQL · Analiza sprzedaży
TOP 3 produkty w każdym sklepie — ROW_NUMBER, RANK i DENSE_RANK w SQL
Chcesz znaleźć trzy najlepiej sprzedające się produkty osobno dla każdego sklepu? Zobacz, jak zbudować ranking w SQL, poprawnie zsumować sprzedaż i świadomie rozstrzygnąć remisy. Gotowy przykład pokaże, dlaczego TOP 3 może oznaczać trzy, cztery, a nawet pięć produktów.
Przykłady są przygotowane dla Microsoft SQL Server / T-SQL. Dane sprzedażowe są fikcyjne i służą nauce. Najpierw uruchomisz samodzielny przykład, a następnie dopasujesz zapytania do własnej tabeli.
1. Co właściwie znaczy TOP 3?
Polecenie „pokaż TOP 3” wydaje się jednoznaczne do chwili, gdy dwa produkty osiągną tę samą sprzedaż. Czy trzeba wybrać dokładnie trzy pozycje? Czy wszystkie produkty remisujące na granicy powinny zostać? A może interesują Cię trzy najwyższe różne wartości sprzedaży? To trzy różne reguły raportowania.
| Potrzeba | Funkcja | Znaczenie filtru ≤ 3 |
|---|---|---|
| Maksymalnie trzy wiersze na sklep | ROW_NUMBER() | Wybiera trzy wiersze, jeśli sklep ma co najmniej trzy produkty. |
| Pozycje w rankingu sportowym, z remisami | RANK() | Zachowuje remisy dla pozycji 1–3; po remisie mogą wystąpić luki. |
| Trzy najwyższe różne wartości sprzedaży | DENSE_RANK() | Zachowuje wszystkie produkty z trzech najwyższych poziomów wartości. |
Dla przykładu: lista 3000, 3000, 2000, 2000, 1000 zł ma pięć produktów, lecz trzy różne wartości sprzedaży. Zanim zaczniesz pisać zapytanie, uzgodnij z odbiorcą raportu, czy ma dostać trzy produkty, pozycje rankingu do trzeciego miejsca czy trzy poziomy sprzedaży.
Założenie biznesowe: ranking w tym artykule opiera się na wartości sprzedaży netto w wybranym okresie. TOP według liczby sztuk albo marży odpowiada na inne pytanie. Nie zmieniaj miary bez zmiany opisu raportu.
2. Przygotuj dane i zsumuj sprzedaż
W systemie sprzedażowym jeden produkt zwykle pojawia się w wielu transakcjach. Funkcja rankingowa nie sumuje ich automatycznie. Najpierw utwórz jeden wiersz dla każdej pary sklep–produkt, a dopiero potem nadaj miejsce w rankingu.
| Kolumna | Znaczenie |
|---|---|
| DataSprzedazy | Data lub data z godziną sprzedaży. |
| SklepID | Trwały identyfikator sklepu. |
| ProduktID | Trwały identyfikator produktu. |
| Produkt | Nazwa produktu do prezentacji. |
| SprzedazNetto | Wartość netto całego wiersza sprzedaży, np. decimal(18,2). |
| Ilosc | Liczba sztuk w wierszu, potrzebna przy rankingu ilościowym. |
Jeżeli SprzedazNetto zawiera wartość całego wiersza, nie mnożysz jej ponownie przez ilość. Przy kolumnie z ceną jednostkową wartość trzeba dopiero wyliczyć. Sprawdź też zasady ujęcia zwrotów: w tym modelu mogą pomniejszać wartość jako kwoty ujemne.
Nazwy produktów mogą się zmieniać. Dlatego agregacja i rozstrzyganie remisów korzystają z ID. W przykładzie przyjmujemy jedną nazwę na ProduktID; MAX(Produkt) pobiera etykietę. W rzeczywistej bazie lepiej dołączyć aktualną nazwę z tabeli produktów po agregacji, przez unikalny ProduktID.
Okres zapisujemy jako przedział od początku pierwszego dnia do początku dnia następującego po końcu okresu. Wrzesień to DataSprzedazy >= 2026-09-01 oraz DataSprzedazy < 2026-10-01. Taka granica obejmie także sprzedaż z godziną 23:59 ostatniego dnia.
3. Pełny przykład do uruchomienia
Poniższe zapytanie zawiera dane oraz wszystkie trzy rankingi. Wklej cały blok do okna zapytania SQL Server, np. w SQL Server Management Studio. Nie wymaga tabeli sprzedaży i nie zapisuje danych do bazy. CTE to nazwane etapy jednego zapytania.
;WITH Dane AS (
SELECT
CONVERT(date, v.DataTekst, 112) AS DataSprzedazy,
v.SklepID, v.ProduktID, v.Produkt,
CAST(v.Kwota AS decimal(18,2)) AS SprzedazNetto
FROM (VALUES
('20260901', 1, 101, N'Chleb na zakwasie', 1000.00),
('20260902', 1, 101, N'Chleb na zakwasie', 2000.00),
('20260901', 1, 102, N'Rogal maślany', 3000.00),
('20260901', 1, 103, N'Drożdżówka z serem', 2000.00),
('20260901', 1, 104, N'Bułka grahamka', 2000.00),
('20260901', 1, 105, N'Bagietka', 1000.00),
('20260901', 2, 101, N'Chleb na zakwasie', 4200.00),
('20260901', 2, 102, N'Rogal maślany', 2800.00),
('20260901', 2, 103, N'Drożdżówka z serem', 1800.00),
('20260901', 2, 104, N'Bułka grahamka', 900.00),
('20260901', 2, 105, N'Bagietka', 500.00),
('20261001', 1, 105, N'Bagietka', 9000.00)
) AS v(DataTekst, SklepID, ProduktID, Produkt, Kwota)
), Sumy AS (
SELECT SklepID, ProduktID,
MAX(Produkt) AS Produkt,
SUM(SprzedazNetto) AS Wartosc
FROM Dane
WHERE DataSprzedazy >= CONVERT(date, '20260901', 112)
AND DataSprzedazy < CONVERT(date, '20261001', 112)
GROUP BY SklepID, ProduktID
), Ranking AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY SklepID
ORDER BY Wartosc DESC, ProduktID ASC
) AS NumerWiersza,
RANK() OVER (
PARTITION BY SklepID ORDER BY Wartosc DESC
) AS Miejsce,
DENSE_RANK() OVER (
PARTITION BY SklepID ORDER BY Wartosc DESC
) AS PoziomWartosci
FROM Sumy
)
SELECT SklepID, ProduktID, Produkt, Wartosc,
NumerWiersza, Miejsce, PoziomWartosci
FROM Ranking
ORDER BY SklepID, Wartosc DESC, ProduktID ASC;Sklep 1 ma celowo dwa wiersze chleba: 1000 i 2000 zł. Po agregacji powinien mieć jeden wiersz o wartości 3000 zł. Dodaliśmy także 9000 zł sprzedaży bagietki z 1 października: ta kwota nie może wejść do wrześniowego rankingu. To dwa proste punkty kontroli poprawności.
W ROW_NUMBER ProduktID rozstrzyga remisy. W RANK i DENSE_RANK pozostaje wyłącznie wartość sprzedaży, ponieważ równe wartości mają dostać wspólną pozycję. Końcowe ORDER BY porządkuje prezentację tabeli.
4. Ten sam sklep, trzy różne rankingi
| ProduktID | Produkt | Sprzedaż netto | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|---|---|
| 101 | Chleb na zakwasie | 3000,00 | 1 | 1 | 1 |
| 102 | Rogal maślany | 3000,00 | 2 | 1 | 1 |
| 103 | Drożdżówka z serem | 2000,00 | 3 | 3 | 2 |
| 104 | Bułka grahamka | 2000,00 | 4 | 3 | 2 |
| 105 | Bagietka | 1000,00 | 5 | 5 | 3 |
Dwa produkty mają po 3000 zł. ROW_NUMBER przyznaje im różne numery: 1 i 2. RANK daje obu miejsce 1, a następnej parze miejsce 3. DENSE_RANK daje pierwszej parze poziom 1, kolejnej poziom 2 i bagietce poziom 3.
| Filtr | ProduktID w sklepie 1 | Liczba produktów |
|---|---|---|
| NumerWiersza ≤ 3 | 101, 102, 103 | 3 |
| Miejsce ≤ 3 | 101, 102, 103, 104 | 4 |
| PoziomWartosci ≤ 3 | 101, 102, 103, 104, 105 | 5 |
W sklepie 2 wartości wynoszą 4200, 2800, 1800, 900 i 500 zł. Nie ma remisów, więc wszystkie trzy metody wybiorą ID 101, 102 i 103. Różnice między funkcjami najlepiej sprawdzać na danych z celowo przygotowanymi remisami.
5. ROW_NUMBER: maksymalnie trzy produkty
Jeżeli raport ma trzy miejsca na produkty w każdym sklepie, użyj ROW_NUMBER. Ustal też regułę remisu, np. mniejszy ProduktID wygrywa przy identycznej wartości. Ten identyfikator jest technicznym rozstrzygnięciem, nie dowodem większej wartości biznesowej produktu.
Poniższy blok jest zapytaniem do dbo.Sprzedaz z kolumnami opisanymi w sekcji 2. Dopasuj nazwy tabeli i pól. Zapytania produkcyjne nie korzystają z CTE „Dane” z poprzedniego przykładu.
DECLARE @Od date = '20260901';
DECLARE @Do date = '20261001'; -- granica wyłączna
;WITH Sumy AS (
SELECT SklepID, ProduktID,
MAX(Produkt) AS Produkt,
SUM(SprzedazNetto) AS Wartosc
FROM dbo.Sprzedaz
WHERE DataSprzedazy >= @Od AND DataSprzedazy < @Do
GROUP BY SklepID, ProduktID
), Ranking AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY SklepID
ORDER BY Wartosc DESC, ProduktID ASC
) AS Pozycja
FROM Sumy
)
SELECT SklepID, ProduktID, Produkt, Wartosc, Pozycja
FROM Ranking
WHERE Pozycja <= 3
ORDER BY SklepID, Pozycja;Sklep z pięcioma produktami zwróci trzy wiersze, a sklep z dwoma — dwa. Sklep bez sprzedaży nie pojawi się w agregacji. Jeśli raport ma pokazywać także puste sklepy, potrzebujesz osobnej listy sklepów i właściwego połączenia z wynikiem.
Kolejność będzie jednoznacznie rozstrzygnięta dla tych samych danych, jeśli para SklepID–ProduktID jest unikalna po agregacji. Samo sortowanie po wartości nie wystarcza, gdy kilka produktów ma tę samą sprzedaż.
6. RANK i DENSE_RANK: jak zachować remisy
W samodzielnym przykładzie z sekcji 3 możesz zastosować każdy wariant bez przepisywania etapów. Zamień końcowy SELECT na jeden z poniższych bloków. Uruchom go razem z całym poprzedzającym CTE; nazwa „Ranking” nie istnieje poza tym jednym zapytaniem.
RANK: zachowaj produkty zajmujące miejsca 1–3
SELECT SklepID, ProduktID, Produkt, Wartosc, Miejsce
FROM Ranking
WHERE Miejsce <= 3
ORDER BY SklepID, Miejsce, ProduktID;W sklepie 1 pozostaną cztery produkty. Remis dwóch liderów sprawia, że następna pozycja ma numer 3. Bagietka jest piąta. To reguła rankingu z lukami, a nie wybór trzech różnych wartości.
DENSE_RANK: zachowaj trzy najwyższe poziomy sprzedaży
SELECT SklepID, ProduktID, Produkt, Wartosc, PoziomWartosci
FROM Ranking
WHERE PoziomWartosci <= 3
ORDER BY SklepID, PoziomWartosci, ProduktID;W sklepie 1 pozostanie wszystkich pięć produktów, ponieważ wartości 3000, 2000 i 1000 zł tworzą trzy poziomy. Przy dużej liczbie remisów taki wynik może mieć znacznie więcej niż trzy wiersze. To prawidłowe, jeśli właśnie tak brzmi reguła raportu.
Uwaga na dodatkowe sortowanie: zapis RANK() OVER (PARTITION BY SklepID ORDER BY Wartosc DESC, ProduktID) rozróżnia produkty z równą sprzedażą. W tym modelu unikalny ProduktID usuwa remisy. Użyj go w końcowym ORDER BY do prezentacji, a przy ROW_NUMBER do świadomego rozstrzygnięcia.
7. Dlaczego filtr rankingu potrzebuje kolejnego etapu?
W SQL Server nie można w tym samym SELECT filtrować w WHERE po aliasie funkcji rankingowej utworzonym w liście SELECT. Ranking trzeba najpierw policzyć w CTE lub podzapytaniu, a dopiero w zapytaniu zewnętrznym zastosować filtr. To dlatego nasze rozwiązania mają etap „Ranking”.
-- Błędny wzorzec dla SQL Server:
SELECT ProduktID,
ROW_NUMBER() OVER (ORDER BY SprzedazNetto DESC) AS Pozycja
FROM dbo.Sprzedaz
WHERE Pozycja <= 3;Oprócz błędu aliasu to zapytanie ma problem biznesowy: szereguje pojedyncze wiersze sprzedaży, a nie miesięczne sumy produktów, i nie rozdziela sklepów. Poprawka powinna objąć cały model, nie tylko przeniesienie WHERE.
Prawidłowa kolejność w naszym raporcie: wybierz okres, zsumuj dane według sklepu i produktu, wylicz ranking, zastosuj limit, uporządkuj wynik. Filtr okresu musi wejść przed sumowaniem. Filtr miejsca musi wejść po obliczeniu miejsca.
8. TOP 3 osobno w każdym miesiącu
Gdy porównujesz kilka miesięcy, nie wystarczy dodać daty do wyniku. Miesiąc musi być zarówno kluczem agregacji, jak i częścią PARTITION BY. W przeciwnym razie produkty z różnych miesięcy będą konkurować w jednym rankingu.
DECLARE @Od date = '20260701';
DECLARE @Do date = '20261001';
;WITH Baza AS (
SELECT SklepID, ProduktID, Produkt, SprzedazNetto,
DATEFROMPARTS(YEAR(DataSprzedazy),
MONTH(DataSprzedazy), 1) AS Miesiac
FROM dbo.Sprzedaz
WHERE DataSprzedazy >= @Od AND DataSprzedazy < @Do
), Sumy AS (
SELECT Miesiac, SklepID, ProduktID,
MAX(Produkt) AS Produkt,
SUM(SprzedazNetto) AS Wartosc
FROM Baza
GROUP BY Miesiac, SklepID, ProduktID
), Ranking AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY Miesiac, SklepID
ORDER BY Wartosc DESC, ProduktID ASC
) AS Pozycja
FROM Sumy
)
SELECT Miesiac, SklepID, ProduktID, Produkt, Wartosc, Pozycja
FROM Ranking
WHERE Pozycja <= 3
ORDER BY Miesiac, SklepID, Pozycja;Miesiac jest tu datą pierwszego dnia miesiąca, np. 2026-09-01. Zawiera rok, więc wrzesień 2025 i wrzesień 2026 nie trafią do tej samej grupy. Ograniczenie danych nadal korzysta z oryginalnej daty sprzedaży.
Jeżeli potrzebujesz TOP 3 osobno w kategorii, dodaj identyfikator kategorii do grupowania i partycji. Poziom agregacji i granice rankingu muszą opisywać ten sam problem. Przy dołączaniu kategorii sprawdź, czy jeden produkt nie jest mnożony przez wiele pasujących wierszy.
9. Błędy, które zmieniają ranking
| Błąd | Skutek | Poprawka |
|---|---|---|
| SELECT TOP (3) dla całej tabeli | Trzy pozycje łącznie, a nie osobno dla każdego sklepu. | Ranking z PARTITION BY SklepID i zewnętrzny filtr. |
| Ranking przed SUM | Porównywanie transakcji zamiast wyników produktów. | Najpierw agregacja sklep–produkt. |
| Brak ProduktID przy ROW_NUMBER | Niejednoznaczny wybór na granicy remisu. | Unikalny klucz jako dodatkowe sortowanie. |
| ProduktID przy RANK / DENSE_RANK | Równe wartości sprzedaży przestają remisować. | W oknie rankingu zostaw tylko kryterium biznesowe. |
| Grupowanie po samej nazwie | Łączenie różnych ID lub rozbijanie jednego ID po zmianie nazwy. | Trwałe ID i kontrolowana etykieta. |
| Miesiąc bez roku | Łączenie tych samych miesięcy z różnych lat. | Data pierwszego dnia miesiąca lub para rok–miesiąc. |
| Duplikaty po JOIN | Zawyżone sumy i błędna kolejność produktów. | Zweryfikuj unikalność klucza w tabeli dołączanej. |
Wartości NULL wymagają sprawdzenia źródła. SUM pomija NULL; jeśli cały zakres dla produktu zawiera NULL, nie otrzymasz rzeczywistej kwoty sprzedaży. Nie zastępuj automatycznie braków zerami tylko po to, aby ranking wyglądał poprawnie. Ustal, czy brak oznacza brak sprzedaży, brak importu czy błąd danych.
Przy pieniądzach stosuj właściwy typ dziesiętny. Jeśli źródło używa przybliżonego typu float, wartości wyglądające tak samo po formatowaniu mogą różnić się w bazie. Ustal dokładność miary przed rankingiem, a nie tylko wygląd liczby w raporcie.
10. Kontrola wyniku i interpretacja biznesowa
Najpierw sprawdź wynik dla jednego sklepu ręcznie. W danych przykładowych wrześniowa suma sklepu 1 wynosi 11 000 zł, a sklepu 2 — 10 200 zł. Suma obu sklepów to 21 200 zł. Październikowe 9000 zł pozostaje poza tym zakresem.
- Porównaj sumy przed rankingiem ze źródłem. TOP 3 nie powinno zastępować sumy całej sprzedaży.
- Sprawdź, czy po agregacji istnieje dokładnie jeden wiersz na sklep i ProduktID.
- Przy ROW_NUMBER potwierdź maksymalnie trzy wiersze na sklep oraz regułę remisu.
- Przy RANK i DENSE_RANK sprawdź, czy większa liczba wierszy wynika z remisów.
- Zweryfikuj granice okresu na danych z ostatniego dnia i pierwszego dnia kolejnego miesiąca.
- Oceń kompletność importu: brak kilku dni może zmienić lidera sklepu.
Dla ROW_NUMBER sklep 1 ma wybrane 8000 zł: 3000 + 3000 + 2000. Stanowi to około 72,73% jego 11 000 zł. RANK wybiera 10 000 zł, czyli około 90,91%, a DENSE_RANK 11 000 zł, czyli 100%. Ten sam napis „udział TOP 3” opisuje różne wyniki, jeśli nie podasz reguły remisów.
Wysoka sprzedaż nie dowodzi wysokiej rentowności. Ranking wartościowy może premiować drogie produkty, a ilościowy tanie. Na decyzję o produkcji wpływają także marża, zwroty, dostępność i sezonowość. Traktuj ranking jako konkretny wskaźnik, nie pełną rekomendację asortymentową.
Przy dużym zbiorze mierz czas wykonania i sprawdzaj plan zapytania. Indeksy i wcześniejsza agregacja mogą pomóc, ale dobór zależy od modelu danych, filtrów i obciążenia. Przetestuj rozwiązanie na reprezentatywnym okresie; nie obiecuj przyspieszenia bez pomiaru.
11. Ćwiczenie z odpowiedzią
Jeden sklep ma wartości: 5000, 4000, 4000, 3000, 3000, 1000 zł. Produkty mają kolejno ID 201–206. Jakie numery przyznają funkcje i ile produktów pozostanie po filtrze ≤ 3? Przy ROW_NUMBER rozstrzygaj remisy rosnącym ProduktID.
Pokaż odpowiedź i wyjaśnienie
| ProduktID | Wartość | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|---|
| 201 | 5000 | 1 | 1 | 1 |
| 202 | 4000 | 2 | 2 | 2 |
| 203 | 4000 | 3 | 2 | 2 |
| 204 | 3000 | 4 | 4 | 3 |
| 205 | 3000 | 5 | 4 | 3 |
| 206 | 1000 | 6 | 6 | 4 |
ROW_NUMBER wybierze trzy produkty, RANK również trzy, a DENSE_RANK pięć. Tutaj reguły ROW_NUMBER i RANK dają tę samą liczbę wierszy, choć działają inaczej. Zgodność jednego wyniku nie oznacza zamienności funkcji. Powtórz ćwiczenie po zmianie wartości produktu 204 z 3000 na 4000: RANK wybierze już cztery produkty.
Podsumowanie
Poprawne TOP 3 zaczyna się od jasnej definicji. Dla listy maksymalnie trzech produktów wybierz ROW_NUMBER i jednoznaczne rozstrzygnięcie remisu. Dla miejsc w rankingu z lukami użyj RANK. Dla trzech najwyższych różnych poziomów sprzedaży użyj DENSE_RANK.
W każdym wariancie najpierw ogranicz okres i zsumuj sprzedaż na właściwym poziomie. Dopiero później licz miejsce osobno w każdej partycji i filtruj wynik. W raporcie podaj miarę, okres oraz regułę remisów, żeby odbiorca wiedział, co dokładnie porównuje.
Źródła i dokumentacja
Przeczytaj również
Ładowanie artykułów…