PostgreSQL Antipatterns: berÀttelsen om iterativ förbÀttring av sökningen efter namn, eller "Optimering hit och dit"

Tusentals chefer frĂ„n försĂ€ljningskontor över hela landet registrerar i vĂ„rt CRM-system dagligen tiotusentals kontakter — kommunikationer med potentiella eller redan befintliga kunder. För att nĂ„ denna kund mĂ„ste man först hitta den, och gĂ€rna vĂ€ldigt snabbt. Och detta sker oftast genom namnet.

DĂ€rför Ă€r det inte förvĂ„nande att nĂ€r jag pĂ„ nytt analyserade "tunga" förfrĂ„gningar i en av de mest belastade databaserna — vĂ„rt eget företagskonto Sbis, sĂ„ upptĂ€ckte jag "i toppen" en förfrĂ„gan för "snabb" sökning efter namn för organisationskort.

Vidare undersökning avslöjade ett intressant exempel av först optimering och sedan nedgradering av prestandan förfrÄgan vid successiv utveckling av flera team, dÀr var och en agerade utifrÄn de bÀsta intentionerna.

0: vad ville anvÀndaren

PostgreSQL Antipatterns: berÀttelsen om iterativ förbÀttring av sökningen efter namn, eller "Optimering hit och dit"[KDPV hÀrifrÄn]

Vad menar anvĂ€ndaren vanligtvis nĂ€r han pratar om "snabb" sökning efter namn? Det visar sig nĂ€stan aldrig vara "Ă€rlig" sökning efter en del av en strĂ€ng sĂ„som ... LIKE '%ros%' — för dĂ„ fĂ„r man inte bara 'Rosalia' och 'Butik Ros'och Ă€ven 'Gros' och Ă€ven 'Farbror Moros'.

AnvĂ€ndaren menar pĂ„ ett vardagligt plan att ni ska ge honom sökning efter början av ordet i namnet och visa mer relevanta resultat som börjar med det inmatade. Och göra detta praktiskt taget omedelbart — vid delvis inmatning.

1: avgrÀnsa uppgiften

Och framför allt kommer ingen mĂ€nniska att sĂ€rskilt skriva 'ros butik', för att ni ska behöva söka prefixbaserat pĂ„ varje ord. Nej, det Ă€r mycket enklare för anvĂ€ndaren att reagera pĂ„ en snabb förslag för det sista ordet Ă€n att medvetet "inte slutföra" de föregĂ„ende — se hur detta fungerar i vilket sökmotor som helst.

Överhuvudtaget, correctly att formulera kraven för uppgiften Ă€r mer Ă€n hĂ€lften av lösningen. Ibland kan noggrant analys av anvĂ€ndningsfall ha en betydande pĂ„verkan pĂ„ resultatet..

Vad gör dÄ en abstrakt utvecklare?

1.0: extern sökmotor

Oj, sökning Ă€r svĂ„rt, jag kĂ€nner inte för att syssla med det alls — lĂ„t oss ge detta till devops! LĂ„t dem installera en extern söksystem relativt databasen: Sphinx, ElasticSearch,


Ett arbetskrĂ€vande, om Ă€n arbetsintensivt, alternativ för synkronisering och snabba Ă€ndringar. Men inte i vĂ„rt fall, eftersom sökningen görs för varje kund endast inom ramen för deras konto. Data har en ganska hög förĂ€ndringshastighet – och om en manager just har lagt till ett kort 'Butik Rosa', kan han efter 5-10 sekunder redan komma ihĂ„g att han glömde ange dĂ€r e-postadressen och vilja hitta och rĂ€tta till det.

SĂ„ lĂ„t oss söka "direkt i databasen". Lyckligtvis tillĂ„ter PostgreSQL oss att göra detta, och inte pĂ„ bara ett sĂ€tt — vi kommer att se pĂ„ dem.

1.1: "Àrlig" delstrÀng

Vi fÀster oss vid ordet "delstrÀng". För exakt indexsökning av delstrÀngar (och till och med med reguljÀra uttryck!) finns det ett utmÀrkt modul pg_trgm! Men först mÄste vi sortera rÀtt.

LÄt oss pröva att anvÀnda en sÄdan tabell för enkelhetens skull:

CREATE TABLE firms(
  id
    serial
      PRIMARY KEY
, name
    text
);

Vi laddar upp 7,8 miljoner poster av verkliga organisationer och indexerar:

