Ricette per interrogazioni SQL problematiche

Qualche mese fa abbiamo annunciato explain.tensor.ru — un servizio pubblico per l'analisi e la visualizzazione dei piani di query per PostgreSQL.

Nel tempo trascorso, lo avete già utilizzato oltre 6000 volte, ma una delle funzionalità comode potrebbe essere rimasta inosservata: si tratta di suggerimenti strutturali, che appaiono più o meno così:

Ricette per interrogazioni SQL problematiche

Seguitele e le vostre query diventeranno "lisce e setose". 🙂

E seriamente, molte situazioni che rendono una query lenta e "affamata" di risorse sono tipiche e possono essere riconosciute dalla struttura e dai dati del piano..

In questo caso, non sarà necessario per ogni singolo sviluppatore cercare un'opzione di ottimizzazione da solo, basandosi esclusivamente sulla propria esperienza: possiamo indicargli cosa sta succedendo, quale potrebbe essere la causa, e come avvicinarsi alla soluzione. E così abbiamo fatto.

Ricette per interrogazioni SQL problematiche

Esaminiamo un po' più da vicino questi casi: come vengono identificati e quali raccomandazioni portano.

Per un migliore approfondimento, prima potete ascoltare il blocco corrispondente del mio intervento a PGConf.Russia 2020, e poi passare all'analisi dettagliata di ogni esempio:

Riproduci video

#1: индексная «недосортировка»

Quando si presenta

Mostra l'ultima fattura del cliente «ООО Колокольчик».

Come identificare

-> Limit
   -> Sort
      -> Index [Only] Scan [Backward] | Bitmap Heap Scan

Raccomandazioni

Indice utilizzato espandere i campi di ordinamento.

Esempio:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk  -- 100K "fatti"
, (random() * 1000)::integer fk_cli; -- 1K chiavi esterne diverse

CREATE INDEX ON tbl(fk_cli); -- indice per la foreign key

SELECT
  *
FROM
  tbl
WHERE
  fk_cli = 1 -- selezione per un legame specifico
ORDER BY
  pk DESC -- vogliamo solo un "ultimo" record
LIMIT 1;

Ricette per interrogazioni SQL problematiche
[guarda su explain.tensor.ru]

Si può subito notare che attraverso l'indice sono stati letti più di 100 record, che sono stati poi tutti ordinati, per poi lasciarne solo uno.

Correggiamo:

DROP INDEX tbl_fk_cli_idx;
CREATE INDEX ON tbl(fk_cli, pk DESC); -- aggiunto chiave di ordinamento

Ricette per interrogazioni SQL problematiche
[guarda su explain.tensor.ru]

Anche su un campione così primitivo — 8,5 volte più veloce e 33 volte meno letture. L'effetto sarà tanto più evidente quanto più avete «fatti» per ogni valore fk.

Osservo che un tale indice funzionerà come un «indice prefissato» non peggio di prima anche su altre query con fk, dove non c'era e non c'è ordinamento per pk (è possibile leggere di più nel mio articolo sulla ricerca di indici inefficaci). Inoltre, assicurerà anche un normale supporto esplicito per la foreign key su questo campo.

#2: пересечение индексов (BitmapAnd)

Quando si presenta

Mostra tutti i contratti del cliente «ООО Колокольчик», stipulati a nome di «НАО Лютик».

Come identificare

-> BitmapAnd
   -> Bitmap Index Scan
   -> Bitmap Index Scan

Raccomandazioni

Crea indice composito sui campi di entrambi gli originali o espandere uno degli esistenti con campi dall'altro.

Esempio:

CREA TABELLA tbl COME
SELEZIONA
  generate_series(1, 100000) pk      -- 100K "fatti"
, (random() *  100)::integer fk_org  -- 100 chiavi esterne diverse
, (random() * 1000)::integer fk_cli; -- 1K chiavi esterne diverse

