Ricette per query SQL problematiche

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

Nel frattempo, l'avete già utilizzato più di 6000 volte, ma una delle funzioni convenienti potrebbe essere rimasta inosservata: si tratta di suggerimenti strutturali, che appaiono più o meno così:

Ricette per query SQL problematiche

Fate attenzione a loro e le vostre query «diventeranno fluide e setose». 🙂

E se vogliamo essere seri, molte situazioni che rendono una query lenta e «affamatica» in termini di risorse sono tipiche e possono essere riconosciute dalla struttura e dai dati del piano.

In questo caso, ogni singolo sviluppatore non dovrà cercare un'opzione di ottimizzazione da solo, basandosi esclusivamente sulla propria esperienza: possiamo dirgli cosa sta succedendo, qual è la causa e come si può arrivare a una soluzione. Ed è esattamente ciò che abbiamo fatto.

Ricette per query SQL problematiche

Esaminiamo più in dettaglio questi casi: come vengono definiti e a quali raccomandazioni portano.

Per un approfondimento dell'argomento, potete prima ascoltare il blocco corrispondente di la mia relazione a PGConf.Russia 2020, e solo dopo passare all'analisi dettagliata di ciascun esempio:

Guarda il video

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

Quando si verifica

Mostra l'ultima fattura del cliente «LLC Campanello».

Come riconoscere

-> Limite
   -> Ordinamento
      -> Scansione dell'indice [Solo] [Backwards] | Scansione Bitmap Heap

Raccomandazioni

Indice utilizzato ampliare con i campi di ordinamento.

Esempio:

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

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

SELEZIONA
  *
DA
  tbl
DOVE
  fk_cli = 1 -- selezione per un particolare relazione
ORDER BY
  pk DESC -- vogliamo solo una "ultima" registrazione
LIMITA 1;

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

Si può notare subito che dall'indice sono stati letti più di 100 registrazioni, che poi sono state tutte ordinate, e alla fine è stata lasciata solo una.

Correggiamo:

DROP INDEX tbl_fk_cli_idx;
CREA INDICE SU tbl(fk_cli, pk DESC); -- aggiunto chiave di ordinamento

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

Anche su un campione così primitivo — è stato 8.5 volte più veloce e 33 volte meno letture.L'effetto sarà tanto più evidente, quanti più «fatti» avrete per ogni valore fk.

Osserva che questo indice funzionerà come «prefisso» altrettanto bene e per altre query con fk, dove non c'era e non c'è ordinamento per pk non c'era e non c'è (si può leggere di più nel mio articolo sulla ricerca di indici inefficaci). Include anche un adeguato supporto esplicito della chiave esterna per questo campo.

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

Quando si verifica

Mostra tutti i contratti del cliente «LLC Campanello», stipulati a nome di «NAO Lutik».

Come riconoscere

-> BitmapAnd
   -> Scansione dell'indice Bitmap
   -> Scansione dell'indice Bitmap

Raccomandazioni

Crea indice composito da campi di entrambe le origini o espandere uno dei campi esistenti con quelli provenienti dall'altra.

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); -- indice per chiave esterna
CREATE INDEX ON tbl(fk_cli); -- indice per chiave esterna

SELECT
  *
FROM
  tbl
WHERE
  (fk_org, fk_cli) = (1, 999); -- selezione per una coppia specifica

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

Correggiamo:

DROP INDEX tbl_fk_org_idx;
CREATE INDEX ON tbl(fk_org, fk_cli);

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

Qui il guadagno è minore, poiché il Bitmap Heap Scan è già abbastanza efficiente di per sé. Tuttavia è 7 volte più veloce e richiede 2,5 volte meno letture.

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

Quando si verifica

Mostra le prime 20 richieste "proprie" o non assegnate per l'elaborazione, con le proprie in priorità.

Come riconoscere

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

Raccomandazioni

di utilizzare UNION [ALL] per unire sottoquery in ciascuno dei blocchi di condizioni OR.

Esempio:

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

CREATE INDEX ON tbl(fk_own, pk); -- indice con ordinamento "sembra appropriato"

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

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

Correggiamo:

