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

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

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

⚡ Коротко

  • List безпечний для довідників, малих таблиць і даних з гарантованим верхнім обмеженням
  • ⚠️ List небезпечний для будь-якої таблиці що зростає з часом без явного LIMIT
  • ⚠️ OutOfMemory — результат завантажується повністю в JVM heap перед поверненням
  • Єдиний безпечний List для великих таблиць — тільки з явним LIMIT у @Query
  • 👇 Нижче — механіка, ризики і чіткі критерії коли List виправданий

📚 Зміст

Інші статті серії:
📌 Slice в Spring Data JPA — infinite scroll без COUNT(*)
📌 Page в Spring Data JPA — пагінація з COUNT і countQuery
📌 Specification — динамічні фільтри без combinatorial explosion
📌 List vs Page vs Slice vs Specification — повний гід у Spring Data JPA

🎯 Що повертає List — і чим це відрізняється від пагінації

List завантажує всі рядки що відповідають запиту — без жодного обмеження

Коли Spring Data JPA виконує метод що повертає List<T>, Hibernate генерує SQL без LIMIT і без OFFSET. Всі рядки що відповідають умові WHERE завантажуються з бази даних, матеріалізуються в Java-об'єкти і зберігаються в пам'яті JVM до того як метод повертає результат. Це фундаментальна відмінність від Page і Slice — ті завжди мають LIMIT.

Пагінація — це контракт між додатком і БД: «поверни не більше N рядків». List такого контракту не має. Розмір результату визначається даними, а не кодом.

Три варіанти List у Spring Data — і що вони генерують

Варіант 1: findAll() без умов — найнебезпечніший. Генерує SELECT * FROM table без жодних умов і обмежень. При таблиці на 1M рядків — завантажує 1M об'єктів в heap.

Як не треба робити:

// Небезпечно для будь-якої таблиці що зростає
@RestController
public class OrderController {

    @GetMapping("/orders")
    public List<Order> getAllOrders() {
        return orderRepository.findAll(); // SELECT * FROM orders — без LIMIT
    }
}

Чому це погано: сьогодні замовлень 5 000 — і все працює. Через рік замовлень 2 000 000 — і цей endpoint покладе сервер. Жодних попереджень, жодних помилок компіляції. Просто OOM в продакшені о 3 годині ночі. Таблиця orders — це класичний приклад таблиці що зростає з часом без верхнього обмеження.

Варіант 2: derived methods з WHERE — краще, але все одно без LIMIT. Наприклад, findByStatus(String status) генерує SELECT * FROM orders WHERE status = 'PENDING'. Умова звужує вибірку — але не обмежує її розмір.

Як не треба робити:

// Виглядає безпечно — але це ілюзія
public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByStatus(String status); // SELECT ... WHERE status = ?
}

// У сервісі:
List<Order> pendingOrders = orderRepository.findByStatus("PENDING");
// Якщо PENDING-замовлень 300K — всі 300K в пам'яті

Чому це погано: розробник бачить WHERE-умову і думає що запит «обмежений». Але WHERE фільтрує рядки — не обмежує їх кількість. Розподіл статусів залежить від бізнесу: якщо обробка замовлень сповільнилась і PENDING накопичились — один запит обваляє сервіс.

Варіант 3: @Query з явним LIMIT — єдиний безпечний варіант для великих таблиць. Розробник явно контролює максимальний розмір вибірки.

Як треба робити (якщо List дійсно потрібен):

// Явний LIMIT — розмір результату контрольований кодом
@Query("""
    SELECT o FROM Order o
    WHERE o.status = :status
    ORDER BY o.createdAt DESC
    LIMIT 50
    """)
List<Order> findTopByStatus(@Param("status") String status);
// Завжди максимум 50 рядків — незалежно від розміру таблиці

Але тут варто задати собі питання: якщо потрібен LIMIT і є ймовірність що даних більше — чи не краще Slice з Pageable? Він дає ті самі гарантії і додатково повідомляє чи є ще дані.

Чому List ≠ пагінація — архітектурна різниця

Пагінація — це не просто «повернути частину даних». Це архітектурний контракт що захищає додаток від неконтрольованого зростання навантаження. Page і Slice гарантують: незалежно від кількості рядків у таблиці, в пам'ять потрапить не більше pageSize + 1 об'єктів за один запит. Ця гарантія не залежить від розподілу даних, бізнес-логіки або поведінки користувачів.

List такої гарантії не має. Ось наочна різниця того що відбувається всередині при однаковому запиті:

// List — немає захисту від розміру даних
List<Order> orders = repository.findByStatus("PENDING");
// SQL: SELECT * FROM orders WHERE status = 'PENDING'
// Може повернути 10 рядків. Може 10 000 000. Код однаковий.

// Slice — завжди обмежено pageSize
Slice<Order> orders = repository.findByStatus("PENDING", PageRequest.of(0, 20));
// SQL: SELECT * FROM orders WHERE status = 'PENDING' LIMIT 21
// Завжди максимум 20 рядків в результаті. Гарантовано.

Якщо сьогодні в таблиці 1 000 рядків і List працює нормально — це не означає що буде нормально через рік коли рядків стане 1 000 000. Це latent bug що чекає свого часу.

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

🎯 Ризики: OutOfMemory і повний table scan

Два незалежних ризики — навантаження на БД і на JVM heap

List без LIMIT створює проблеми на двох рівнях одночасно.

На рівні бази даних — повний table scan або index scan без обмеження

кількості рядків що читаються. На рівні JVM — всі ці рядки

матеріалізуються в об'єкти і займають heap до завершення запиту.

При великих таблицях обидва ризики реалізуються одночасно.

OutOfMemory від List — це не теоретичний ризик.

Це реальна причина production-інцидентів коли таблиця

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

Як розраховується реальне споживання пам'яті

Рядок у PostgreSQL і Java-об'єкт в heap — це не одне й те саме.

Рядок у БД зберігається компактно: тільки дані і мінімальний overhead.