CREA INDICE SU tbl(fk_org); -- indice per chiave esterna
CREA INDICE SU tbl(fk_cli); -- indice per chiave esterna

SELEZIONA
  *
DA
  tbl
DOVE
  (fk_org, fk_cli) = (1, 999); -- filtro per una coppia specifica

Ricette per interrogazioni SQL problematiche
[guarda su explain.tensor.ru]

Correggiamo:

DROP INDEX tbl_fk_org_idx;
CREA INDICE SU tbl(fk_org, fk_cli);

Ricette per interrogazioni SQL problematiche
[guarda su explain.tensor.ru]

Qui il guadagno è minore, poiché il Bitmap Heap Scan è già abbastanza efficace di per sé. Ma comunque è 7 volte più veloce e 2.5 volte meno letture.

#3: объединение индексов (BitmapOr)

Quando si presenta

Mostra i primi 20 «propri» o richieste non assegnate per l'elaborazione, dando priorità a quelle proprie.

Come identificare

-> BitmapOr
   -> Bitmap Index Scan
   -> Bitmap Index Scan

Raccomandazioni

Usa UNIONE [TUTTE] per unire le sottoquery in ciascuno dei blocchi di condizioni OR.

Esempio:

CREA UNA TABELLA tbl AS
SELECT
  generate_series(1, 100000) pk  -- 100K "fatti"
, CASE
    WHEN random() < 1::real/16 THEN NULL -- con probabilità 1:16 inserimento "pareggio"
    ELSE (random() * 100)::integer -- 100 chiavi esterne diverse
  END fk_own;

CREA INDICE SU tbl(fk_own, pk); -- indice con ordinamento "apparentemente appropriato"

SELECT
  *
FROM
  tbl
WHERE
  fk_own = 1 OR -- propri
  fk_own IS NULL -- ... oppure "pareggi"
ORDER BY
  pk
, (fk_own = 1) DESC -- prima "propri"
LIMIT 20;

Ricette per interrogazioni SQL problematiche
[guarda su explain.tensor.ru]

Correggiamo:

(
  SELECT
    *
  FROM
    tbl
  WHERE
    fk_own = 1 -- prima "propri" 20
  ORDER BY
    pk
  LIMIT 20
)
UNIONE TUTTO
(
  SELECT
    *
  FROM
    tbl
  WHERE
    fk_own IS NULL -- poi "pareggi" 20
  ORDER BY
    pk
  LIMIT 20
)
LIMIT 20; -- ma in totale - 20, non serve di più

Ricette per interrogazioni SQL problematiche
[guarda su explain.tensor.ru]

Abbiamo sfruttato il fatto che tutte le 20 righe necessarie sono state ottenute immediatamente nel primo blocco, quindi il secondo, con un Bitmap Heap Scan più "costoso", non è stato nemmeno eseguito — di conseguenza 22 volte più veloce, 44 volte meno letture!

Una spiegazione più dettagliata di questo metodo di ottimizzazione con esempi specifici può essere letta negli articoli Antipattern di PostgreSQL: JOIN e OR dannosi e Antipattern di PostgreSQL: una storia sulla continua revisione della ricerca per titolo, o "Ottimizzazione avanti e indietro".

Una variante generalizzata di selezione ordinata per più chiavi (e non solo per una coppia const/NULL) è trattata nell'articolo SQL HowTo: scrivere un ciclo while direttamente nella query, o "La semplice tre vie".

#4: читаем много лишнего

Quando si presenta

Di norma, si presenta quando si desidera "aggiungere un altro filtro" a una query già esistente.

"Ma non avete qualcosa di simile, ma con bottoni perlatifilm "La mano di diamante"

Ad esempio, modificando il compito sopra, mostrare le prime 20 richieste "critiche" più vecchie da elaborare, indipendentemente dalla loro assegnazione.

Come identificare

-> Seq Scan | Bitmap Heap Scan | Index [Only] Scan [Backward]
   && 5 × righe < RRbF -- filtrato >80% di quanto letto
   && cicli × RRbF > 100 -- e comunque più di 100 record in totale

