GIN Index Pending List Overflow: Rýchle Zápisy, Pomalé Vyhľadávanie
Vyzeralo to ako nahodne zastavky, az kym kernel nepriznal pending list. “Full-text search išiel z 5ms na 500ms po dátovej migrácii.” Príčina: GIN index pending list naakumuloval tisíce záznamov počas bulk insertov a vyhľadávania musia lineárne skenovať všetky pending záznamy.
Prostredie: PostgreSQL 14+, GIN indexy pre full-text search alebo JSONB, bulk data loads alebo high-write workloady
Problém
Incident Pomalého Vyhľadávania
-- Pred bulk insertom: Rýchle vyhľadávanie
EXPLAIN ANALYZE SELECT * FROM documents
WHERE tsv @@ to_tsquery('postgresql');
-- Index Scan using documents_tsv_gin
-- Execution Time: 5.234 ms
-- Po bulk inserte 100,000 dokumentov
EXPLAIN ANALYZE SELECT * FROM documents
WHERE tsv @@ to_tsquery('postgresql');
-- Index Scan using documents_tsv_gin
-- Execution Time: 523.891 ms -- 100x pomalšie!
-- Čo sa stalo?
Skrytý Pending List
-- Skontroluj veľkosť pending listu
SELECT
indexrelid::regclass AS index_name,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
n_pending_pages,
n_pending_tuples
FROM pg_stat_user_indexes
JOIN pg_index USING (indexrelid)
LEFT JOIN pg_class c ON c.oid = indexrelid
CROSS JOIN LATERAL (
SELECT * FROM gin_metapage_info(get_raw_page(indexrelid::regclass::text, 0))
) meta
WHERE indexprs IS NOT NULL OR indkey::text LIKE '%gin%';
-- Výstup:
-- index_name | index_size | n_pending_pages | n_pending_tuples
-- documents_tsv_gin | 250 MB | 1523 | 98,234
-- ^^^^ ^^^^^^
-- Tieto pending záznamy spomaľujú search!
Príčina
Ako GIN Indexy Fungujú
GIN Index Štruktúra:
┌─────────────────────────────────────────────────────────────┐
│ Hlavný B-tree: slovo → zoznam document ID │
│ │
│ "postgresql" → [doc1, doc5, doc42, doc789, ...] │
│ "database" → [doc1, doc2, doc3, doc5, ...] │
│ "index" → [doc5, doc42, doc100, ...] │
│ │
│ Vloženie dokumentu znamená update MNOHO záznamov │
│ Dokument so 100 slovami = 100 index insertov! │
└─────────────────────────────────────────────────────────────┘
Pre zrýchlenie insertov GIN používa "pending list":
┌─────────────────────────────────────────────────────────────┐
│ Pending List (nezoradený, rýchly na append): │
│ │
│ doc1001: "postgresql", "rocks", "performance" │
│ doc1002: "gin", "index", "pending", "list" │
│ doc1003: "full", "text", "search", "postgresql" │
│ ... │
│ │
│ Počas INSERT: Len append do pending listu (rýchle!) │
│ Počas SEARCH: Musí skenovať VŠETKY pending záznamy (pomalé!)│
│ Počas VACUUM: Pending list zlúčený do hlavného B-tree │
└─────────────────────────────────────────────────────────────┘
Keď Pending List Narastie Príliš
-- Default: pending list sa auto-čistí pri ~4MB dát
-- Ale počas bulk insertov môže narásť oveľa viac
-- Skontroluj gin_pending_list_limit
SHOW gin_pending_list_limit;
-- 4096kB (4MB)
-- Počas bulk insertu:
-- - Inserty sú rýchle (len append do pending listu)
-- - Pending list narastie na 100MB+
-- - Každé vyhľadávanie musí skenovať 100MB nezoradených dát
-- - VACUUM to nakoniec vyčistí, ale nemusí bežať počas bulk load
Diagnostika
Skontroluj Veľkosť Pending Listu
-- Použi pageinspect extension
CREATE EXTENSION IF NOT EXISTS pageinspect;
-- Skontroluj GIN meta page pre pending info
SELECT *
FROM gin_metapage_info(get_raw_page('documents_tsv_gin', 0));
-- pending_head: 1234
-- pending_tail: 2345
-- n_pending_pages: 1523 -- Vysoké = problém
-- n_pending_tuples: 98234
-- Pravidlo:
-- n_pending_pages > 100 = pravdepodobne spôsobuje spomalenie
-- n_pending_tuples > 10000 = definitívne pomalé
Porovnaj Query Plány
-- Pre porovnanie
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM documents
WHERE tsv @@ to_tsquery('postgresql');
-- Hľadaj:
-- "Buffers: shared hit=15234" -- Veľa bufferov = skenuje pending list
Riešenie
Možnosť 1: Vynúť Pending List Cleanup
-- Vyčisti pending list bez full vacuum
VACUUM documents;
-- Alebo agresívnejšie:
VACUUM (VERBOSE) documents;
-- Skontroluj výsledok
SELECT n_pending_pages, n_pending_tuples
FROM gin_metapage_info(get_raw_page('documents_tsv_gin', 0));
-- Mal by ukazovať 0 alebo veľmi nízke čísla
Možnosť 2: Vylaď gin_pending_list_limit
-- Nižší limit = častejší cleanup, pomalšie inserty, rýchlejšie search
SET gin_pending_list_limit = '1MB'; -- Prísnejší
-- Alebo úplne vypni pending list (pre malé indexy)
SET gin_pending_list_limit = 0;
-- Teraz každý INSERT okamžite updatene hlavný index
-- Inserty pomalšie, vyhľadávanie vždy rýchle
-- Per-index nastavenie (PostgreSQL 14+):
ALTER INDEX documents_tsv_gin SET (gin_pending_list_limit = 1024);
Možnosť 3: Explicitný Cleanup Po Bulk Load
-- Bulk load vzor s cleanup
BEGIN;
-- Vypni autovacuum počas load
ALTER TABLE documents SET (autovacuum_enabled = false);
-- Urob bulk insert
COPY documents FROM '/data/import.csv';
-- Znova zapni autovacuum
ALTER TABLE documents SET (autovacuum_enabled = true);
COMMIT;
-- Okamžitý cleanup
VACUUM documents;
REINDEX INDEX CONCURRENTLY documents_tsv_gin;
Možnosť 4: Použi fastupdate = off pre Predvídateľný Výkon
-- Vytvor GIN index bez pending listu
CREATE INDEX documents_tsv_gin ON documents USING gin(tsv)
WITH (fastupdate = off);
-- Alebo alter existujúci
ALTER INDEX documents_tsv_gin SET (fastupdate = off);
REINDEX INDEX CONCURRENTLY documents_tsv_gin;
-- Trade-off:
-- fastupdate = on: Inserty rýchle, search môže byť pomalé
-- fastupdate = off: Inserty pomalé, search vždy rýchle
Checklist
## GIN Index Pending List Overflow
### Symptómy
- [ ] Full-text search zrazu pomalý
- [ ] Spomalenie koreluje s bulk insertmi
- [ ] VACUUM dočasne zrýchli veci
- [ ] Query plány ukazujú vysoké buffer reads
### Diagnostika
- [ ] Skontroluj n_pending_pages v gin_metapage_info
- [ ] Porovnaj pre/post insert query výkon
- [ ] Skontroluj autovacuum aktivitu počas bulk loads
- [ ] Monitoruj veľkosť pending listu v čase
### Riešenia
- [ ] Spusti VACUUM po bulk insertoch
- [ ] Zníž gin_pending_list_limit
- [ ] Zváž fastupdate = off pre read-heavy workloady
- [ ] Zahrň VACUUM do bulk load skriptov
### Prevencia
- [ ] Monitoruj GIN pending list metriky
- [ ] Naplánuj VACUUM po známych bulk operáciách
- [ ] Testuj search výkon po migráciách
- [ ] Dokumentuj očakávané pending list správanie
Záver
Lekcia: GIN indexy obchodujú rýchlosť vyhľadávania za rýchlosť insertu cez pending list. Počas bulk operácií sa tento trade-off môže stať extrémnym. Vždy spusti VACUUM po bulk loads do GIN-indexovaných tabuliek.
Kľúčové body:
- Pending list sa skenuje lineárne - tisíce záznamov = pomalé vyhľadávanie
- VACUUM čistí pending list - spusti ho po bulk loads
- fastupdate = off dáva predvídateľný výkon vyhľadávania
- Monitoruj n_pending_pages ako leading indicator
Súvisiace články
- PostgreSQL HOT Updates Index Trap - Ďalšia index pasca
- work_mem Parallel Workers OOM - PostgreSQL memory problémy
Súvisiace články
PostgreSQL Partial Index: Plánovač Ignoruje Tvoj Index
Query skenuje celú tabuľku napriek perfektnému partial indexu. Príčina: WHERE klauzula query sa presne nezhoduje s predikátom indexu, alebo štatistiky zavádzajú plánovač.
ICU Collation Version Drift: Keď Upgrade Databázy Rozbije Tvoje Indexy
Query vracia nesprávne výsledky po upgrade OS. Príčina: ICU library verzia sa zmenila, pravidlá collation sa posunuli a indexy sú teraz zoradené nekonzistentne s novým poradím.
PostgreSQL Read Replica Konflikty: Prečo sa vaše dotazy rušia
Dotazy na read replikách zlyhávajú s 'canceling statement due to conflict with recovery'. Riešenie závisí od toho, ktorý z 5 typov konfliktov máte - tu je návod ako diagnostikovať a vyriešiť každý z nich.
PostgreSQL OOM by Design: work_mem × Parallel Workers × Plan Nodes
work_mem vyzerá malé na 256MB, ale parallel hash join so 4 workers naprieč 3 plan nodes používa 3GB. Tu je ako zabrániť PostgreSQL legitímne OOMnúť váš kontajner.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.