List vs Page vs Slice vs Specification у Spring Data JPA: повний гід Spring Boot 3

Оновлено:
List vs Page vs Slice vs Specification у Spring Data JPA: повний гід Spring Boot 3

Spring Data JPA пропонує чотири принципово різних підходи до отримання даних.

Кожен з них генерує різний SQL, по-різному поводиться з пам'яттю

і вирішує різний клас задач. Неправильний вибір — це або зайвий

COUNT(*) при кожному scroll-івенті, або OOM при зростанні таблиці,

або 64 derived methods замість одного. Ця стаття — повний гід

для того щоб обирати правильно з першого разу.

⚡ Коротко — якщо немає часу читати все

  • 📋 List — довідники, агрегація, дані з гарантованим верхнім обмеженням
  • 📄 Page — адмін-панель, пошук з лічильником, numbered pagination
  • 🔄 Slice — infinite scroll, «завантажити ще», мобільний API
  • 🔍 Specification — динамічні фільтри з 3+ опціональними умовами
  • 👇 Нижче — деталі, таблиця порівняння і decision tree

📚 Зміст

🎯 Чому вибір типу повернення важливіший ніж здається

Тип повернення визначає скільки SQL-запитів виконається і скільки пам'яті буде використано

Більшість розробників сприймають List, Page,

Slice як різні «обгортки» навколо одних і тих самих даних.

Насправді — це принципово різні контракти з базою даних і JVM.

Вибір між ними визначає кількість SQL-запитів, наявність LIMIT,

споживання heap і поведінку при зростанні даних.

Змінити тип повернення з Page на Slice

одна зміна в сигнатурі методу. Ефект — вдвічі менше SQL-запитів

при кожному scroll-івенті. Це не мікрооптимізація, це архітектурне рішення.

Що кожен тип робить під капотом

List<T> — виконує один SELECT без LIMIT.

Всі відповідні рядки завантажуються в heap.

Розмір результату визначається даними, а не кодом.

Безпечний тільки коли розмір обмежений природою домену.

Page<T> — виконує два SQL-запити: основний SELECT

з LIMIT/OFFSET і окремий COUNT(*). Повертає totalElements

і totalPages. Виправданий тільки коли UI ці значення відображає.

Якщо не відображає — COUNT виконується даремно.

Slice<T> — виконує один SELECT з LIMIT = pageSize + 1.

Визначає hasNext() без COUNT. Оптимальний вибір для

infinite scroll і будь-якого UI де потрібна тільки відповідь «є ще / немає».

Specification<T> — не тип повернення, а механізм

побудови WHERE-умов. Використовується разом з Page або

Slice коли фільтри динамічні і опціональні.

Вирішує проблему combinatorial explosion у derived methods.

Реальна вартість неправильного вибору

Page замість Slice для infinite scroll.

При 100 scroll-івентах за хвилину — 100 зайвих COUNT(*) запитів.

При таблиці 5M рядків з фільтрами кожен COUNT може займати

200–500ms. Тобто 20–50 секунд зайвої роботи БД за хвилину —

тільки щоб порахувати те що UI не показує.

List без LIMIT для таблиці що зростає.

При 100 рядках — непомітно. При 1M рядках — OOM або

2–3 секундний запит що блокує connection pool для всіх інших запитів.

Код однаковий, поведінка кардинально різна залежно від розміру таблиці.

Derived methods замість Specification для 4+ фільтрів.

4 опціональних фільтри = 16 методів у Repository і 16 відповідних

if/else блоків у сервісі. Кожен новий фільтр подвоює складність.

📊 Швидке порівняння — таблиця з 10 критеріями

Повне порівняння за технічними і практичними параметрами.

Актуально для Spring Boot 3.x / Spring Data JPA 3.x.

