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 по стабильной уникальной колонке.