Пагинация

Представь: ты работаешь с таблицей из 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.

SQL
-- Быстрый запрос (Страница 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.

SQL
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» (только «Вперед/Назад»). Требует более сложной логики на бэкенде, особенно при кастомных сортировках.