КритерійList<T>Slice<T>Page<T>Specification<T>
SQL LIMIT❌ відсутній✅ pageSize+1✅ pageSizeЗалежить від типу повернення
COUNT(*) запит✅ завждиПри використанні з Page
totalElements / totalPagesПри використанні з Page
hasNext()При використанні з Slice
Захист від OOM✅ (через Slice/Page)
Динамічні фільтри✅ основна задача
JOIN FETCH сумісність⚠️ потребує countQuery⚠️ потребує fetch у Specification
@Cacheable паттерн✅ стандартний⚠️ overhead⚠️ overhead
Infinite scroll / «ще»✅ оптимально⚠️ зайвий COUNT✅ з Slice
Адмін-панель з лічильником✅ оптимально✅ з Page
Batch-обробка великих даних⚠️ тільки з LIMIT⚠️ зайвий COUNT✅ з Slice
Довідники / конфігурація✅ оптимально⚠️⚠️

List vs Page vs Slice vs Specification у Spring Data JPA: повний гід Spring Boot 3

🎯 Сценарії — що обрати для конкретної задачі

Тип визначається тим що відображає UI і як зростають дані

Абстрактні правила («використовуйте Page для пагінації») не дають відповіді

на конкретні задачі. Розглянемо шість реальних product-сценаріїв

і обґрунтований вибір для кожного.

Сценарій 1: Каталог товарів інтернет-магазину з infinite scroll

Що потрібно: підвантаження наступної порції товарів при прокрутці.

UI не показує «Сторінка 3 з 47» — тільки нові товари знизу.

Вибір: Slice. Один SQL без COUNT. hasNext()

повідомляє чи підвантажувати ще. При 10 000 товарів і 50 scroll-івентах

за сесію — Slice генерує 50 запитів, Page — 100.

Для динамічних фільтрів (категорія + ціна + бренд) — Specification + Slice.

Сценарій 2: Адмін-панель замовлень з numbered pagination

Що потрібно: таблиця замовлень з навігацією «1 2 3 ... 47»

і лічильником «Показано 41–60 з 1 234».

Вибір: Page. totalElements і totalPages

відображаються в UI — COUNT виправданий. Для фільтрів по статусу, даті,

клієнту — Specification + Page.

Сценарій 3: Мобільний API для стрічки новин

Що потрібно: endpoint що повертає наступну порцію постів.

Мобільний клієнт показує кнопку «Завантажити ще» або автоматичний scroll.

Загальна кількість постів не відображається.

Вибір: Slice. Мінімальний JSON у відповіді:

content, hasNext, currentPage.

Жодного COUNT. Для cursor-based підходу (стабільніший при активній стрічці) —

Slice з createdAtBefore замість page number.

Сценарій 4: Завантаження довідників при старті додатку

Що потрібно: завантажити список країн, валют, категорій

при старті і закешувати на весь час роботи.

Вибір: List + @Cacheable. Дані стабільні, розмір обмежений

природою домену (195 країн, ~170 валют). Page і Slice тут надлишкові —

вони додають overhead для даних що завантажуються один раз.

Сценарій 5: Batch-job для щонічної обробки замовлень

Що потрібно: обробити всі замовлення зі статусом PENDING

що надійшли за останню добу. Їх може бути 10K–100K.

Вибір: Slice. Обробка пакетами по 100–500 замовлень.

hasNext() визначає чи продовжувати.

List з таким об'ємом — ризик OOM.

Page — зайвий COUNT при кожному пакеті.

Для великих обсягів (1M+) — Stream<T> з курсором.

Сценарій 6: Пошук публікацій в адмін-панелі з 5 фільтрами

Що потрібно: фільтрація по статусу, автору, категорії,

даті створення і наявності зображення — всі фільтри опціональні.

UI показує numbered pagination і лічильник результатів.

Вибір: Specification + Page.

5 опціональних фільтрів = 32 комбінації = 32 derived methods без Specification.

З Specification — один findAll(spec, pageable).

Page виправданий бо UI показує totalElements.

⚠️ Антипатерни — типові помилки і їх наслідки

Три антипатерни зустрічаються в більшості Spring-проектів

Більшість помилок у виборі типу — не через незнання існування альтернатив,

а через неусвідомлення наслідків. Ось три найпоширеніші антипатерни

з конкретними наслідками в продакшені.

Антипатерн 1: Page для infinite scroll

Як виглядає: розробник реалізує infinite scroll і обирає

Page бо «Page — це стандартна пагінація».

Наслідок: кожен scroll-івент генерує COUNT(*).

При таблиці 5M рядків з фільтрами кожен COUNT — 200–500ms.

При 50 активних користувачах що одночасно гортають стрічку —

50 паралельних COUNT-запитів кожні 2–3 секунди.

БД перевантажена роботою що не приносить жодної користі UI.

Рішення: замінити Page<T> на

Slice<T> в сигнатурі Repository-методу.

Одна зміна, нуль змін у бізнес-логіці.

Антипатерн 2: List без LIMIT для таблиці що зростає

Як виглядає: findByStatus("ACTIVE") або

findAll() без явного LIMIT на таблиці замовлень,

транзакцій або будь-якій іншій що зростає з часом.

Наслідок: при 100 рядках — непомітно.

При 500K — 800MB heap і 3-секундний запит.

При 2M — OOM і падіння сервісу. Код не змінювався — змінились дані.

Це latent bug що проявляється при масштабуванні бізнесу.

Рішення: для всіх таблиць що зростають — тільки

Slice або Page. List допустимий тільки

для довідників і агрегованих результатів з гарантованим верхнім обмеженням.

Антипатерн 3: Derived methods для динамічних фільтрів

Як виглядає: Repository з десятками методів для

різних комбінацій фільтрів. Або один @Query з

IS NULL OR ... для всіх опціональних умов.

Наслідок для derived methods: 4 фільтри = 16 методів,

16 if/else в сервісі. Кожен новий фільтр подвоює складність.

Підтримувати неможливо, тестувати — 16 окремих тест-кейсів мінімум.

Наслідок для IS NULL OR: PostgreSQL фіксує query plan

при першому виконанні — і цей план може бути неоптимальним для інших

комбінацій фільтрів. При великих таблицях це suboptimal execution plans

для більшості запитів.

Рішення: Specification + JpaSpecificationExecutor.

Один метод Repository, будь-яка комбінація фільтрів, оптимальний SQL

для кожної комбінації.

Антипатерн 4: JOIN FETCH + Page без countQuery

Як виглядає: @Query з JOIN FETCH що повертає

Page<T> без явного countQuery.

Наслідок: Hibernate може застосувати LIMIT/OFFSET

в пам'яті замість SQL — тобто завантажити всі рядки

з JOIN і потім відфільтрувати в JVM. Попередження в логах:

HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory.

При великих таблицях — OOM або катастрофічна деградація продуктивності.

Рішення: завжди вказувати явний countQuery

для @Query з JOIN FETCH. Активувати

spring.jpa.properties.hibernate.query.fail_on_pagination_over_collection_fetch=true

щоб отримати exception замість мовчазної деградації.

🔀 Decision tree — алгоритм вибору

Три питання визначають правильний тип у більшості випадків

Замість запам'ятовування правил — алгоритм з питань.

Відповідайте послідовно і отримаєте обґрунтований вибір.

Крок 1: Чи може кількість рядків перевищити ~1000 при зростанні даних?

├── НІ (довідники, агрегація, конфігурація)

│ └── → LIST + @Cacheable якщо дані стабільні

└── ТАК (будь-яка таблиця що зростає)

Крок 2: Чи є динамічні опціональні фільтри (3+)?

├── ТАК

│ └── Крок 3: Чи показує UI загальну кількість?

│ ├── ТАК → SPECIFICATION + PAGE

│ └── НІ → SPECIFICATION + SLICE

└── НІ (фільтри фіксовані або відсутні)

Крок 3: Чи показує UI totalElements або totalPages?

├── ТАК (numbered pagination, лічильник результатів)

│ └── → PAGE

└── НІ (infinite scroll, «ще», мобільний API)

└── → SLICE

Швидка шпаргалка по сценаріях