Raccomandazioni

Creare [un] indice più specializzato con una condizione WHERE o includere campi aggiuntivi nell'indice.

Se la condizione di filtraggio è "statica" per le tue operazioni — cioè non prevede l'ampliamento dell'elenco dei valori in futuro — è meglio utilizzare un indice WHERE. Questa categoria si adatta bene a diversi stati booleani/enumerativi.

Se la condizione di filtraggio può assumere valori diversi, è meglio ampliare l'indice con questi campi — come nella situazione con BitmapAnd sopra.

Esempio:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk -- 100K "fatti"
, CASE
    WHEN random() < 1::real/16 THEN NULL
    ELSE (random() * 100)::integer -- 100 diversi chiavi esterne
  END fk_own
, (random() < 1::real/50) critical; -- 1:50, che la richiesta è "critica"

CREATE INDEX ON tbl(pk);
CREATE INDEX ON tbl(fk_own, pk);

SELECT
  *
FROM
  tbl
WHERE
  critical
ORDER BY
  pk
LIMIT 20;

Ricette per interrogazioni SQL problematiche
[guarda su explain.tensor.ru]

Correggiamo:

CREATE INDEX ON tbl(pk)
  WHERE critical; -- aggiunto "una" condizione di filtraggio "statica"

Ricette per interrogazioni SQL problematiche
[guarda su explain.tensor.ru]

Come possiamo vedere, la filtrazione dal piano è completamente scomparsa e la richiesta è diventata 5 volte più veloce.

#5: разреженная таблица

Quando si presenta

Vari tentativi di creare una coda di lavoro propria, quando un gran numero di aggiornamenti/cancellazioni di record nella tabella porta a una situazione con un gran numero di record 'morti'.

Come identificare

-> Seq Scan | Bitmap Heap Scan | Index [Only] Scan [Backward]
   && loops × (rows + RRbF)  64

Raccomandazioni

Eseguire regolarmente a mano VACUUM [FULL] o garantire una lavorazione sufficientemente frequente autovacuum tramite un'adeguata configurazione dei suoi parametri, incluso per una tabella specifica.

In molti casi, tali problemi si rivelano causati da una cattiva strutturazione delle query nelle chiamate dalla logica di business, come quelle esaminate in Antipattern di PostgreSQL: combattiamo contro le orde di "mortiferi".

Ma bisogna capire che anche VACUUM FULL può non sempre aiutare. Per tali casi, è utile consultare l'algoritmo dell'articolo DBA: quando VACUUM non basta — puliamo la tabella manualmente.

#6: чтение с «середины» индекса

Quando si presenta

Sembra che abbiamo letto un po', e tutto tramite indice, e non abbiamo filtrato nessuno extra — eppure sono state lette notevolmente più pagine di quanto ci si aspettasse.

Come identificare

-> Indice [Solo] Scansione [Indietro]
   && cicli × (righe + RRbF)  64

Raccomandazioni

Esaminare attentamente la struttura dell'indice utilizzato e i campi chiave specificati nella richiesta — molto probabilmente, parte dell'indice non è specificata. Molto probabilmente, sarà necessario creare un indice simile, ma senza i campi prefissi o imparare a iterare i loro valori.

Esempio:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk      -- 100K "fatti"
, (random() *  100)::integer fk_org  -- 100 chiavi esterne diverse
, (random() * 1000)::integer fk_cli; -- 1K chiavi esterne diverse

CREATE INDEX ON tbl(fk_org, fk_cli); -- tutto quasi come in #2
-- solo che un indice separato su fk_cli lo abbiamo già considerato superfluo e rimosso

SELECT
  *
FROM
  tbl
WHERE
  fk_cli = 999 -- ma fk_org non è specificato, anche se è presente nell'indice prima
LIMIT 20;

Ricette per interrogazioni SQL problematiche
[guarda su explain.tensor.ru]