(
  SELECT
    *
  FROM
    tbl
  WHERE
    fk_own = 1 -- prima "proprie" 20
  ORDER BY
    pk
  LIMIT 20
)
UNION ALL
(
  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 query SQL problematiche
[guarda su explain.tensor.ru]

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

Una descrizione più dettagliata di questo metodo di ottimizzazione con esempi specifici può essere letta negli articoli PostgreSQL Antipatterns: JOIN e OR dannosi e PostgreSQL Antipatterns: una storia di sviluppo iterativo della ricerca per nome, o "Ottimizzazione avanti e indietro".

Versione generalizzata selezione ordinata per più chiavi (e non solo per una coppia const/NULL) è stata trattata nell'articolo SQL HowTo: scriviamo un ciclo while direttamente nella query, o "Elementare tre strade".

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

Quando si verifica

Di solito si verifica quando si desidera "aggiungere un ulteriore filtro" a una query già esistente.

"E non avete anche uno simile, ma con bottoni di madreperlafilm "La mano di diamante"

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

Come riconoscere

-> Seq Scan | Bitmap Heap Scan | Index [Only] Scan [Backward]
   && 5 × righe < RRbF -- filtrato >80% di quanto letto
   && loops × RRbF > 100 -- e con oltre 100 registrazioni in totale

Raccomandazioni

Creare [più] specializzato indice con condizione WHERE o includere nel indice campi aggiuntivi.

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

Se invece la condizione di filtraggio può assumere valori diversi,, è preferibile ampliare l'indice con questi campi, come nella situazione con BitmapAnd menzionata 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 chiavi esterne diverse
  END fk_own
, (random() < 1::real/50) critical; -- 1:50, che richiede "critico"

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

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

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

Correggiamo:

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

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

Come vediamo, il filtraggio è completamente scomparso dal piano, e la query è diventata 5 volte più veloce.

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

Quando si verifica

Varie tentativi di creare una propria coda di elaborazione compiti, quando un gran numero di aggiornamenti/cancellazioni di record nella tabella porta a una situazione con un gran numero di record "morti".

Come riconoscere

-> Seq Scan | Bitmap Heap Scan | Index [Only] Scan [Backward]
   && loops × (righe + RRbF) < (hit condivisi + lettura condivisa) × 8
      -- letti più di 1KB per ogni record
   && hit condivisi + lettura condivisa > 64

Raccomandazioni

Eseguire regolarmente a mano VACUUM [FULL] o garantire un'esecuzione adeguatamente frequente autovacuum affinando i suoi parametri, incluso per una tabella specifica..

Nella maggior parte dei casi, simili problemi sono causati da una scarsa strutturazione delle query durante le chiamate dalla logica di business come quelle esaminate in PostgreSQL Antipatterns: combattiamo contro orde di "morti viventi".

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

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

Quando si verifica

Sembra che abbiamo letto un po', e tutto tramite l'indice, e non abbiamo filtrato nulla in eccesso - eppure sono state lette sostanzialmente più pagine di quanto desiderato.

Come riconoscere

-> Index [Only] Scan [Backward]
   && loops × (righe + RRbF) < (hit condivisi + lettura condivisa) × 8
      -- letti più di 1KB per ogni record
   && hit condivisi + lettura condivisa > 64

Raccomandazioni

Esaminare attentamente la struttura dell'indice utilizzato e i campi chiave specificati nella query - probabilmente, una parte dell'indice non è stata definita.Probabilmente dovrai creare un indice simile, ma senza campi prefissi o imparare a iterare i loro valori..

Esempio:

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

CREA INDICE SU tbl(fk_org, fk_cli); -- tutto quasi come in #2
-- solo che abbiamo già considerato inutile un indice separato su fk_cli e l'abbiamo rimosso

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

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

Sembra che vada tutto bene, anche per l'indice, ma è un po' sospetto: per ciascuna delle 20 righe lette si sono dovute consultare 4 pagine di dati, 32KB per scrittura — non è un po' troppo? E anche il nome dell'indice tbl_fk_org_fk_cli_idx invita a riflessioni.

Correggiamo:

CREA INDICE SU tbl(fk_cli);

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

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

Altri esempi di situazioni di inefficace utilizzo degli indici possono essere visti nell'articolo DBA: troviamo indici inutili.

#7: CTE × CTE

Quando si verifica

Nella query abbiamo scritto CTE "grasse" da tabelle diverse e poi abbiamo deciso di fare" tra di loro JOIN.

Il caso è pertinente per versioni inferiori a v12 o query con WITH MATERIALIZZATO.

Come riconoscere

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

Raccomandazioni

Analizzare attentamente la query — se servono davvero CTE qui? Если все-таки да, то applicare "dizionarizzazione" in hstore/json secondo il modello descritto in Antipatterns di PostgreSQL: colpiamo il dizionario con un pesante JOIN.

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

Quando si verifica

L'elaborazione una tantum (ordinamento o univocizzazione) di un grande numero di righe non rientra nella memoria allocata per questo.

Come riconoscere

-> *
   && temp scritto > 0

Raccomandazioni

Se la quantità di memoria utilizzata dall'operazione non supera di molto il valore impostato del parametro work_mem, vale la pena regolarlo. Puoi farlo direttamente nel config 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 query SQL problematiche
[guarda su explain.tensor.ru]

Correggiamo:

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

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

Per motivi evidenti, se si utilizza solo la memoria e non il disco, la query verrà eseguita molto più velocemente. Inoltre, parte del carico viene tolto dall'HDD.

Ma bisogna capire che non si può sempre allocare molta memoria — semplicemente non ci sarà sufficiente per tutti.

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

Quando si verifica

Sono stati iniettati molti dati nella base, ma non è stato possibile eseguire ANALIZZARE.

Come riconoscere

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

Raccomandazioni

Procedere ANALIZZARE.

Questa situazione è descritta in dettaglio in PostgreSQL Antipatterns: statistiche sopra ogni cosa.

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

Quando si verifica

Si è verificata un'attesa di blocco, imposta da una query concorrente, o non ci sono stati sufficienti risorse hardware CPU/ipervisor.

Come riconoscere

-> *
   && (hit condiviso / 8K) + (lettura condivisa / 1K)  100ms -- abbiamo letto poco, ma troppo a lungo

Raccomandazioni

Utilizza esterno sistema per il monitoraggio del server per la presenza di blocchi o di un consumo anomalo delle risorse. Già abbiamo parlato della nostra modalità di organizzazione di questo processo per centinaia di server qui e qui.

Ricette per query SQL problematiche
Ricette per query 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