Späť na blog

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:

  1. Query musí implikovať predikát - nie len prekrývať
  2. Syntaktické porovnávanie, nie sémantické - nevie dokázať komplexnú logiku
  3. Parametre neznáme v čase plánovania - pridaj explicitný predikát
  4. Typ záleží - 'active' vs 'active'::text sú rôzne

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. "PostgreSQL Partial Index: Plánovač Ignoruje Tvoj Index". https://www.michal-drozd.com/sk/blog/postgresql-partial-index-planner-miss/ (Publikované 4. marca 2025).