Sembra tutto a posto, anche per l'indice, ma è un po' sospetto — per ognuno dei 20 record letti è stato necessario leggere 4 pagine di dati, 32KB per record — non è un po' troppo? E anche il nome dell'indice tbl_fk_org_fk_cli_idx fa riflettere.

Correggiamo:

CREATE INDEX ON tbl(fk_cli);

Ricette per interrogazioni SQL problematiche
[guarda su explain.tensor.ru]

Improvvisamente — 10 volte più veloce e 4 volte meno da leggere!

Altri esempi di situazioni di uso inefficiente degli indici possono essere visti nell'articolo DBA: identifichiamo indici inutili.

#7: CTE × CTE

Quando si presenta

Nella query abbiamo creato CTE 'grassi' da diverse tabelle, e poi abbiamo deciso di fare tra di loro JOIN.

Il caso è attuale per versioni inferiori alla v12 o per query che utilizziamo con WITH MATERIALIZED.

Come identificare

-> Analisi CTE
   && cicli > 10
   && cicli × (righe + RRbF) > 10000
      -- prodotto cartesiano CTE troppo grande

Raccomandazioni

Analizzare attentamente la query — e se sono necessari CTE qui? Если все-таки да, то applicare 'dizionari' in hstore/json secondo il modello descritto in PostgreSQL Antipatterns: colpiamo con il dizionario i pesanti JOIN.

#8: swap на диск (temp written)

Quando si presenta

Un trattamento singolo (ordinamento o univocizzazione) di un grande numero di registrazioni non può rientrare nella memoria dedicata per questo.

Come identificare

-> *
   && temporaneo scritto > 0

Raccomandazioni

Se la quantità di memoria utilizzata dall'operazione non supera di molto il valore impostato del parametro work_mem, è consigliabile regolarlo. Puoi farlo direttamente nel file di configurazione per tutti, oppure tramite SET [LOCAL] per una specifica query/transazione.

Esempio:

SHOW work_mem;
-- "16MB"

SELECT
  random()
FROM
  generate_series(1, 1000000)
ORDER BY
  1;

Ricette per interrogazioni SQL problematiche
[guarda su explain.tensor.ru]

Correggiamo:

SET work_mem = '128MB'; -- prima di eseguire la query

Ricette per interrogazioni SQL problematiche
[guarda su explain.tensor.ru]

Per ovvi motivi, se utilizzi solo la memoria e non il disco, la query verrà eseguita molto più velocemente. Inoltre, parte del carico viene alleviato dal HDD.

Ma è fondamentale capire che non è sempre possibile assegnare molta memoria — semplicemente non basta per tutti.

#9: неактуальная статистика

Quando si presenta

Sono stati immessi molti dati, ma non sono stati elaborati in tempo. ANALIZZA.

Come identificare

-> Seq Scan | Bitmap Heap Scan | Index [Only] Scan [Backward]
   && ratio >> 10

Raccomandazioni

Procedere ANALIZZA.

Maggiore dettaglio su questa situazione è descritto in PostgreSQL Antipatterns: la statistica è tutto.

#10: «что-то пошло не так»

Quando si presenta

Si è verificato un timeout a causa di un blocco imposto da una query concorrente, oppure non sono state sufficienti le risorse hardware CPU/ipervisor.

Come identificare

-> *
   && (shared hit / 8K) + (shared read / 1K) < time / 1000
      -- RAM hit = 64MB/s, HDD read = 8MB/s
   && time > 100ms -- abbiamo letto poco, ma troppo a lungo

Raccomandazioni

Utilizza un sistema esterno di monitoraggio del server per verificare la presenza di blocchi o l'uso anomalo delle risorse. Abbiamo già parlato della nostra modalità di organizzazione di questo processo per centinaia di server. qui e qui.

Ricette per interrogazioni SQL problematiche
Ricette per interrogazioni SQL problematiche

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster