Представь: ты работаешь с таблицей из 10 миллионов пользователей. Твоя задача — показать их список по 10 записей на странице. Звучит просто, но когда пользователь (или парсер) запрашивает 500,000-ю страницу, твой сервер начинает «умирать». Это классическая проблема пагинации в высоконагруженных системах.
Неправильная реализация приводит к катастрофе в продакшене:
- Экспоненциальная деградация: запросы к далеким страницам выполняются в сотни раз медленнее первых.
- Большое потребление RAM: базы данных сжигают гигабайты RAM для выполнения одного такого запроса.
- Тайм-ауты: пользователи видят ошибку 504 Gateway Timeout.
Влияние на UX: В Instagram при прокрутке ленты используется Cursor-based пагинация. Если бы они использовали классический Offset, то при добавлении нового поста кем-то из твоих подписок все посты сдвинулись бы вниз, и ты бы увидел дубликаты уже просмотренных фотографий.
1. Offset/Limit: Классический подход
Традиционный метод, где мы указываем, сколько записей пропустить () и сколько вернуть (offset). Например: limit.GET /users?offset=20&limit=10
-- Быстрый запрос (Страница 1) SELECT * FROM orders ORDER BY created_at DESC LIMIT 10 OFFSET 0; -- Время выполнения: 2ms -- УБИЙЦА БАЗЫ ДАННЫХ (Страница 100,000) SELECT * FROM orders ORDER BY created_at DESC LIMIT 10 OFFSET 1000000; -- Время выполнения: 8000ms
Почему Offset так сильно деградирует по скорости? Посмотрим на механику базы данных:
Помимо деградации скорости, Offset страдает проблемами консистентности. Если между запросами страниц кто-то добавит новую запись, весь список сдвинется, и пользователь увидит дубликаты.
Когда использовать Offset/Limit: Админ-панели (где нужен переход на конкретную страницу «№45»), небольшие датасеты (до 100k записей), статичные данные.
2. Cursor-based Pagination
Использует уникальный идентификатор (cursor) как закладку в книге. Вместо того чтобы говорить базе «пропусти миллион строк», мы говорим: «дай мне 10 записей, которые идут после вот этого конкретного ID». Например: .GET /items?after=cursor_value&limit=10
SELECT id, name, created_at FROM items WHERE id > 1000000 -- Наш распакованный курсор ORDER BY id ASC LIMIT 10; -- Время выполнения: 3ms (Всегда константное!)
- Константная производительность: БД использует индекс, чтобы прыгнуть прямо к нужному ID, вообще не читая предыдущий миллион строк.
- Стабильность (Без дубликатов): Если добавить новый пост, «закладка» не собьется.
Ловушка на собеседовании: Никогда не используй в качестве курсора только время ()! Два поста могут быть созданы в одну миллисекунду. Если курсор остановится на первом, второй будет пропущен навсегда. Курсор обязан быть уникальным. Правильный подход — использовать составной курсор: created_at(created_at, id).
Ограничения Cursor-based: Невозможно сделать кнопку «Перейти на страницу 100» (только «Вперед/Назад»). Требует более сложной логики на бэкенде, особенно при кастомных сортировках.