Запрос выглядел примерно так:
SELECT p.id, p.title, p.price, p.rating, c.name AS category
FROM products p
JOIN categories c ON c.id = p.category_id
WHERE p.is_active = true
AND p.category_id IN (142, 143, 144, 145)
AND p.price BETWEEN 1000 AND 5000
AND p.rating >= 4.0
ORDER BY p.rating DESC, p.created_at DESC
LIMIT 40 OFFSET 800;
Смотрим план выполнения:
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT p.id, p.title, p.price, p.rating, c.name AS category
FROM products p
JOIN categories c ON c.id = p.category_id
WHERE p.is_active = true
AND p.category_id IN (142, 143, 144, 145)
AND p.price BETWEEN 1000 AND 5000
AND p.rating >= 4.0
ORDER BY p.rating DESC, p.created_at DESC
LIMIT 40 OFFSET 800;
-- Результат (сокращённо):
-- Limit (cost=89234.12..89234.22 rows=40 width=128)
-- (actual time=378.442..378.451 rows=40 loops=1)
-- -> Sort (cost=89232.12..89456.33 rows=89684 width=128)
-- (actual time=375.221..378.102 rows=840 loops=1)
-- Sort Key: p.rating DESC, p.created_at DESC
-- Sort Method: top-N heapsort Memory: 312kB
-- -> Seq Scan on products p (cost=0.00..72145.00 rows=89684 width=128)
-- (actual time=0.023..298.445 rows=87234 loops=1)
-- Filter: (is_active AND category_id = ANY(...) AND ...)
-- Rows Removed by Filter: 7912766
-- Buffers: shared hit=12045 read=43210
Проблема очевидна: Seq Scan по 8 миллионам строк с фильтрацией 7.9 миллионов из них. Индексов подходящих нет.
-- Partial index только для активных товаров
-- (неактивных 15%, они никогда не запрашиваются)
CREATE INDEX CONCURRENTLY idx_products_active_category_price_rating
ON products (category_id, price, rating DESC)
WHERE is_active = true;
-- Covering index для избежания обращений к таблице (Index Only Scan)
CREATE INDEX CONCURRENTLY idx_products_catalog_covering
ON products (category_id, rating DESC, created_at DESC)
INCLUDE (title, price)
WHERE is_active = true;
После создания индекса тот же запрос:
-- Limit (cost=2.34..18.56 rows=40 width=128)
-- (actual time=0.089..0.134 rows=40 loops=1)
-- -> Index Only Scan using idx_products_catalog_covering on products p
-- (cost=0.56..1823.44 rows=89684 width=128)
-- (actual time=0.044..0.128 rows=840 loops=1)
-- Index Cond: (category_id = ANY(...) AND rating >= 4.0)
-- Filter: (price >= 1000 AND price <= 5000)
-- Heap Fetches: 0
-- Buffers: shared hit=12
Время упало с 378 мс до 0.13 мс — ускорение почти в 3000 раз. Index Only Scan не обращается к таблице вообще (Heap Fetches: 0), потому что все нужные колонки есть в индексе через INCLUDE.
Оставить комментарий