Как реализовать пагинацию данных на уровне SQL и в приложении?

Ответ

Паганация — это разбиение большого набора результатов на отдельные страницы.

1. Пагинация на уровне SQL

Используются операторы LIMIT и OFFSET (или их аналоги, например, FETCH NEXT ... OFFSET в стандарте SQL).

-- Получить 10 записей, начиная с 21-й (т.е. страница 3 при размере страницы 10)
SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 20;

Проблемы OFFSET:

  • Низкая производительность на глубоких страницах: СУБД должна пропустить (OFFSET) N записей, что требует их чтения и сортировки.
  • Консистентность при изменении данных: Если данные добавляются/удаляются между запросами, результаты могут "поплыть".

Решение — Keyset Pagination (пагинация по ключу):

-- Первая страница
SELECT * FROM products WHERE id > 0 ORDER BY id LIMIT 10;
-- Следующая страница: используем последний ID с предыдущей страницы
SELECT * FROM products WHERE id > :last_seen_id ORDER BY id LIMIT 10;

Этот метод эффективен, так как использует индекс для поиска и не требует пропуска записей.

2. Пагинация в приложении (Java/Spring)

Spring Data JPA предоставляет абстракцию Pageable и Page:

public interface ProductRepository extends JpaRepository<Product, Long> {
    Page<Product> findAllByCategory(String category, Pageable pageable);
}

// Использование в сервисе
Pageable pageable = PageRequest.of(2, 10, Sort.by("id")); // страница 3, размер 10
Page<Product> page = productRepository.findAllByCategory("Electronics", pageable);

List<Product> content = page.getContent(); // Данные страницы
int totalPages = page.getTotalPages();     // Общее число страниц
long totalElements = page.getTotalElements(); // Всего элементов

Практические аспекты:

  • Выбор между OFFSET и Keyset Pagination в зависимости от глубины пагинации и требований к консистентности.
  • Возврат клиенту не только данных страницы, но и метаданных (общее количество элементов, признак наличия следующей страницы).
  • Для очень больших наборов данных общее количество (COUNT(*)) может быть дорогой операцией, иногда его опускают.

Ответ 18+ 🔞

Да ты посмотри, какая, блядь, тема подъехала — пагинация! Это ж когда тебе, сука, не вываливают сразу все десять тысяч записей в рожу, а делят их по страничкам, как умные люди. А то бывает, запросишь список товаров, а тебе в ответ — вся база данных, пиздец просто, браузер зависает, как будто его ебнули кирпичом по голове.

1. Пагинация на уровне SQL, или как не обосраться с запросами

Вот смотри, классика жанра — LIMIT и OFFSET. Типа, дай мне, дружок-пирожок, 10 записей, начиная с двадцатой.

SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 20;

Вроде всё красиво, страница третья, всё чинно. Но тут, сука, подвох! Этот самый OFFSET — он же тупой, как пробка. Чтобы тебе дать запись номер 1000, база должна сначала прочитать и отсортировать все 999 предыдущих, а потом их выкинуть нахуй! Представляешь, какой расход? Это как искать иголку в стоге сена, перебирая каждую соломинку по очереди. На глубоких страницах производительность просто ложится и не встаёт.

И второй геморрой — консистентность. Пока ты листаешь, кто-то может запись добавить или удалить. И всё, твоя нумерация поплыла, как мудак в шторм. Ты на странице 3, а там уже половина записей с прошлой страницы 2.

Решение, блядь, гениальное и простое — Keyset Pagination, или пагинация по ключу. Забудь про OFFSET как страшный сон.

-- Первая страница: просто берём первые 10
SELECT * FROM products WHERE id > 0 ORDER BY id LIMIT 10;
-- Дальше: запоминаем последний ID с прошлой страницы и ищем дальше
SELECT * FROM products WHERE id > :last_seen_id ORDER BY id LIMIT 10;

Суть в чём? Мы не пропускаем записи, а продолжаем с того места, где остановились. База данных использует индекс по id, находит нужную точку за микросекунды и выдаёт следующие 10. Быстро, надёжно, и данные не плывут. Красота, ёпта!

2. Пагинация в приложении (Java/Spring), или как не изобретать велосипед

Тут, слава богу, всё уже придумали за нас. Spring Data JPA — это просто песня, ебать мои старые костыли. Смотри, как легко:

public interface ProductRepository extends JpaRepository<Product, Long> {
    Page<Product> findAllByCategory(String category, Pageable pageable);
}

// А вот как этим пользоваться в коде
Pageable pageable = PageRequest.of(2, 10, Sort.by("id")); // Страница 3 (нумерация с нуля, блядь!), размер 10
Page<Product> page = productRepository.findAllByCategory("Electronics", pageable);

List<Product> content = page.getContent(); // Собственно, товары на странице
int totalPages = page.getTotalPages();     // Сколько всего страниц
long totalElements = page.getTotalElements(); // А сколько всего товаров вообще

Вот это, сука, уровень! Всё в одном объекте: и данные, и метаинформация. Клиенту отдал — и он там себе кнопочки «вперёд-назад» рисует.

Но есть нюансы, блядь, всегда есть нюансы:

  • Выбор стратегии: Для админки, где листают по две страницы — можно и OFFSET. А для бесконечной ленты в соцсети — только keyset, иначе сервер сдохнет, пытаясь посчитать миллионные оффсеты.
  • Метаданные: Клиенту кроме списка товаров надо кинуть и общее количество, и флажок «есть ли следующая страница». Иначе он будет тупо жать «дальше» в пустоту.
  • *`COUNT()` — операция дорогая.** Когда в таблице овердохуища записей, этот запрос может тормозить как черепаха в патруле. Иногда его специально не делают, а просто возвращают данные + флаг «hasNext». Чистая прагматика, в рот меня чих-пых!

В общем, пагинация — вещь нужная. Главное — делать её с умом, а не просто OFFSET 1000000 в запрос сувать. И тогда и пользователи довольны, и база данных тебя за человека считает.