CREATE EXTENSION pg_trgm;
CREATE INDEX ON firms USING gin(lower(name) gin_trgm_ops);

LÄt oss söka efter de första 10 posterna för delstrÀngssökning:

SELECT
  *
FROM
  firms
WHERE
  lower(name) ~ ('(^|s)' || 'rosa')
ORDER BY
  lower(name) ~ ('^' || 'rosa') DESC -- börjar med "
, lower(name) -- resten alfabetiskt
LIMIT 10;

PostgreSQL Antipatterns: berÀttelsen om iterativ förbÀttring av sökningen efter namn, eller "Optimering hit och dit"
[titta pÄ explain.tensor.ru]

NĂ„vĂ€l, sĂ„dĂ€r.. 26ms, 31MB mĂ€ngd lĂ€sta data och mer Ă€n 1,7K filtrerade poster — för 10 sökta. Överhettningar Ă€r alldeles för stora, finns det nĂ„got mer effektivt sĂ€tt?

1.2: söka i text? Det Àr ju FTS!

Verkligen, PostgreSQL erbjuder en mycket kraftfull fulltextsökningsmekanism (Full Text Search), inklusive möjligheter för prefixsökning. Ett utmÀrkt alternativ, Àven utan att installera tillÀgg! LÄt oss pröva:

CREATE INDEX ON firms USING gin(to_tsvector('simple'::regconfig, lower(name)));

SELECT
  *
FROM
  firms
WHERE
  to_tsvector('simple'::regconfig, lower(name)) @@ to_tsquery('simple', 'rosa:*')
ORDER BY
  lower(name) ~ ('^' || 'rosa') DESC
, lower(name)
LIMIT 10;

PostgreSQL Antipatterns: berÀttelsen om iterativ förbÀttring av sökningen efter namn, eller "Optimering hit och dit"
[titta pÄ explain.tensor.ru]

HĂ€r fick vi lite hjĂ€lp av parallellisering av frĂ„geexekvering som halverade tiden till 11ms. Och vi behövde lĂ€sa 1,5 gĂ„nger mindre — bara 20MB. HĂ€r gĂ€ller det att ju mindre desto bĂ€ttre, eftersom ju större volym vi lĂ€ser, desto större blir chansen för cache miss, och varje överflödig lĂ€sning frĂ„n disk Ă€r potentiella "trögheter" för förfrĂ„gan.

1.3: hur Àr det med LIKE?

Den föregÄende frÄgan Àr bra, men om vi kör den hundra tusen gÄnger pÄ en dag, sÄ samlas det upp 2TB data som har lÀsts. I bÀsta fall frÄn minnet, men om vi har otur, Àven frÄn disken. SÄ lÄt oss försöka göra det mindre.

LÄt oss komma ihÄg att anvÀndaren vill se först "som börjar med ...". Det Àr ju rent av prefixsökning med hjÀlp av text_pattern_ops! Och bara om vi "inte har tillrÀckligt" med 10 sökresultat, dÄ mÄste vi lÀsa dem med hjÀlp av FTS-sökning:

CREATE INDEX ON firms(lower(name) text_pattern_ops);

SELECT
  *
FROM
  firms
WHERE
  lower(name) LIKE ('rosa' || '%')
LIMIT 10;

PostgreSQL Antipatterns: berÀttelsen om iterativ förbÀttring av sökningen efter namn, eller "Optimering hit och dit"
[titta pÄ explain.tensor.ru]

UtmĂ€rkta resultat — totalt 0.05 ms och strax över 100 KB lĂ€sta! Men vi glömde ju sorteringen efter namn, för att anvĂ€ndaren inte ska gĂ„ vilse i resultaten:

SELECT
  *
FROM
  firms
WHERE
  lower(name) LIKE ('rosa' || '%')
ORDER BY
  lower(name)
LIMIT 10;

PostgreSQL Antipatterns: berÀttelsen om iterativ förbÀttring av sökningen efter namn, eller "Optimering hit och dit"
[titta pÄ explain.tensor.ru]

Oj, nĂ„got ser redan inte sĂ„ vackert ut — det verkar som att indexet finns, men sorteringen gĂ„r förbi det
 Visst Ă€r det betydligt effektivare Ă€n den tidigare versionen, men


1.4: "finputsa med en fil"