СценарійРекомендаціяЧому
Infinite scroll в каталозіSliceCOUNT не потрібен, hasNext достатньо
Адмін-панель з «1 з 47»PageUI потребує totalPages
Мобільний API стрічкиSliceМінімальний JSON, без COUNT
Завантаження країн/валютList + @CacheableСтабільний розмір, кешується
Batch-job по 500 записівSliceCOUNT при кожному пакеті — зайво
Пошук з 5 фільтрамиSpecification + Page/SliceДинамічні умови, один метод
Статистика по місяцяхListGROUP BY, максимум 12 рядків
«Знайдено 234 результати»PagetotalElements в UI

List vs Page vs Slice vs Specification у Spring Data JPA: повний гід Spring Boot 3

📚 Серія статей — детальні розбори кожного типу

Ця стаття — огляд і гід для вибору. Кожен тип має окремий детальний розбір

з EXPLAIN ANALYZE, production-нюансами, антипатернами і повним кодом:

Slice в Spring Data JPA — infinite scroll без COUNT(*)

Механіка pageSize+1, порівняння SQL з Page, cursor-based pagination

для великих таблиць, три типові production-помилки і як їх уникнути.

Якщо реалізуєте infinite scroll або мобільний API — почніть звідси.

Page в Spring Data JPA — пагінація з COUNT і countQuery

Три EXPLAIN ANALYZE сценарії для COUNT (простий, з фільтрами, з JOIN),

небезпечна комбінація JOIN FETCH + Page, оптимізація через countQuery

і @QueryHints. Все що потрібно знати щоб Page не став bottleneck.

List в Spring Data JPA — ризики без LIMIT і коли безпечно

Як розраховується реальне споживання heap (в 3–5 разів більше ніж рядок у БД),

таблиця деградації від 100 рядків до OOM, безпечні і небезпечні сценарії

з прикладами ❌/✅. Stream<T> як альтернатива для batch-обробки.

Specification в Spring Data JPA — динамічні фільтри без combinatorial explosion

Математика combinatorial explosion (2ⁿ методів для N фільтрів),

механіка Criteria API, JpaSpecificationExecutor, повний приклад

адмін-панелі з 4 динамічними фільтрами і N+1 вирішення через fetch у Specification.

❓ FAQ

Чи можна комбінувати Specification з List замість Page або Slice?

Технічно так — findAll(Specification) без Pageable повертає

List<T>. Але для більшості сценаріїв де потрібна Specification

(адмін-панель, пошук) дані можуть бути великими — і List без LIMIT небезпечний.

Виняток: якщо Specification використовується для завантаження малого

фіксованого набору даних (наприклад, конфігурація з динамічними умовами) —

List прийнятний. В усіх інших випадках — Specification + Slice або Page.

Page extends Slice — чи можна завжди використовувати Page замість Slice?

Технічно Page є Slice і надає всі його методи включаючи hasNext().

Але «можна» не означає «варто». Page завжди виконує COUNT — навіть якщо

ви використовуєте тільки hasNext() з його Slice-частини.

Це зайвий SQL-запит при кожному виклику. Slice обирають свідомо

саме щоб уникнути цього overhead.

Як мігрувати з List на Slice у існуючому проекті?

Міграція мінімальна на рівні Repository — змінити тип повернення

з List<T> на Slice<T> і додати

Pageable параметр. На рівні сервісу — передавати

PageRequest.of(page, size). На рівні контролера —

змінити тип відповіді і додати hasNext в DTO.

Найскладніша частина — визначити правильний pageSize

і переконатись що клієнт коректно обробляє hasNext = false.

Specification сповільнює запити порівняно з @Query?

Ні, якщо є правильні індекси. Specification генерує такий самий SQL

що і еквівалентний @Query — різниця тільки в тому як SQL будується

(програмно через Criteria API vs рядком). На рівні виконання PostgreSQL

обробляє їх однаково. Єдиний нюанс: кожна унікальна комбінація Specification

генерує новий query plan — при дуже великій кількості комбінацій це може

вплинути на Hibernate query plan cache. На практиці для 4–6 фільтрів несуттєво.

Stream<T> — де він у цьому порівнянні?

Stream<T> — окремий тип повернення для batch-обробки

великих даних. На відміну від List, він завантажує рядки поступово

через cursor без матеріалізації всіх об'єктів в heap одночасно.

Він не замінює Page або Slice для API і UI — він для серверної

обробки коли потрібно пройтись по мільйонах записів не завантажуючи

їх всі в пам'ять. Вимагає @Transactional і явного закриття.

✅ Висновки

Вибір між List, Page, Slice і Specification — це не питання переваг

чи стилю. Це архітектурне рішення що визначає кількість SQL-запитів,

споживання пам'яті і поведінку системи при зростанні даних.

  • List — тільки для даних з гарантованим верхнім обмеженням. Для всього іншого — ризик
  • Slice — дефолтний вибір для пагінації коли UI не показує загальну кількість
  • Page — тільки коли UI дійсно відображає totalElements або totalPages
  • Specification — як тільки опціональних фільтрів стає 3+ і derived methods перестають масштабуватись
  • Specification не є самостійним — завжди використовується разом з Page або Slice
  • JOIN FETCH + Page без countQuery — небезпечна комбінація яку варто активно виявляти

Джерела:

Spring Data JPA — офіційна документація

Vlad Mihalcea — Spring Data JPA Specifications

PostgreSQL — EXPLAIN документація

Останні статті

Читайте більше цікавих матеріалів

List vs Page vs Slice vs Specification у Spring Data JPA: повний гід Spring Boot 3

List vs Page vs Slice vs Specification у Spring Data JPA: повний гід Spring Boot 3

Spring Data JPA пропонує чотири принципово різних підходи до отримання даних.Кожен з них генерує різний SQL, по-різному поводиться з пам'яттюі вирішує різний клас задач. Неправильний вибір — це або зайвийCOUNT(*) при кожному scroll-івенті, або OOM при зростанні таблиці,або 64 derived methods...

List в Spring Data JPA: коли безпечно і коли це ризик — Spring Boot 3

List в Spring Data JPA: коли безпечно і коли це ризик — Spring Boot 3

List&lt;T&gt; — найпростіший тип повернення в Spring Data JPA. Саме тому він найчастіше використовується там де не повинен. Відсутність LIMIT у згенерованому SQL означає: розмір результату нічим не обмежений. При таблиці на 100K+ рядків це пряма дорога до OutOfMemory або...

Specification в Spring Data JPA: динамічні запити без combinatorial explosion — Spring Boot 3

Specification в Spring Data JPA: динамічні запити без combinatorial explosion — Spring Boot 3

Коли фільтрів в адмін-панелі стає більше трьох — derived methods перетворюютьсяна комбінаторний вибух: 4 опціональних фільтри дають 16 можливих комбінаційі стільки ж методів у Repository. Specification вирішує цю проблему:один метод, будь-яка комбінація фільтрів, чистий SQL під капотом.⚡ Коротко✅...

Page в Spring Data JPA: повноцінна пагінація з COUNT — Spring Boot 3

Page в Spring Data JPA: повноцінна пагінація з COUNT — Spring Boot 3

Page&lt;T&gt; — найпопулярніший тип пагінації в Spring Data JPA, і водночас найчастіше використовуваний там де він не потрібен. Він виконує два SQL-запити при кожному виклику: основний SELECT і COUNT(*). Ключове питання не «як використовувати Page», а «коли він...

Slice в Spring Data JPA: легка пагінація для infinite scroll у Spring Boot 3

Slice в Spring Data JPA: легка пагінація для infinite scroll у Spring Boot 3

Більшість розробників за замовчуванням обирають Page&lt;T&gt; для пагінації — і платять за це зайвим COUNT(*) запитом при кожному scroll-івенті. Slice вирішує саме цю проблему: повертає наступну порцію даних без підрахунку загальної кількості рядків — і саме тому є...

OpenAI випустив GPT-5.4: що змінилось  в 2026

OpenAI випустив GPT-5.4: що змінилось в 2026

5 березня 2026 року OpenAI випустив GPT-5.4 — одночасно у ChatGPT, API і Codex.Це не черговий incremental update: модель вперше об'єднує coding pipeline GPT-5.3-Codexіз загальним reasoning, отримує native computer use і контекстне вікно до 1M токенів.Коротко: якщо ви будуєте агентні воркфлоу або...