Menu

SQLite LIMIT и OFFSET: пагинация и срезы выборки

Разбираемся, как работают LIMIT и OFFSET в SQLite: ограничение строк, пропуск, безопасная пагинация и подводные камни производительности на больших таблицах.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

LIMIT ограничивает количество возвращаемых строк

LIMIT — это самый простой «регулятор» в SQL: он говорит SQLite «верни мне не больше стольких-то строк». Допишите его в конце SELECT — и получите не больше указанного числа результатов. Может быть и меньше, если в таблице просто не наберётся столько строк.

Возвращаются первые три строки. Только вот какие именно? В этом и подвох — без ORDER BY SQLite сам решает, в каком порядке отдавать данные. Сегодня это может быть порядок вставки, а завтра, после обновления или изменения индекса, — уже что-то другое. LIMIT сам по себе вполне годится, чтобы «глянуть пару строк для примера», но как только порядок начинает иметь значение — его нужно задать явно.

OFFSET: пропускаем строки с начала

Связка LIMIT и OFFSET позволяет вытащить срез откуда-то из середины результата. OFFSET k отбрасывает первые k строк, а LIMIT n затем возвращает не более n строк из того, что осталось.

То есть «пропусти две строки и верни следующие две» — это будут 3-я и 4-я строки в отсортированном результате. Запомните логику: WHERE фильтрует, ORDER BY сортирует, OFFSET пропускает, LIMIT ограничивает. Порядок именно такой, и каждый шаг важен.

Пагинация в SQLite немыслима без ORDER BY

Самый частый сценарий для LIMIT и OFFSET — это пагинация, когда длинный список нужно разбить на страницы, скажем, по 20 строк. Первая страница — LIMIT 20 OFFSET 0, вторая — LIMIT 20 OFFSET 20, и так далее.

На что здесь стоит обратить внимание. Во-первых, ORDER BY — это не опция, а необходимость: без него «вторая страница» вообще не имеет смысла, и строки могут перетасовываться между запросами. Во-вторых, в ключе сортировки рядом с основным полем стоит id как тай-брейкер. Если у двух постов совпадает created_at, нужна уникальная колонка, чтобы порядок был детерминированным, иначе строки могут меняться местами и одна и та же запись «перетечёт» на соседнюю страницу.

Запомните простое правило: сортируйте либо по уникальному полю, либо по основному столбцу плюс уникальный тай-брейкер.

Сокращённая форма: LIMIT n, m

В SQLite сохранён старый синтаксис через запятую — для совместимости с MySQL: LIMIT offset, count. По смыслу это то же самое, что и LIMIT count OFFSET offset, только порядок аргументов обратный, и его легко перепутать.

-- Эти два запроса эквивалентны:
SELECT * FROM books LIMIT 10 OFFSET 20;
SELECT * FROM books LIMIT 20, 10;     -- сначала смещение, потом количество

Второй вариант короче, но многих сбивает с толку: люди ждут, что первое число — это количество строк. Лучше держаться LIMIT n OFFSET k — запись явная и читается слева направо.

OFFSET без LIMIT: трюк с LIMIT -1

Сам по себе OFFSET использовать нельзя — грамматика SQLite требует, чтобы он шёл после LIMIT. Как же тогда сказать «пропусти первые 10 строк и отдай всё остальное»? Принятое решение — LIMIT -1, которое SQLite понимает как «без верхнего предела».

Любой отрицательный LIMIT делает то же самое, просто -1 — это устоявшийся идиом. Чаще всего такое встречается в скриптах, которые проходят по результату постранично и на последней итерации хотят сказать «отдай мне всё, что осталось».

Подводный камень производительности OFFSET

Вот о чём обычно молчат, пока сами на это не наткнёшься: OFFSET не заставляет SQLite пропускать работу — он заставляет SQLite пропускать вывод. Чтобы вернуть строки с 10 001-й по 10 020-ю, движок всё равно внутри себя пробегает первые десять тысяч строк, прежде чем начать отдавать результат. Маленькие смещения практически бесплатны, а вот OFFSET в десятки и сотни тысяч заметно проседает по скорости.

Для глубокой пагинации стандартное решение — keyset-пагинация: вместо «пропусти N строк» мы запоминаем ключ сортировки последней показанной строки и просим «дай строки после вот этой».

Каждая страница делает индексированный поиск вместо того, чтобы пролистывать всё, что было до неё. Цена вопроса: нельзя прыгнуть сразу на «страницу 47» — только идти вперёд по данным. Для бесконечной ленты и курсоров в API это как раз то, что нужно.

Пагинация через OFFSET отлично подходит для админок и небольших выборок. Но если данные растут бесконечно — берите keyset-пагинацию.

Разбираем на примере

Соберём всё вместе — запрос с пагинацией, фильтрацией, сортировкой и детерминированным тай-брейкером:

Отфильтровываем офисные товары, сортируем по цене по возрастанию, а в качестве тай-брейкера используем имя, и берём первые две строки. Чтобы получить вторую страницу, меняем OFFSET 0 на OFFSET 2. Запрос короткий, но каждое выражение в нём работает на результат.

Дальше: DISTINCT

LIMIT определяет, сколько строк вернётся, а DISTINCT — вернутся ли дубликаты вообще. Это следующее выражение в арсенале SELECT, и его обманчиво легко применить не туда, куда нужно. Об этом — на следующей странице.

Часто задаваемые вопросы

Что делает LIMIT в SQLite?

LIMIT n ограничивает количество строк, которые вернёт SELECT, числом n. Важный момент: он применяется уже после WHERE, GROUP BY и ORDER BY — то есть режется итоговая выборка, а не то, что сканирует движок. Например, SELECT * FROM users LIMIT 10 отдаст не больше десяти строк.

Как OFFSET работает вместе с LIMIT?

OFFSET k пропускает первые k строк результата, и только потом начинает считать LIMIT. То есть LIMIT 10 OFFSET 20 вернёт строки с 21-й по 30-ю. Под капотом SQLite всё равно проходит по всем пропущенным строкам — поэтому большие значения OFFSET и работают медленно.

Можно ли использовать OFFSET без LIMIT?

Напрямую — нет: OFFSET существует только как часть конструкции LIMIT. Обходной путь — LIMIT -1 OFFSET k, где -1 означает «без верхней границы». SQLite пропустит k строк и вернёт всё остальное. Полезный костыль, который стоит запомнить.

Зачем для пагинации обязательно нужен ORDER BY?

Без ORDER BY SQLite вправе отдавать строки в любом порядке, и этот порядок может меняться от запроса к запросу. В итоге пагинация ломается: одна и та же запись может появиться и на первой, и на третьей странице — или вовсе исчезнуть. Поэтому LIMIT/OFFSET всегда нужно сочетать с ORDER BY по стабильной уникальной колонке.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