Men det finns ett index som tillĂ„ter bĂ„de sökning inom ett intervall och att sorteringen fungerar normalt — vanlig btree!

CREATE INDEX ON firms(lower(name));

Men frÄgan för det mÄste "byggas manuellt":

SELECT
  *
FROM
  firms
WHERE
  lower(name) >= 'rosa' AND
  lower(name) <= ('rosa' || chr(65535)) -- för UTF8, för ensuratiga - chr(255)
ORDER BY
   lower(name)
LIMIT 10;

PostgreSQL Antipatterns: berÀttelsen om iterativ förbÀttring av sökningen efter namn, eller "Optimering hit och dit"
[titta pÄ explain.tensor.ru]

UtmĂ€rkt — bĂ„de sorteringen fungerar, och resursanvĂ€ndningen förblir "mikroskopisk", tusentals gĂ„nger effektivare Ă€n "ren" FTS! Kvar Ă€r att samla allt till en enda frĂ„ga:

(
  SELECT
    *
  FROM
    firms
  WHERE
    lower(name) >= 'rosa' AND
    lower(name) <= ('rosa' || chr(65535)) -- för UTF8, för ensuratiga kodningar - chr(255)
  ORDER BY
     lower(name)
  LIMIT 10
)
UNION ALL
(
  SELECT
    *
  FROM
    firms
  WHERE
    to_tsvector('simple'::regconfig, lower(name)) @@ to_tsquery('simple', 'rosa:*') AND
    lower(name) NOT LIKE ('rosa' || '%') -- "som börjar med" har vi redan funnit ovan
  ORDER BY
    lower(name) ~ ('^' || 'rosa') DESC -- vi anvÀnder samma sortering för att INTE gÄ via btree-indexet
  , lower(name)
  LIMIT 10
)
LIMIT 10;

Jag noterar att den andra underfrÄgan utförs bara om den första returnerade mindre Àn förvÀntat sista LIMIT antalet rader. Om denna typ av optimering för frÄgor har jag redan skrivit tidigare.

SÄ ja, vi har nu bÄde btree och gin pÄ tabellen, men statistiskt har det blivit sÄ att mindre Àn 10% av förfrÄgningarna gÄr till den andra blocket. Det innebÀr att med dessa kÀnda typiska begrÀnsningar för uppgiften har vi lyckats minska den totala resursanvÀndningen pÄ servern praktiskt taget tusen gÄnger!

1.5*: vi klara oss utan filen

Ovanför LIKE Vi stötte pÄ problem med att anvÀnda fel sortering. Men den kan "ledas pÄ rÀtt vÀg" med hjÀlp av USING-operatören:

Som standard antas ASC. Dessutom kan man ange namnet pÄ en specifik sorteringsoperator i satsen USING. Sorteringsoperatören mÄste vara en del av "mindre Àn" eller "större Àn" i ett visst familj av B-trÀdoperatörer. ASC vanligtvis motsvarar USING < och DESC vanligtvis motsvarar USING >.

I vÄrt fall Àr "mindre Àn" det ~<~:

SELECT
  *
FROM
  firms
WHERE
  lower(name) LIKE ('ros' || '%')
ORDER BY
  lower(name) USING ~<~
LIMIT 10;

PostgreSQL Antipatterns: berÀttelsen om iterativ förbÀttring av sökningen efter namn, eller "Optimering hit och dit"
[titta pÄ explain.tensor.ru]

2: hur frÄgor "fermenterar"

Nu lĂ„ter vi vĂ„r frĂ„ga "sĂ€tta sig" i ett halvt Ă„r till ett Ă„r, och med förvĂ„ning upptĂ€cker vi den igen "i toppen" med siffror för det totala dagliga "minnespumpandet" (buffers shared hit) i 5.5TB — det vill sĂ€ga Ă€nnu mer Ă€n vad som ursprungligen fanns.

Nej, naturligtvis, och vĂ„r verksamhet har vuxit och belastningen har ökat, men inte sĂ„ mycket! Det betyder att det Ă€r nĂ„got som inte stĂ€mmer — lĂ„t oss reda ut det.

2.1: födelse av paging

Vid en viss tidpunkt ville ett annat utvecklingsteam möjliggöra att frÄn snabb undermeny-sökning "hoppa" till registret med samma, men utvidgade resultat. Och vilket register har ingen sidnavigation? LÄt oss koppla pÄ det!

( ... LIMIT  + 10)
UNION ALL
( ... LIMIT  + 10)
LIMIT 10 OFFSET ;

Nu kunde man utan anstrÀngning för utvecklaren visa resultatregistret med "typ-sidvis" inlÀsning.

SjĂ€lvklart, i verkligheten, för varje nĂ€sta datorsida lĂ€ses allt mer och mer (allt frĂ„n föregĂ„ende gĂ„ng, som vi slĂ€nger bort, plus den behövda "svansen") — det vill sĂ€ga detta Ă€r ett tydligt antipattern. Det skulle vara bĂ€ttre att starta sökningen pĂ„ nĂ€sta iteration frĂ„n den som kom ihĂ„g i grĂ€nssnittet, men om det — en annan gĂ„ng.

2.2: vill ha exotiska inslag

Vid ett visst tillfÀlle ville utvecklaren variera det resulterande urvalet med data frÄn en annan tabell, för vilket hela den föregÄende frÄgan skickades till CTE:

WITH q AS (
  ...
  LIMIT  + 10
)
SELECT
  *
, (SELECT ...) sub_query -- nÄgon frÄga till den relaterade tabellen
FROM
  q
LIMIT 10 OFFSET ;

Och Ă€ven sĂ„ — inte dĂ„ligt, eftersom den inbĂ€ddade frĂ„gan berĂ€knas endast för 10 returnerade poster, om det inte vore för...

2.3: DISTINCT meningslös och skoningslös

NĂ„gonstans i processen av sĂ„dan evolution förlorades NOT LIKE villkor . Det Ă€r tydligt att efter dettabörjade Ă„tervĂ€nda UNION ALL vissa poster tvĂ„ gĂ„nger tillbaka. — först de som hittades i början av strĂ€ngen, och sedan igen — vid början av det första ordet i denna strĂ€ng. I grĂ€nsen kan alla poster i den andra underfrĂ„gan matcha posterna i den första.

Vad gör utvecklaren istÀllet för att hitta orsaken?.. Ingen frÄga!

  • vi dubblerar storleken ursprungliga urval
  • vi anvĂ€nde DISTINCT, sĂ„ att endast unika förekomster av varje rad skulle Ă„terges

WITH q AS (
  ( ... LIMIT  + 10)
  UNION ALL
  ( ... LIMIT  + 10)
  LIMIT  + 10
)
SELECT DISTINCT
  *
, (SELECT ...) sub_query
FROM
  q
LIMIT 10 OFFSET ;

Det Àr tydligt att resultatet i slutÀndan Àr detsamma, men risken att "misslyckas" i den andra underfrÄgan CTE har ökat betydligt, och utan detta, det lÀses uppenbarligen mer.

Men det Ă€r inte det största problemet. Eftersom utvecklaren bad om att vĂ€lja DISTINCT inte efter specifika, utan genast efter alla fĂ€lt poster, sĂ„ kom fĂ€ltet sub_query — resultatet av underfrĂ„gan automatiskt med. DISTINCTNu tvingades databasen att utföra inte 10 underfrĂ„gor, utan alla + 10!

2.4: samarbete Àr allt!

SĂ„ hĂ€r levde utvecklarna — utan bekymmer, eftersom det uppenbarligen inte fanns tillrĂ€ckligt med tĂ„lamod i registret för att "justera" till betydande vĂ€rden N vid det kroniska fördröjningen av att fĂ„ varje nĂ€sta "sida" för anvĂ€ndaren.

Tills utvecklarna frĂ„n en annan avdelning kom till dem och ville anvĂ€nda en sĂ„ praktisk metod för iterativ sökning — det vill sĂ€ga, vi tar en bit av ett urval, filtrerar efter ytterligare villkor, ritar resultatet, sedan nĂ€sta bit (vilket i vĂ„rt fall uppnĂ„s genom att öka N), och sĂ„ fortsĂ€tter vi tills skĂ€rmen Ă€r full.

Sammanfattningsvis, i det fĂ„ngade exemplet N nĂ„dde vĂ€rden pĂ„ nĂ€stan 17K, och totalt genomfördes minst 4K sĂ„dana förfrĂ„gningar "i kedja" under en dag. De senaste av dem skannades redan med 1GB minne vid varje iteration


Sammanfattningsvis

PostgreSQL Antipatterns: berÀttelsen om iterativ förbÀttring av sökningen efter namn, eller "Optimering hit och dit"

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster