Späť na blog

GIN Index Pending List Overflow: Rýchle Zápisy, Pomalé Vyhľadávanie

|
| postgresql, debugging, indexes, full-text-search, performance

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:

  1. Pending list sa skenuje lineárne - tisíce záznamov = pomalé vyhľadávanie
  2. VACUUM čistí pending list - spusti ho po bulk loads
  3. fastupdate = off dáva predvídateľný výkon vyhľadávania
  4. Monitoruj n_pending_pages ako leading indicator

Súvisiace články

Súvisiace články

Citujte tento článok

Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.

Michal Drozd. "GIN Index Pending List Overflow: Rýchle Zápisy, Pomalé Vyhľadávanie". https://www.michal-drozd.com/sk/blog/gin-index-pending-list-overflow/ (Publikované 17. apríla 2025).