> Какие стратегии пагинации можно использовать вместо LIMIT OFFSET? (Go)

Уровень: senior · Роль: backend · Категория: Технические вопросы

Компании: Reksoft

Стек: Go

> Пример ответа

Короткий ответ

Основные альтернативы LIMIT OFFSET - keyset pagination (на основе уникального сортируемого ключа), cursor-based pagination с непрозрачным курсором, и pagination по страницам с фиксированным размером на основе индекса. Для больших наборов данных keyset и cursor предпочтительнее: они дают O(log n) вместо O(n) и стабильны при вставках/удалениях. Также используют поиск по диапазону (например, по дате) или комбинацию с фильтрацией. В Go часто реализуют через SQL-запросы с WHERE и ORDER BY, а не через OFFSET.

Подробное объяснение

LIMIT OFFSET имеет два фундаментальных недостатка:

  • производительность деградирует с ростом OFFSET - база сканирует и отбрасывает строки до смещения;
  • нестабильность при изменении данных между запросами - строки могут дублироваться или пропадать.

Keyset pagination использует условие на уникальный ключ, например WHERE (created_at, id) > ($1, $2) ORDER BY created_at, id LIMIT $3. Это позволяет базе идти по индексу, а не по смещению. Требует составного индекса и передачи последнего значения ключа.

Cursor-based pagination - обобщение keyset: клиент получает непрозрачный токен, который сервер декодирует в условия запроса. Преимущество - скрытие структуры ключа и возможность менять сортировку без изменения API. Минус - сложнее реализовать и нельзя перейти на произвольную страницу.

Другие варианты:

  • поиск по диапазону дат или ID (например, WHERE id > $1), если данные монотонно растут;
  • сегментация по хэшу или шарду - для распределённых систем;
  • пагинация на основе поиска (Elasticsearch search_after).

Выбор зависит от требований: нужен ли произвольный доступ к страницам (тогда только OFFSET или номер страницы), или достаточно последовательного просмотра (тогда keyset/cursor).

На практике

Для senior-позиции важно показать понимание trade-off:

  • keyset не поддерживает переход к произвольной странице - только "вперёд";
  • при сортировке по нестабильному полю нужно добавлять tie-breaker (например, id);
  • для cursor нужно продумать кодирование (base64, подпись для защиты от подделки);
  • в Go удобно использовать time.Time и uuid как ключи, но помнить про коллизии времени - поэтому всегда добавляют id.

Также стоит упомянуть, что для малых объёмов (до нескольких тысяч строк) OFFSET допустим, и оптимизировать преждевременно не нужно. Для API с большими таблицами - keyset обязателен.

Пример кода

GO
// Keyset pagination в Go + PostgreSQL
type Page struct {
Limit int
LastCreatedAt time.Time
LastID int64
}
func FetchPage(ctx context.Context, db *sql.DB, p Page) ([]Item, error) {
rows, err := db.QueryContext(ctx, `
SELECT id, created_at, name
FROM items
WHERE (created_at, id) > ($1, $2)
ORDER BY created_at, id
LIMIT $3
`, p.LastCreatedAt, p.LastID, p.Limit)
// ...
}
GO
// Cursor encoding
type Cursor struct {
CreatedAt time.Time `json:"c"`
ID int64 `json:"i"`
}
func EncodeCursor(c Cursor) string {
data, _ := json.Marshal(c)
return base64.URLEncoding.EncodeToString(data)
}
func DecodeCursor(s string) (Cursor, error) {
data, err := base64.URLEncoding.DecodeString(s)
if err != nil { return Cursor{}, err }
var c Cursor
err = json.Unmarshal(data, &c)
return c, err
}

Как отвечать на собеседовании

Начни с недостатков OFFSET, затем перечисли альтернативы. Обязательно упомяни keyset и cursor как основные. Приведи пример SQL с составным условием. Покажи понимание ограничений: отсутствие произвольного доступа, необходимость индекса, tie-breaker. Если спросят про API - объясни, как кодировать курсор. Для senior важно сказать, что выбор зависит от сценария: админка с номерами страниц - OFFSET, публичный API с лентой - cursor.

Что проверяет интервьюер

  • Понимание внутреннего устройства пагинации в БД (индексы, сканирование).
  • Умение видеть trade-off между простотой и производительностью.
  • Знание реальных ограничений: нестабильность, отсутствие произвольного доступа.
  • Способность проектировать API с учётом клиентского опыта.
  • Практический опыт с Go и SQL.

Типичные ошибки

  • Предлагать keyset без tie-breaker при неуникальной сортировке.
  • Забывать про необходимость составного индекса.
  • Утверждать, что cursor решает все проблемы - он не даёт перейти к странице 5.
  • Использовать OFFSET для больших таблиц без оговорки о допустимости.
  • Не упоминать про кодирование и валидацию курсора на сервере.
  • Путать keyset и cursor - второе это надстройка над первым.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью