PostgreSQL Partial Index: Plánovač Ignoruje Tvoj Index
Pamatam si query plan, ktory minul partial index. “Vytvoril som partial index pre aktívnych používateľov ale queries stále robia sekvenčné skeny.” Príčina: PostgreSQL plánovač môže použiť partial indexy len keď podmienka query implikuje predikát indexu - a je vyberavejší než by si čakal.
Prostredie: PostgreSQL 13+, partial indexy na veľkých tabuľkách, queries s rôznymi WHERE klauzulami
Problém
Index Existuje Ale Nepoužíva Sa
-- Vytvor partial index pre aktívnych používateľov (5% tabuľky)
CREATE INDEX idx_users_active_email ON users(email)
WHERE status = 'active';
-- Tabuľka má 10 miliónov riadkov, 500k aktívnych
EXPLAIN ANALYZE
SELECT * FROM users WHERE status = 'active' AND email = '[email protected]';
-- Očakávané: Index Scan using idx_users_active_email
-- Skutočné: Seq Scan on users (cost=0.00..412583.00 rows=1 width=128)
-- Filter: ((status = 'active') AND (email = '[email protected]'))
-- Rows Removed by Filter: 9999999
-- Time: 4523ms
-- Prečo ignoruje môj perfektný partial index?!
Variácie Ktoré Zlyhávajú
-- Prípad 1: Iný operátor
SELECT * FROM users WHERE status != 'inactive' AND email = '[email protected]';
-- Plánovač: "status != 'inactive'" nedokazuje "status = 'active'"
-- Prípad 2: Spracovanie NULL
SELECT * FROM users WHERE status = 'active' AND email = '[email protected]'
AND deleted_at IS NULL;
-- Plánovač: Môj index nehovorí nič o deleted_at
-- Prípad 3: OR podmienka
SELECT * FROM users
WHERE (status = 'active' OR is_admin = true) AND email = '[email protected]';
-- Plánovač: Neviem dokázať predikát indexu z OR
-- Prípad 4: Premenná/funkcia v query
SELECT * FROM users WHERE status = $1 AND email = '[email protected]';
-- Plánovač: V čase plánovania je $1 neznáme - nemusí byť 'active'
Príčina
Pravidlá Implikácie Predikátu
Požiadavky na použitie PostgreSQL partial indexu:
WHERE klauzula query musí IMPLIKOVAŤ WHERE klauzulu indexu
(Nie len prekrývať, nie len byť rovnaká - musí logicky implikovať!)
┌─────────────────────────────────────────────────────────────┐
│ Predikát indexu: status = 'active' │
│ │
│ ✓ Query: status = 'active' → Priama zhoda │
│ ✓ Query: status = 'active' AND x → Implikuje predikát │
│ │
│ ✗ Query: status IN ('active', 'x') → Neimplikuje │
│ ✗ Query: status != 'inactive' → Logicky rovnaké, ale │
│ plánovač nevie dokázať│
│ ✗ Query: status = $1 → Neznáme v čase plánu │
│ ✗ Query: status = ANY(array) → Nevie dokázať │
└─────────────────────────────────────────────────────────────┘
Systém Dôkazov Plánovača
-- PostgreSQL používa jednoduché syntaktické porovnávanie + nejaké transformácie
-- NEROBÍ plné logické uvažovanie
-- Toto funguje (syntaktická zhoda):
WHERE status = 'active' AND email = 'x'
-- Toto zlyháva (vyžadovalo by sémantické uvažovanie):
WHERE (CASE WHEN true THEN status ELSE 'x' END) = 'active'
-- Toto zlyháva (iná syntax, rovnaký význam):
WHERE status = 'active'::text -- Cast mení strom výrazu!
-- Skontroluj čo plánovač vidí
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM users WHERE status = 'active' AND email = 'x';
Diagnostika
Skontroluj Zhodu Predikátu Indexu
-- Pozri definíciu indexu
SELECT indexdef FROM pg_indexes
WHERE indexname = 'idx_users_active_email';
-- CREATE INDEX idx_users_active_email ON users(email) WHERE status = 'active'
-- Otestuj či plánovač vie zhodovať tvoju query
EXPLAIN (COSTS OFF)
SELECT * FROM users WHERE status = 'active' AND email = 'test';
-- Malo by ukázať: Index Scan using idx_users_active_email
-- Ak nie, skús presnú zhodu predikátu
EXPLAIN (COSTS OFF)
SELECT * FROM users WHERE email = 'test' AND status = 'active';
-- Rovnaká logika, iné poradie - stále by malo fungovať
Skontroluj Nezhody Typov
-- Častá pasca: text vs varchar vs char
SELECT
a.attname,
pg_catalog.format_type(a.atttypid, a.atttypmod) as type
FROM pg_attribute a
JOIN pg_class c ON a.attrelid = c.oid
WHERE c.relname = 'users' AND a.attname = 'status';
-- Vráti: character varying(20)
-- Ak tvoja query používa iný typ:
EXPLAIN SELECT * FROM users
WHERE status = 'active'::text AND email = 'x';
-- Nemusí sa zhodovať ak status je varchar!
Skontroluj Štatistiky
-- Aj so správnym predikátom, zlé štatistiky môžu zabrániť použitiu indexu
SELECT
n_distinct,
null_frac,
most_common_vals,
most_common_freqs
FROM pg_stats
WHERE tablename = 'users' AND attname = 'status';
-- Obnov štatistiky
ANALYZE users;
-- Skontroluj štatistiky indexu
SELECT
indexrelid::regclass AS index_name,
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
WHERE indexrelname = 'idx_users_active_email';
Riešenie
Možnosť 1: Zhoduj Predikát Presne
-- Použi presne rovnaký výraz ako predikát indexu
-- NEROB:
SELECT * FROM users WHERE status != 'inactive' AND email = 'x';
-- ROB:
SELECT * FROM users WHERE status = 'active' AND email = 'x';
-- Pre prepared statements, pridaj explicitnú podmienku
PREPARE get_active_user(text) AS
SELECT * FROM users WHERE status = 'active' AND email = $1;
-- Teraz plánovač vie status = 'active' v čase plánovania
Možnosť 2: Vytvor Expression Index
-- Ak potrebuješ status != 'inactive', vytvor index pre to
CREATE INDEX idx_users_not_inactive_email ON users(email)
WHERE status != 'inactive';
-- Alebo použi zložitejšie predikáty ak potrebné
CREATE INDEX idx_users_active_or_pending ON users(email)
WHERE status IN ('active', 'pending');
Možnosť 3: Použi Immutable Function Wrapper
-- Obal komplexnú logiku do immutable funkcie
CREATE OR REPLACE FUNCTION is_active_user(status text)
RETURNS boolean AS $$
SELECT status = 'active' OR status = 'pending'
$$ LANGUAGE sql IMMUTABLE;
-- Vytvor index používajúci funkciu
CREATE INDEX idx_users_active_fn ON users(email)
WHERE is_active_user(status);
-- Query používajúca rovnakú funkciu
SELECT * FROM users WHERE is_active_user(status) AND email = 'x';
Možnosť 4: Nastavenie Constraint Exclusion
-- Pre partitioned tabuľky alebo inheritance, zapni constraint_exclusion
SET constraint_exclusion = on; -- Default je 'partition'
-- Toto pomáha plánovaču používať partition constraints
-- Podobná logika platí pre partial indexy
Možnosť 5: Vynúť Index s CTE
-- Posledná možnosť: Donúť plánovač
WITH active_users AS MATERIALIZED (
SELECT * FROM users WHERE status = 'active'
)
SELECT * FROM active_users WHERE email = 'x';
-- Toto vynúti vyhodnotenie status = 'active' najprv
-- Nie ideálne, ale funguje keď plánovač nespolupracuje
Monitoring
groups:
- name: postgresql-indexes
rules:
- alert: PartialIndexNotUsed
expr: |
increase(pg_stat_user_indexes_idx_scan{indexrelname=~".*partial.*"}[1d]) == 0
and pg_stat_user_indexes_idx_tup_read{indexrelname=~".*partial.*"} > 0
for: 1d
labels:
severity: warning
annotations:
summary: "Partial index {{ $labels.indexrelname }} neskenovaný za 24h"
- alert: SequentialScanOnLargeTable
expr: |
rate(pg_stat_user_tables_seq_scan{relname="users"}[5m]) > 10
and pg_stat_user_tables_n_live_tup{relname="users"} > 1000000
for: 30m
labels:
severity: warning
annotations:
summary: "Vysoká miera sekvenčného skenu na veľkej tabuľke users"
Checklist
## PostgreSQL Partial Index Nepoužívaný
### Diagnostika
- [ ] Skontroluj EXPLAIN výstup pre použitie indexu
- [ ] Over že WHERE query presne implikuje predikát indexu
- [ ] Skontroluj nezhody typov (text vs varchar)
- [ ] Over že štatistiky sú aktuálne (ANALYZE)
- [ ] Testuj s literálnymi hodnotami vs parametrami
### Časté Príčiny
- [ ] Použitie != namiesto = v query
- [ ] Použitie IN/ANY namiesto presnej hodnoty
- [ ] Prepared statement s neznámym parametrom
- [ ] Type cast meniaci strom výrazu
- [ ] OR podmienky lámajúce implikáciu
### Riešenia
- [ ] Prepíš query aby presne zodpovedala predikátu
- [ ] Vytvor dodatočný partial index pre iné predikáty
- [ ] Použi immutable function wrapper pre komplexnú logiku
- [ ] Pridaj explicitný predikát do prepared statements
- [ ] Obnov štatistiky s ANALYZE
Záver
Lekcia: Použitie PostgreSQL partial indexu vyžaduje syntaktický dôkaz že query implikuje predikát indexu. “Logicky ekvivalentné” nestačí - plánovač potrebuje vidieť presne rovnaký výraz alebo niečo čo vie triviálne transformovať.
Kľúčové princípy:
- Query musí implikovať predikát - nie len prekrývať
- Syntaktické porovnávanie, nie sémantické - nevie dokázať komplexnú logiku
- Parametre neznáme v čase plánovania - pridaj explicitný predikát
- Typ záleží -
'active'vs'active'::textsú rôzne
Súvisiace články
- PostgreSQL HOT Updates Index Trap - Prekvapenia správania indexov
- GIN Index Pending List Overflow - Problémy údržby indexov
Súvisiace články
GIN Index Pending List Overflow: Rýchle Zápisy, Pomalé Vyhľadávanie
Full-text search bol rýchly, teraz je pomalý. Príčina: GIN index pending list narástol obrovský počas bulk insertov a každé vyhľadávanie musí teraz skenovať nezoradené pending záznamy.
EXPLAIN vám klamal: PostgreSQL Prepared Statement Plan Cliff
Váš EXPLAIN vyzerá perfektne ale produkcia horí. Vinník: PostgreSQL ticho prepol z custom plánu na generic plán po dostatočnom počte vykonaní, a generic plán je katastrofálne zlý.
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.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.