Java-об'єкт несе значно більший тягар:

  • Object header — 16 байт на кожен об'єкт (mark word + class pointer)
  • Поля entity — самі дані, але з Java-типами (String, LocalDateTime, BigDecimal — кожен є окремим об'єктом з власним header)
  • Посилання на зв'язані entities — навіть LAZY-зв'язки зберігають proxy-об'єкт
  • Hibernate identity map — кожен завантажений entity реєструється в Session cache для dirty checking

Практичний приклад з реальними цифрами:

// Entity Order з типовими полями

@Entity

public class Order {

private Long id; // 8 байт + Long object overhead

private String orderNumber; // посилання + char[] масив

private BigDecimal total; // посилання + BigDecimal overhead ~100 байт

private String status; // посилання + char[]

private LocalDateTime createdAt; // посилання + LocalDateTime overhead

@ManyToOne(fetch = LAZY)

private User user; // proxy-об'єкт навіть без завантаження

// ... ще поля

}

// Рядок у PostgreSQL: ~150-200 байт

// Java Order об'єкт в heap: ~800-1200 байт (в 5-6 разів більше)

// 500K рядків в БД: ~100MB

// 500K Order об'єктів в JVM: ~500MB - 600MB тільки для даних

// + Hibernate Session cache: ще ~200MB

// Разом: ~700-800MB тільки від одного findAll() виклику

При типовому heap Spring Boot додатку в 512MB або 1GB —

один такий запит може вичерпати всю доступну пам'ять.

❌ Як не треба: findAll() у методі що викликається регулярно

// ПОГАНО — викликається при кожному запиті до API

@Service

public class ReportService {

public Map<String, Long> getOrderCountByStatus() {

List<Order> allOrders = orderRepository.findAll(); // ❌

// Завантажує ВСІ замовлення щоб порахувати їх по статусах

return allOrders.stream()

.collect(Collectors.groupingBy(Order::getStatus, Collectors.counting()));

}

}

Чому це катастрофічно погано: задача — порахувати кількість

замовлень по статусах. Для цього завантажуються всі замовлення в пам'ять,

потім ітеруються в Java. БД вміє робити це в одному SQL-запиті без

передачі даних по мережі і без завантаження в heap:

// ДОБРЕ — агрегація на рівні БД

@Query("SELECT o.status, COUNT(o) FROM Order o GROUP BY o.status")

List<Object[]> countByStatus();

// SQL: SELECT status, COUNT(*) FROM orders GROUP BY status

// Повертає максимум стільки рядків скільки різних статусів — зазвичай 3-5

Cascading effect: один запит блокує весь додаток

OutOfMemory — найгірший сценарій, але не єдиний спосіб як великий

List руйнує продуктивність:

GC pressure і stop-the-world паузи.

Завантаження 500K об'єктів створює величезне навантаження на garbage collector.

Коли GC не встигає звільняти пам'ять — відбувається Full GC: додаток

зупиняється на сотні мілісекунд або навіть секунди.

В цей час всі HTTP-запити чекають — включаючи прості запити

що зайняли б 5ms без GC-паузи.

Прихована проблема: List у методі що здається безпечним

// Виглядає як просте завантаження налаштувань

@Scheduled(fixedDelay = 60_000) // кожну хвилину

public void syncActiveUsers() {

List<User> activeUsers = userRepository.findByActiveTrue(); // ❌

// Якщо активних користувачів 200K — кожну хвилину 200K об'єктів в heap

// GC спрацьовує після кожного sync — деградація для всього додатку

activeUsers.forEach(this::processUser);

}

Чому це погано: scheduled задача виглядає нешкідливо,

але якщо findByActiveTrue() повертає сотні тисяч записів —

вона регулярно створює GC-тиск. Правильний підхід — обробка через

Slice пакетами або Stream з курсором.

З'єднання з БД зайняте на весь час читання.

Поки Hibernate читає мільйон рядків з БД, з'єднання з connection pool

зайняте. При типовому пулі HikariCP в 10–20 з'єднань —

якщо кілька таких «важких» запитів виконуються одночасно,

пул вичерпується. Нові запити отримують Connection timeout

навіть якщо самі по собі прості і швидкі.

Як проблема з'являється непомітно

Найнебезпечніше в List без LIMIT — це те що проблема невидима

при розробці і на старті проекту. Типовий lifecycle:

ЧасРядків у таблиціЧас запитуHeapСтатус
День 1 (розробка)~100<1ms~1MB✅ OK
Місяць 3 (реліз)~5 000~10ms~50MB✅ OK
Місяць 12~200 000~400ms~800MB⚠️ Повільно
Місяць 24~1 000 000~2000msOOM❌ Інцидент

Код не змінювався жодного разу. Змінились тільки дані.

І саме тому це latent bug — він не проявляється в тестах

де 100 рядків, і вистрілює в продакшені при масштабуванні.

🎯 Коли List безпечний — чіткі критерії

List безпечний коли розмір даних обмежений природою домену

Єдина умова безпечного List — гарантія що кількість рядків

що повертаються не може перевищити розумного ліміту

незалежно від зростання бізнесу. Якщо ця гарантія є —

List прийнятний. Якщо ні — потрібна пагінація або явний LIMIT.

Запитайте себе: «Чи може кількість рядків у цьому запиті

зрости до 10K через рік?». Якщо відповідь «так» або «не знаю» —

List без LIMIT є ризиком.

✅ Безпечні сценарії — з поясненням чому вони безпечні

Довідники і словники.

Таблиці з фіксованим або майже фіксованим набором значень:

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

Ці дані безпечні бо їх розмір обмежений зовнішньою реальністю —

кількість країн не збільшиться в 100 разів через рік.

// ✅ ДОБРЕ — довідник країн, розмір стабільний

@Cacheable("countries")

public List<Country> getAllCountries() {

return countryRepository.findAll(); // 195 рядків, кешується

}

// findAll() тут виправданий і оптимальний:

// - розмір обмежений природою домену (країн у світі ~195)

// - @Cacheable означає SQL виконується рідко — тільки при cache miss

// - немає сенсу ускладнювати Slice для 195 статичних записів

Конфігурація що завантажується при старті.

Feature flags, налаштування, маппінги кодів — зазвичай десятки записів,

завантажуються один раз і кешуються на весь час роботи додатку.

// ✅ ДОБРЕ — конфігурація при старті додатку

@Component

public class ConfigLoader {

@PostConstruct

public void loadConfig() {

List<AppConfig> configs = configRepository.findAll();

// Завантажується один раз — ризику немає

configs.forEach(config -> configMap.put(config.getKey(), config.getValue()));

}

}

Агрегація з GROUP BY де кількість груп обмежена.

Статистика по місяцях (12 рядків), по статусах (3–5 рядків),

по категоріях якщо їх кількість фіксована — List цілком прийнятний.

// ✅ ДОБРЕ — агрегація повертає обмежену кількість рядків

@Query("""

SELECT MONTH(o.createdAt) as month, COUNT(o) as count

FROM Order o

WHERE YEAR(o.createdAt) = :year

GROUP BY MONTH(o.createdAt)

""")

List<Object[]> getMonthlyStats(@Param("year") int year);

// Максимум 12 рядків — незалежно від кількості замовлень в таблиці

❌ Небезпечні сценарії — з поясненням чому вони небезпечні

Таблиці що зростають без верхнього обмеження.

Замовлення, транзакції, події, логи, повідомлення — їх кількість

необмежена і залежить від активності бізнесу.

// ❌ ПОГАНО — таблиця що зростає

public List<Transaction> getTransactionsForReport() {

return transactionRepository.findByCreatedAtAfter(startOfYear);

// За рік транзакцій може бути 5 000 000

// findByCreatedAtAfter без LIMIT завантажить всі 5M в пам'ять

}

// ✅ ДОБРЕ — обмежуємо або агрегуємо

@Query("""

SELECT DATE(t.createdAt) as date, SUM(t.amount) as total

FROM Transaction t

WHERE t.createdAt > :startOfYear

GROUP BY DATE(t.createdAt)

ORDER BY date

""")

List<Object[]> getDailyTotals(@Param("startOfYear") LocalDateTime startOfYear);

// Повертає максимум 365 рядків незалежно від кількості транзакцій

Фільтри де розподіл даних непередбачуваний.

Особливо небезпечний патерн — коли розробник додає фільтр

і думає що це «обмежує» вибірку:

// ❌ ПОГАНО — виглядає обмежено, але не є

List<Notification> findByReadFalse();

// Здається: «ненрочитаних повідомлень небагато»

// Реальність: якщо сервіс відправки повідомлень запрацював агресивніше

// або bulk-розсилка — за тиждень 2M непрочитаних повідомлень

// Один запит — 2M об'єктів в пам'яті

// ✅ ДОБРЕ — явний ліміт або пагінація

Slice<Notification> findByReadFalseOrderByCreatedAtDesc(Pageable pageable);

// PageRequest.of(0, 50) — завжди максимум 50 повідомлень

API endpoints без захисту.

Будь-який публічний або внутрішній endpoint що повертає List

без обмеження — вразливий до навмисного або випадкового abuse:

// ❌ ПОГАНО — endpoint без захисту

@GetMapping("/api/products/search")

public List<Product> searchProducts(@RequestParam String query) {

return productRepository.findByNameContaining(query);

// query = "a" → може повернути 100K продуктів що містять букву "а"

// Один запит може покласти сервер

}

// ✅ ДОБРЕ — завжди захищаємо API пагінацією

@GetMapping("/api/products/search")

public SliceResponse<Product> searchProducts(

@RequestParam String query,

@RequestParam(defaultValue = "0") int page,

@RequestParam(defaultValue = "20") @Max(100) int size) {

Pageable pageable = PageRequest.of(page, size);

Slice<Product> result = productRepository

.findByNameContainingOrderByNameAsc(query, pageable);

return new SliceResponse<>(result.getContent(), result.hasNext(), page);

}

🎯 SQL під капотом — три сценарії

List генерує SELECT без LIMIT — і це завжди потенційний full scan

Відсутність LIMIT означає що PostgreSQL повертає всі рядки що відповідають

умові. З індексом це index scan всіх відповідних рядків.

Без індексу — seq scan всієї таблиці. В обох випадках кількість

оброблених рядків не обмежена кодом.

Сценарій 1: findAll() — повний table scan

-- Spring Data генерує:

SELECT c.id, c.name, c.code FROM countries c; -- для довідника — OK

SELECT o.id, o.status, o.total FROM orders o; -- для orders — небезпечно

-- EXPLAIN ANALYZE (таблиця countries, 200 рядків):

Seq Scan on countries (cost=0.00..4.00 rows=200 width=40)

Planning Time: 0.1 ms

Execution Time: 0.3 ms ✅ прийнятно для довідника

-- EXPLAIN ANALYZE (таблиця orders, 1M рядків):

Seq Scan on orders (cost=0.00..28000.00 rows=1000000 width=120)

Planning Time: 0.2 ms

Execution Time: 2800 ms ❌ 2.8 секунди + 1M об'єктів в heap

Та сама операція findAll(), кардинально різний результат.

Ключова різниця — не в методі, а в природі таблиці:

countries не зростає, orders зростає нескінченно.

Сценарій 2: derived method з WHERE — часткова ілюзія безпеки

Найчастіша помилка: розробник додає WHERE-умову і вважає що запит

«обмежений». Але WHERE фільтрує рядки — не обмежує їх кількість.

Як не треба думати:

// «Активних користувачів не так багато» — небезпечне припущення

List<User> findByActiveTrue();

-- EXPLAIN при 50K активних користувачів:

Bitmap Index Scan on idx_users_active

Index Cond: (active = true)

Bitmap Heap Scan on users

Recheck Cond: (active = true)

Rows Removed by Recheck: 0

rows=50000 -- 50K рядків без обмеження

Execution Time: 180 ms

-- Плюс: 50K User об'єктів в heap ≈ 200-300MB

-- Плюс: 50K записів в Hibernate identity map

Чому це небезпечніше ніж здається: розробник бачить

швидкий EXPLAIN (180ms) і вирішує що все нормально. Але він не враховує

що 50K об'єктів в heap — це 200MB пам'яті, і при 10 паралельних

запитах це вже 2GB тільки від цієї операції.

-- Після додавання LIMIT (правильно):

SELECT u.id, u.email, u.active FROM users u

WHERE u.active = true

ORDER BY u.created_at DESC

LIMIT 21; -- Slice або явний LIMIT

Index Scan Backward using idx_users_created_at on users

Filter: (active = true)

rows=21

Execution Time: 0.4 ms ✅ 21 рядок замість 50K

Сценарій 3: @Query з явним LIMIT — правильний підхід для List

Якщо List дійсно потрібен (а не Slice), явний LIMIT в запиті —

єдиний спосіб гарантувати розмір результату:

// ✅ ДОБРЕ — явний LIMIT, розмір контрольований

@Query("""

SELECT o FROM Order o

WHERE o.status = :status

ORDER BY o.createdAt DESC

LIMIT 100

""")

List<Order> findTopByStatus(@Param("status") String status);

-- SQL:

SELECT o.id, o.status, o.total, o.created_at

FROM orders o

WHERE o.status = 'PENDING'

ORDER BY o.created_at DESC

LIMIT 100;

Index Scan Backward using idx_orders_created_at on orders

Filter: (status = 'PENDING')

rows=100

Execution Time: 0.8 ms ✅ завжди максимум 100 рядків

Але перш ніж писати List з явним LIMIT —

задайте собі питання: чи є ще дані за цим лімітом?

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

Slice вирішує обидві задачі одночасно: обмежує розмір

і повідомляє про наявність наступної порції через hasNext().

// Порівняння: коли List з LIMIT, коли Slice

// List з LIMIT — підходить коли:

// - потрібні топ-N записів для внутрішньої обробки

// - клієнту не потрібно знати чи є ще дані

// - фіксована вибірка: «останні 10 активностей для логу»

@Query("SELECT e FROM Event e ORDER BY e.createdAt DESC LIMIT 10")

List<Event> findLatestEvents();

// Slice — підходить коли:

// - клієнт може запросити наступну порцію

// - потрібен контроль і над розміром, і над наявністю наступних даних

// - infinite scroll, «завантажити ще», API з пагінацією

Slice<Product> findAllByOrderByCreatedAtDesc(Pageable pageable);

🎯 List vs Page vs Slice — правило вибору

Три запитання визначають правильний тип

Чи може кількість рядків перевищити розумний ліміт? → Якщо так — не List.

Чи потрібна загальна кількість елементів або сторінок? → Якщо так — Page.

Потрібна тільки наступна порція? → Slice.

Дані гарантовано малі і стабільні? → List.

КритерійListSlicePage
LIMIT в SQL❌ відсутній✅ pageSize+1✅ pageSize
COUNT(*) запит✅ завжди
Захист від OOM
Довідники / словники✅ оптимально⚠️ overhead⚠️ overhead
Infinite scroll / «ще»⚠️ зайвий COUNT
Адмін-панель з лічильником
Таблиці що зростають❌ ризик
Batch-обробка з LIMIT✅ з @Query⚠️ зайвий COUNT
Агрегація / GROUP BY⚠️⚠️

Правило вибору одним реченням

  • List — коли розмір результату обмежений природою домену

    і не залежить від зростання даних.

  • Slice — коли потрібна наступна порція даних

    і загальна кількість не важлива.

  • Page — коли UI потребує загальної кількості

    елементів або сторінок.

Детальний розбір кожного типу — в окремих статтях серії:

❓ FAQ

findAll() завжди небезпечний?

Ні — залежить від таблиці. findAll() на таблиці

countries (195 рядків) або currencies (~170 рядків) —

абсолютно прийнятний і типовий паттерн, особливо з @Cacheable.

findAll() на таблиці orders або transactions

що зростає з часом — ризик який реалізується при масштабуванні.

Питання не в методі, а в таблиці до якої він застосовується.

Чи є різниця між List<T> і Collection<T> як типом повернення?

З точки зору генерованого SQL — ніякої. Обидва типи повернення

призводять до виконання запиту без LIMIT. Різниця тільки в Java-контракті:

List гарантує порядок і доступ по індексу,

Collection — ні. Для Spring Data JPA обидва еквівалентні

з точки зору поведінки.

Stream<T> як альтернатива List для великих даних?

Так, і це важлива альтернатива для batch-обробки. Stream<T>

в Spring Data JPA завантажує рядки поступово (через cursor або fetch size),

а не всі одразу в heap. Це дозволяє обробляти мільйони записів

без ризику OOM — але вимагає явного закриття stream і обов'язково

транзакційного контексту (@Transactional).

Для batch-jobs де потрібно пройтись по всіх записах — Stream<T>

краще за List<T>.

Як захиститись від випадкового findAll() на великій таблиці?

Кілька підходів в залежності від контексту. По-перше, code review policy:

будь-який метод що повертає List без @Query з LIMIT

або без Pageable-параметра має бути явно обґрунтований.

По-друге, моніторинг повільних запитів: запити без LIMIT на великих таблицях

будуть видні в slow query log PostgreSQL (log_min_duration_statement).

По-третє, integration tests з реалістичним обсягом даних —

тест на 1M рядків покаже проблему до продакшену.

List і @Cacheable — правильна комбінація?

Так, для довідників це стандартний і правильний паттерн.

@Cacheable на методі що повертає List

означає: перший виклик виконує SQL і зберігає результат в кеші,

всі наступні — повертають кешований List без SQL-запиту.

Це особливо ефективно для даних що рідко змінюються:

країни, категорії, конфігурація. Важливо: кеш має мати

розумний TTL і інвалідуватись при змінах у довіднику.

✅ Висновки

List<T> — не «погана» абстракція.

Це правильний інструмент для конкретного класу задач:

довідники, агрегація, дані з гарантованим верхнім обмеженням.

Проблема виникає коли List використовується для таблиць що зростають —

і тоді це latent bug що чекає свого часу.

  • List не має LIMIT — розмір результату визначається даними, а не кодом
  • OOM від List — це не баг запиту, це наслідок зростання даних яке не було передбачено
  • Безпечний List — тільки для даних де розмір обмежений природою домену
  • Таблиці що зростають з часом завжди вимагають Slice або Page
  • Stream<T> — правильна альтернатива List для batch-обробки великих даних
  • Запитання «чи може цих рядків стати 10K через рік» — достатній критерій для рішення

Джерела:

Spring Data JPA — Query Methods documentation

Spring Data JPA — Streaming Query Results

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 токенів.Коротко: якщо ви будуєте агентні воркфлоу або...