Baza wiedzy / SQL

LIMIT w SQL: kolejność, OFFSET i stabilna paginacja

Na liście publikacji pokazuję, dlaczego LIMIT potrzebuje ustalonej kolejności, co zmienia OFFSET i kiedy warto przejść do paginacji po ostatnim rekordzie.

Kajo RudzińskiOpublikowano Aktualizacja 5 min czytania

Przez prostokątne okno widać wybrany fragment uporządkowanej kolejki klocków.

LIMIT ogranicza liczbę wierszy wyniku zapytania. Zanim go dodam, ustalam jednak kolejność. Gdy chcę najnowsze publikacje, potrzebuję reguły rozstrzygającej również sytuację, w której kilka wpisów ma tę samą datę. Bez tego dwie poprawne realizacje zapytania mogą wybrać różne rekordy na granicy strony.

Przykłady wykonano w SQLite 3.53.4 przez Node 24. Dane są syntetyczne. Porównanie innych dialektów poniżej opiera się na dokumentacji producentów i pomaga wybrać właściwą składnię; nie jest deklaracją wykonania przykładów na wszystkich wymienionych silnikach.

Przygotuj małą listę publikacji

Wykonaj blok w nowej, pustej bazie. Daty zapisuję w jednakowym formacie ISO, więc tekstowe sortowanie odpowiada tutaj kolejności dni. Klucz sortowania nie dopuszcza NULL; przy brakujących datach potrzebna byłaby dodatkowa reguła.

-- example: fixture
CREATE TABLE entries (
    id INTEGER PRIMARY KEY,
    title TEXT NOT NULL,
    published_at TEXT NOT NULL
);
INSERT INTO entries VALUES
    (1, 'Alpha', '2026-10-20'),
    (2, 'Beta', '2026-10-22'),
    (3, 'Gamma', '2026-10-22'),
    (4, 'Delta', '2026-10-23'),
    (5, 'Epsilon', '2026-10-24'),
    (6, 'Zeta', '2026-10-24');

Każdy wiersz jest jedną publikacją. W ramach tej samej daty większe ID ma być wcześniej. To jawna reguła ćwiczenia; w innym systemie możesz potrzebować dokładnego czasu publikacji albo innego sposobu rozstrzygania remisów.

LIMIT z jednoznacznym ORDER BY

Pobieram dwa pierwsze wiersze według ustalonej kolejności:

-- example: first-page
SELECT id, title, published_at
FROM entries
ORDER BY published_at DESC, id DESC
LIMIT 2;
id title published_at
6 Zeta 2026-10-24
5 Epsilon 2026-10-24

Sama data nie wystarcza do rozstrzygnięcia kolejności wpisów z tego samego dnia. Dodatkowe ID tworzy jednoznaczny porządek. Znaczenie takiego sortowania przy LIMIT opisuje także dokumentacja PostgreSQL. Bez ORDER BY nie nazywam pobranych rekordów najnowszymi ani pierwszymi według domyślnego porządku tabeli.

LIMIT jest limitem maksymalnym. Gdy cały wynik ma jeden wiersz, LIMIT 2 zwróci jeden. Nie uzupełnia brakujących pozycji. Nie wybiera też automatycznie reprezentatywnej próby: najnowsze dwie publikacje to konkretny fragment danych, który może różnić się od pozostałych.

OFFSET pomija wiersze wyniku

Druga strona przy rozmiarze dwa wymaga pominięcia dwóch pierwszych pozycji:

-- example: second-page
SELECT id, title, published_at
FROM entries
ORDER BY published_at DESC, id DESC
LIMIT 2 OFFSET 2;
id title published_at
4 Delta 2026-10-23
3 Gamma 2026-10-22

OFFSET liczy wiersze, a nie wartość identyfikatora. Dla stron numerowanych od jedynki wzór to offset = (numer_strony - 1) * rozmiar_strony. Najpierw odejmujesz jeden od numeru strony, potem mnożysz przez jej rozmiar. Trzecia strona ma więc OFFSET 4 i zawiera ID 2 oraz 1. Pierwsza używa OFFSET 0 albo pomija tę część zapytania.

Jeśli wcześniej dodasz WHERE, paginacja dotyczy wyniku po filtrze. Nie jest to wybieranie dwóch rekordów, a następnie odrzucanie tych, które nie pasują. Przykładowo filtr na datę 22 października lub późniejszą zostawia pięć rekordów; trzecia strona zawiera wtedy tylko ID 2.

Dlaczego strony zmieniają się po dopisaniu danych

Załóżmy, że odczytałeś pierwszą stronę, czyli ID 6 i 5. Potem pojawia się publikacja ID 7 z datą 25 października. Nowa kolejność zaczyna się od 7, 6, 5, 4. Ponowne pobranie drugiej strony przez OFFSET 2 da 5 i 4. Rekord 5 zobaczysz drugi raz, choć ORDER BY nadal jest poprawny.

Problemem jest zmiana danych między odczytami. Jednoznaczne sortowanie rozwiązuje remisy w danym wyniku, ale nie zamraża całej listy na czas przeglądania. Jeśli potrzebujesz odtwarzalnego eksportu, ustal zasady spójności odczytu lub stabilny wycinek danych. Sam LIMIT tego nie zapewnia.

Stałe okno przesuwa wybór po dodaniu klocka, a znacznik przy konkretnym elemencie zachowuje punkt kontynuacji.

Paginacja po ostatnim kluczu

Dla przewijania listy mogę zapamiętać datę i ID ostatniego wiersza poprzedniej strony. Po ID 5 z datą 24 października szukam rekordów z wcześniejszą datą albo mniejszym ID przy tej samej dacie. To przykład paginacji keyset, nazywanej też paginacją po kursorze.

-- example: keyset
SELECT id, title, published_at
FROM entries
WHERE published_at < '2026-10-24'
   OR (published_at = '2026-10-24' AND id < 5)
ORDER BY published_at DESC, id DESC
LIMIT 2;
id title published_at
4 Delta 2026-10-23
3 Gamma 2026-10-22

Na początkowych danych wynik jest taki sam jak drugiej strony OFFSET. Po opisanym dopisaniu nowego, późniejszego wpisu nadal otrzymam 4 i 3, ponieważ nowy rekord znajduje się przed zapamiętanym punktem. Kursor musi zawierać oba elementy kolejności. Użycie samego id < 5 byłoby inną regułą, jeśli ID i data nie rosłyby razem.

Ta metoda także ma ograniczenia. Zmiana daty istniejącego wpisu może przenieść go na inną stronę, a usunięcie zmieni dostępny zbiór. Keyset utrudnia również bezpośredni skok do dowolnego numeru strony bez znajomości punktu startowego. Dobieram go do sposobu używania listy, zamiast traktować jako automatyczny zamiennik każdego OFFSET.

Przypadki graniczne i koszt zapytania

W SQLite LIMIT 0 zwraca pusty wynik:

-- example: zero-limit
SELECT id FROM entries ORDER BY id LIMIT 0;

Pusty wynik dostanę również po pominięciu większej liczby wierszy, niż istnieje:

-- example: past-end
SELECT id FROM entries ORDER BY id LIMIT 2 OFFSET 100;

Używaj nieujemnych liczb całkowitych w granicach przyjętych dla aplikacji. Zachowanie wartości ujemnych i NULL różni się między silnikami; nie przenoś go bez sprawdzenia. W SQLite ujemny limit oznacza brak górnego ograniczenia, co opisuje dokumentacja LIMIT. Nie używam tej własności jako przenośnej składni.

Duży OFFSET może wymagać przetworzenia wielu pomijanych rekordów. LIMIT także nie gwarantuje taniego zapytania, gdy wcześniej trzeba wykonać kosztowne łączenie lub sortowanie. Sprawdziłbym plan i indeks zgodny z filtrem oraz porządkiem. Przykład nie zawiera pomiaru wydajności, więc nie przypisuję mu konkretnego przyspieszenia.

Różnice między dialektami

LIMIT występuje w SQLite, PostgreSQL i MySQL. W przykładach używam czytelnej postaci LIMIT z osobnym OFFSET. Nie mieszam jej z wariantem przecinkowym, w którym pozycje liczb mogą łatwo wprowadzić w błąd.

W SQL Server do paginacji służy ORDER BY z OFFSET, po którym następuje opcjonalne FETCH. Osobny TOP ogranicza początek wyniku, ale nie jest słowem LIMIT. W Oracle 19c klauzula ograniczania wierszy również umieszcza OFFSET przed FETCH. Odwrócenie tej kolejności w dawnym tekście było błędem. Przy przenoszeniu zapytania sprawdź składnię konkretnej wersji serwera.

Ćwiczenie: sprawdź granicę strony

Zmień rozmiar strony na trzy. Pierwsza strona powinna zawierać 6, 5, 4, a druga 3, 2, 1. Następnie dopisz opisany rekord ID 7 i porównaj drugą stronę OFFSET z kontynuacją po kluczu. Wyjaśnij, skąd bierze się różnica, zamiast zmieniać oczekiwany wynik tylko dlatego, że baza zwróciła inne wiersze.

Jeśli powtórzenia pochodzą z samych danych, a nie przesunięcia stron, zobacz SELECT DISTINCT. Najpierw ustal, co oznacza wiersz wyniku, potem jego kolejność, a dopiero na końcu fragment, który chcesz pobrać.

Najważniejsze w pigułce

Czy LIMIT zwraca pierwsze rekordy tabeli?

Zwraca co najwyżej wskazaną liczbę wierszy wyniku. Bez ORDER BY nie masz określonej kolejności. Przy remisach dodaj unikalny klucz, żeby wybór był jednoznaczny.

Czym różnią się LIMIT i OFFSET?

OFFSET określa liczbę pomijanych wierszy uporządkowanego wyniku, a LIMIT maksymalną liczbę zwracanych po tym pominięciu. LIMIT 2 OFFSET 2 oznacza trzeci i czwarty wiersz.

Czy paginacja po kluczu gwarantuje niezmienny wynik?

Nie. Pomaga uniknąć przesunięć wynikających z nowych rekordów przed kursorem, ale zmiana klucza sortowania lub usunięcie danych nadal wpływa na wynik. Pełna spójność wymaga osobnych reguł odczytu.

Przy pracy nad artykułem użyto w zautomatyzowany sposób sztucznej inteligencji. Jeśli też chcesz tak umieć, sprawdź ten projekt.

Kajo Rudziński

Newsletter KajoData

Zostańmy w kontakcie.

Piszę o analizie danych, uczeniu się i zmianie pracy. Dzielę się tym, co pomaga mi ogarniać technologię i zawodowe decyzje. Jeśli lubisz ten sposób myślenia, zapraszam do mojego newslettera.