TGViewer
SQL Ready | Базы Данных SQL Ready | Базы Данных @sql_ready · 17.5K subscribers
Post #2112 2.01K
Почему OFFSET деградирует на больших таблицах!

OFFSET часто используют для пагинации, потому что запрос выглядит просто. На небольших таблицах проблем обычно нет, но на больших объёмах стоимость такого подхода растёт вместе с глубиной страницы.

Есть таблица:
events(
id BIGINT,
user_id BIGINT,
payload JSONB,
created_at TIMESTAMPTZ
)


Типичная пагинация:
SELECT
id,
user_id,
created_at
FROM events
ORDER BY created_at DESC
LIMIT 50 OFFSET 1000000;


Кажется, что база должна просто пропустить миллион строк и вернуть следующие 50. Но SQL-движок не может сделать настоящий прыжок через OFFSET. Ему нужно обработать строки до нужной позиции, а потом отбросить их.

Без подходящего индекса это может выглядеть так: база читает строки, определяет порядок сортировки, обрабатывает OFFSET + LIMIT записей, выбрасывает ненужные строки и возвращает только нужный результат.

Даже если есть индекс:
CREATE INDEX idx_events_created_at
ON events(created_at DESC);


ситуация всё равно не идеальна.

Индекс помогает быстрее читать данные в нужном порядке, но базе всё равно приходится пройти большое количество записей, чтобы добраться до нужного OFFSET.

Например:
LIMIT 50 OFFSET 1000000;


означает, что базе нужно обработать примерно миллион строк перед тем, как вернуть результат.

Для глубоких страниц обычно используют keyset pagination (cursor pagination). Вместо номера страницы передаётся последнее значение из предыдущего результата:
SELECT
id,
user_id,
created_at
FROM events
WHERE created_at < '2026-08-01 12:00:00'
ORDER BY created_at DESC
LIMIT 50;


Теперь база может использовать индекс и сразу искать позицию, с которой нужно продолжить чтение.

Но одного created_at недостаточно, если несколько событий имеют одинаковое время.

Например:
2026-08-01 12:00:00

2026-08-01 12:00:00

2026-08-01 12:00:00


Порядок становится неоднозначным. Поэтому добавляют уникальный ключ:
CREATE INDEX idx_events_cursor
ON events(created_at DESC, id DESC);


И запрос становится:
SELECT
id,
user_id,
created_at
FROM events
WHERE (created_at, id) < ('2026-08-01 12:00:00', 1500000)
ORDER BY created_at DESC, id DESC
LIMIT 50;


Теперь курсор точно определяет позицию в индексе.

Главное преимущество такого подхода — стоимость запроса зависит в основном от размера страницы, а не от глубины пагинации. Первая страница:
LIMIT 50;


и страница после миллионов записей:
WHERE (created_at, id) < (...)
LIMIT 50;


используют один и тот же принцип: найти позицию в B-tree индексе и продолжить чтение.

OFFSET остаётся нормальным решением для административных интерфейсов, внутренних инструментов и небольших таблиц. Но для лент событий, истории операций, логов и больших списков он быстро становится узким местом.

🔥 Делаем вывод: OFFSET удобен для разработки, но плохо масштабируется на больших объёмах данных. Для больших таблиц лучше использовать keyset pagination с составным индексом и стабильным курсором.

➡️ SQL Ready | #практика
  • 🔥 14
  • 👍 7
  • 🤝 7
More from @sql_ready
  1. Oct 5, 2026Проверяйте JSON без преобразования! Если JSON приходит как text, необязательно делать ::js…
  2. Oct 2, 2026Настройте оценку пользовательских функций! Для пользовательской функции PostgreSQL позволя…
  3. Oct 2, 2026🔎 Гибридный поиск в YDB: когда SQL ищет не только по словам В YDB, созданном Yandex B2B T…
  4. Oct 2, 2026😍 SQL-Tutorial — бесплатный учебник по SQL с практическими заданиями! Онлайн-учебник для…
  5. Oct 1, 2026🐱 PostgreSQL Course RU — курс по PostgreSQL и SQL для разработчиков! В репозитории собран…
  6. Sep 30, 2026📂 Шпаргалка по числовым функциям! Например, ROUND() используется для округления значений,…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →