Recepten voor falende SQL-query's

Enkele maanden geleden hebben we aangekondigd explain.tensor.ru — een openbare service voor het analyseren en visualiseren van query-executieplannen voor PostgreSQL.

In de tussentijd hebt u er al meer dan 6000 keer gebruik van gemaakt, maar een van de handige functies is misschien niet opgemerkt — dit is structurele hints, die er ongeveer zo uitzien:

Recepten voor falende SQL-query's

Luister ernaar en uw verzoeken zullen "glad en soepel" worden. 🙂

En om serieus te zijn, veel situaties die een verzoek traag en "resource-intensief" maken, zijn typisch en kunnen worden herkend aan de structuur en gegevens van het plan..

In dit geval hoeft elke afzonderlijke ontwikkelaar niet zelf naar een optimalisatie-oplossing te zoeken op basis van zijn eigen ervaring — we kunnen hem vertellen wat er aan de hand is, wat de oorzaak kan zijn en hoe hij dit kan benaderen.. Wat we ook gedaan hebben.

Recepten voor falende SQL-query's

Laten we deze gevallen iets gedetailleerder bekijken — hoe ze worden gedefinieerd en welke aanbevelingen ze opleveren.

Voor een betere kennismaking met het onderwerp kan je eerst de bijbehorende sectie uit mijn lezing op PGConf.Russia 2020, en dan verder gaan naar de gedetailleerde analyse van elk voorbeeld:

Video afspelen

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

Wanneer er ontstaat

Toon het laatste factuur voor klant "LLC Bell".

Hoe te herkennen

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

Aanbevelingen

De gebruikte index uitbreiden met sorteervelden.

Voorbeeld:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk  -- 100K "feiten"
, (random() * 1000)::integer fk_cli; -- 1K verschillende vreemde sleutels

CREATE INDEX ON tbl(fk_cli); -- index voor vreemde sleutel

SELECT
  *
FROM
  tbl
WHERE
  fk_cli = 1 -- selectie op specifieke relatie
ORDER BY
  pk DESC -- we willen slechts één "laatste" record
LIMIT 1;

Recepten voor falende SQL-query's
[bekijk op explain.tensor.ru]

Je zult meteen opmerken dat er meer dan 100 records uit de index zijn gelezen, die vervolgens allemaal zijn gesorteerd, en daarna is er slechts één overgebleven.

Oplossing:

DROP INDEX tbl_fk_cli_idx;
CREATE INDEX ON tbl(fk_cli, pk DESC); -- sorteer sleutel toegevoegd

Recepten voor falende SQL-query's
[bekijk op explain.tensor.ru]

Zelfs op zo'n primitieve selectie — 8,5 keer sneller en 33 keer minder leesacties.. Het effect zal des te duidelijker zijn, naarmate je meer "feiten" hebt voor elke waarde fk.

Ik merk op dat deze index net zo goed zal functioneren als "prefix" voor andere verzoeken met fk, waarbij er geen sorteringen op pk waren en zijn (meer hierover kun je lezen in mijn artikel over het vinden van ondoeltreffende indexen). Bovendien zal het ook zorgen voor een normale ondersteuning van de expliciete vreemde sleutel voor dit veld.

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

Wanneer er ontstaat

Toon alle contracten voor klant "LLC Bell", afgesloten namens "JSC Buttercup".

Hoe te herkennen

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

Aanbevelingen

Creëren samengestelde index over velden uit beide bronnen of breid een van de bestaande uit met velden uit de tweede.

Voorbeeld:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk      -- 100K "feiten"
, (random() *  100)::integer fk_org  -- 100 verschillende externe sleutels
, (random() * 1000)::integer fk_cli; -- 1K verschillende externe sleutels

CREATE INDEX ON tbl(fk_org); -- index voor foreign key
CREATE INDEX ON tbl(fk_cli); -- index voor foreign key

SELECT
  *
FROM
  tbl
WHERE
  (fk_org, fk_cli) = (1, 999); -- selectie op een specifieke paar

Recepten voor falende SQL-query's
[bekijk op explain.tensor.ru]

Oplossing:

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

Recepten voor falende SQL-query's
[bekijk op explain.tensor.ru]

Hier is de winst minder, omdat Bitmap Heap Scan vrij efficiënt op zichzelf is. Maar toch 7 keer sneller en 2,5 keer minder lezingen.

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

Wanneer er ontstaat

Toon de eerste 20 oudste "eigen" of niet toegewezen aanvragen voor verwerking, waarbij eigen de prioriteit heeft.

Hoe te herkennen

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

Aanbevelingen

Gebruik UNION [ALL] voor de combinatie van subquery's voor elk van de OR-blokken van voorwaarden.

Voorbeeld:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk  -- 100K "feiten"
, CASE
    WHEN random() < 1::real/16 THEN NULL -- met een kans van 1:16 een "gelijkspel"
    ELSE (random() * 100)::integer -- 100 verschillende externe sleutels
  END fk_own;

CREATE INDEX ON tbl(fk_own, pk); -- index met "blijkbaar passende" sortering

SELECT
  *
FROM
  tbl
WHERE
  fk_own = 1 OR -- eigen
  fk_own IS NULL -- ... of "gelijkspel"
ORDER BY
  pk
, (fk_own = 1) DESC -- eerst "eigen"
LIMIT 20;

Recepten voor falende SQL-query's
[bekijk op explain.tensor.ru]

Oplossing:

(
  SELECT
    *
  FROM
    tbl
  WHERE
    fk_own = 1 -- eerst "eigen" 20
  ORDER BY
    pk
  LIMIT 20
)
UNION ALL
(
  SELECT
    *
  FROM
    tbl
  WHERE
    fk_own IS NULL -- dan "gelijkspel" 20
  ORDER BY
    pk
  LIMIT 20
)
LIMIT 20; -- maar in totaal - 20, meer is niet nodig

Recepten voor falende SQL-query's
[bekijk op explain.tensor.ru]

We hebben gebruikgemaakt van het feit dat alle 20 benodigde records al in het eerste blok waren verkregen, waardoor het tweede blok met de meer "kostbare" Bitmap Heap Scan zelfs niet werd uitgevoerd - uiteindelijk 22 keer sneller, 44 keer minder lezingen!

Een gedetailleerder verhaal over deze optimalisatiemethode aan de hand van concrete voorbeelden kan worden gelezen in artikelen PostgreSQL-antipatronen: schadelijke JOIN en OR en PostgreSQL Antipatterns: een verhaal over iteratieve aanpassingen van de zoekfunctie op naam, of 'Optimalisatie heen en weer'.

Algemene variant van geordende selectie op meerdere sleutels (en niet alleen op het paar const/NULL) is besproken in het artikel SQL HowTo: een while-lus schrijven direct in de query, of 'Elementaire driewegkoppeling'.

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

Wanneer er ontstaat

Meestal ontstaat dit wanneer er een "extra filter" aan de bestaande query wil worden toegevoegd.

"Heeft u ook zoiets, maar met parelmoeren knopen?» film "De Diamanten Hand"

Bijvoorbeeld, door de eerdere taak aan te passen, toon de eerste 20 oudste "kritische" aanvragen voor verwerking, ongeacht hun toewijzing.

Hoe te herkennen

-> Seq Scan | Bitmap Heap Scan | Index [Only] Scan [Backward]
   && 5 × rijen 80% gelezen
   && loops × RRbF > 100 -- en tegelijkertijd meer dan 100 records in totaal

Aanbevelingen

Maak [meer] gespecialiseerde index met WHERE-voorwaarde of extra fields in the index.

If the filtering condition is "static" for your tasks — that is, does not imply expansion of the list of values in the future — it is better to use a WHERE index. Different boolean/enum statuses fit well into this category.

If the filtering condition can take different values,, then it is better to expand the index with these fields — as in the situation with BitmapAnd above.

Voorbeeld:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk -- 100K "facts"
, CASE
    WHEN random() < 1::real/16 THEN NULL
    ELSE (random() * 100)::integer -- 100 different foreign keys
  END fk_own
, (random() < 1::real/50) critical; -- 1:50, that the request is "critical"

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

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

Recepten voor falende SQL-query's
[bekijk op explain.tensor.ru]

Oplossing:

CREATE INDEX ON tbl(pk)
  WHERE critical; -- added a "static" filtering condition

Recepten voor falende SQL-query's
[bekijk op explain.tensor.ru]

As we can see, the filtering has completely disappeared from the plan, and the query has become 5 times faster.

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

Wanneer er ontstaat

Various attempts to create a custom task processing queue, when a large number of updates/deletions of records in the table result in a situation with a large number of "dead" records.

Hoe te herkennen

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

Aanbevelingen

Regularly perform manually VACUUM [FULL] or achieve adequately frequent operation autovacuum by finely tuning its parameters, including for a specific table.

In most cases, such problems are caused by poor query assembly in calls from business logic like those discussed in PostgreSQL Antipatterns: vechten tegen de legers van 'doden'.

But it should be understood that even VACUUM FULL may not always help. For such cases, it's worth reviewing the algorithm from the article DBA: when VACUUM fails — clean the table manually.

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

Wanneer er ontstaat

It seems that we've read a little, everything is indexed, and no unnecessary filters were applied — yet significantly more pages were read than desired.

Hoe te herkennen

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

Aanbevelingen

Carefully examine the structure of the index used and the key fields defined in the query — most likely, part of the index is not defined.Most likely, you will need to create a similar index, but without prefix fields or learn to iterate their values..

Voorbeeld:

MAAK TABEL tbl AS
SELECT
  generate_series(1, 100000) pk      -- 100K "feiten"
, (random() *  100)::integer fk_org  -- 100 verschillende externe sleutels
, (random() * 1000)::integer fk_cli; -- 1K verschillende externe sleutels

MAAK INDEX OP tbl(fk_org, fk_cli); -- bijna alles zoals in #2
-- alleen hebben we de aparte index op fk_cli als overbodig beschouwt en verwijderd

SELECT
  *
FROM
  tbl
WHERE
  fk_cli = 999 -- en fk_org is niet opgegeven, hoewel het eerder in de index staat
LIMIT 20;

Recepten voor falende SQL-query's
[bekijk op explain.tensor.ru]

Alles lijkt goed, zelfs met de index, maar het is een beetje verdacht — voor elk van de 20 gelezen records moesten we 4 pagina's gegevens doorlezen, 32KB per record — is dat niet teveel? En de naam van de index tbl_fk_org_fk_cli_idx roept vragen op.

Oplossing:

MAAK INDEX OP tbl(fk_cli);

Recepten voor falende SQL-query's
[bekijk op explain.tensor.ru]

Plotseling — 10 keer sneller, en 4 keer minder te lezen!

Andere voorbeelden van inefficiënt gebruik van indexen zijn te zien in het artikel DBA: we vinden nutteloze indexen.

#7: CTE × CTE

Wanneer er ontstaat

In de query hebben we "dikke" CTE's verzameld uit verschillende tabellen, en toen besloten we om ze tussen elkaar te maken JOIN.

Deze case is relevant voor versies onder v12 of queries met MET MATERIALIZED.

Hoe te herkennen

-> CTE Scan
   && loops > 10
   && loops × (rows + RRbF) > 10000
      -- te grote cartesiaanse product CTE

Aanbevelingen

Analyseer de query zorgvuldig — en zijn CTE's hier überhaupt nodig? Если все-таки да, то toepassen van "woordenboeken" in hstore/json volgens het model beschreven in PostgreSQL Antipatterns: laten we de woordenboek op zware JOINs gebruiken..

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

Wanneer er ontstaat

Een eenmalige verwerking (sorteren of uniek maken) van een groot aantal records past niet in het daarvoor toegewezen geheugen.

Hoe te herkennen

-> *
   && temp written > 0

Aanbevelingen

Als de hoeveelheid geheugen die door de operatie is gebruikt niet veel hoger is dan de ingestelde waarde van de parameter work_mem, is het verstandig om deze aan te passen. Dit kan meteen in de configuratie voor iedereen, of via SET [LOCAL] voor een specifieke query/transactie.

Voorbeeld:

TOON work_mem;
-- "16MB"

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

Recepten voor falende SQL-query's
[bekijk op explain.tensor.ru]

Oplossing:

SET work_mem = '128MB'; -- vóór het uitvoeren van de query

Recepten voor falende SQL-query's
[bekijk op explain.tensor.ru]

Om begrijpelijke redenen, als alleen geheugen wordt gebruikt en geen schijf, zal de query veel sneller worden uitgevoerd. Daarnaast wordt ook een deel van de belasting van de HDD verminderd.

Maar het moet duidelijk zijn dat je niet eenvoudigweg veel geheugen kunt toewijzen — er is simpelweg niet genoeg voor iedereen.

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

Wanneer er ontstaat

Er is veel tegelijk in de database geladen, maar er was geen tijd om ANALYZE.

Hoe te herkennen

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

Aanbevelingen

Ongetwijfeld ANALYZE.

Deze situatie wordt uitgebreider besproken in PostgreSQL Antipatterns: statistiek is alles.

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

Wanneer er ontstaat

Er was een blokkering die werd verwacht, opgelegd door een concurrerende query, of er waren niet genoeg hardwarebronnen beschikbaar voor CPU/hypervisor.

Hoe te herkennen

-> *
   && (gedeeld hit / 8K) + (gedeeld lezen / 1K)  100ms -- we hebben weinig gelezen, maar te lang

Aanbevelingen

Gebruik een externe systeem voor monitoring van servers om blokkades of onregelmatig middelenverbruik te detecteren. Over onze aanpak voor het organiseren van dit proces voor honderden servers hebben we al verteld. here en here.

Recepten voor falende SQL-query's
Recepten voor falende SQL-query's

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster