PostgreSQL Antipatterns: Navigera i registret

Idag blir det inga komplexa fall eller snĂ„riga algoritmer i SQL. Allt kommer att vara vĂ€ldigt enkelt, pĂ„ nivĂ„ med Kapten Upplysning — vi gör en visning av hĂ€ndelseregistret med sortering efter tid.

Det vill sĂ€ga, hĂ€r har vi en tabell i databasen events, och den har ett fĂ€lt ts — exakt den tid vi vill visa dessa poster ordnat:

CREATE TABLE events(
  id
    serial
      PRIMARY KEY
, ts
    timestamp
, data
    json
);

CREATE INDEX ON events(ts DESC);

SjÀlvklart kommer det att finnas fler Àn ett dussin poster, sÄ vi behöver pÄ nÄgot sÀtt sida navigering.

#0. «Я у ĐŒĐ°ĐŒŃ‹ ĐżĐŸĐłŃ€ĐŸĐŒĐŒĐžŃŃ‚Â»

cur.execute("SELECT * FROM events;")
rows = cur.fetchall();
rows.sort(key=lambda row: row.ts, reverse=True);
limit = 26
print(rows[offset:offset+limit]);

Det Ă€r nĂ€stan ingen skĂ€mt — det dyker sĂ€llan upp i det vilda. Ibland kan det vara svĂ„rt att stĂ€lla om frĂ„n ORM till "direkt" arbete med SQL.

Men lÄt oss gÄ vidare till mer vanliga och mindre uppenbara problem.

#1. OFFSET

SELECT
  ...
FROM
  events
ORDER BY
  ts DESC
LIMIT 26 OFFSET $1; -- 26 - poster pÄ sidan, $1 - början pÄ sidan

Var kom siffran 26 ifrÄn? Det Àr den ungefÀrliga mÀngden poster för att fylla en skÀrm. NÀrmare bestÀmt 25 visade poster, plus 1, som signalerar att det fortfarande finns nÄgot mer i urvalet och att det Àr vÀrt att gÄ vidare.

SjĂ€lvklart kan detta vĂ€rde inte "bakas in" i frĂ„gan, utan överföras via en parameter. Men i sĂ„ fall kan PostgreSQL:s planering inte lita pĂ„ att det kommer att finnas fĂ„ poster — och kan lĂ€tt vĂ€lja en ineffektiv plan.

Och sĂ„ lĂ€nge visningen av hĂ€ndelseregistret i applikationen Ă€r implementerad som en vĂ€xling mellan visuella "sidor", lĂ€gger ingen lĂ€nge mĂ€rke till nĂ„got misstĂ€nkt. Fram till dess att man i jakten pĂ„ anvĂ€ndarvĂ€nlighet UI/UX bestĂ€mmer sig för att göra om grĂ€nssnittet till "oĂ€ndlig scroll" — det vill sĂ€ga alla registreringsposter visas som en lista som anvĂ€ndaren kan scrolla upp och ner.

Och nu, vid nÀsta testning fÄngas du pÄ duplicering av poster i registret. Varför, eftersom det finns ett normalt index i tabellen (ts), som din frÄga förlitar sig pÄ?

Exakt dĂ€rför att du inte tog hĂ€nsyn till att ts inte Ă€r en unik nyckel i denna tabell. I sjĂ€lva verket Ă€r vĂ€rdena inte unika, som med all "tid" i verkliga förhĂ„llanden — dĂ€rför kan samma post i tvĂ„ angrĂ€nsande förfrĂ„gningar lĂ€tt "hoppa" frĂ„n sida till sida pĂ„ grund av en annan slutlig ordning i sorteringen av samma nyckelvĂ€rdesresultat.

Faktum Ă€r att hĂ€r döljer sig Ă€ven ett andra problem, som Ă€r mycket svĂ„rare att upptĂ€cka — vissa poster kommer inte att visas överhuvudtaget! Eftersom de "dubbelnoterade" posterna tog upp nĂ„gon annans plats. En detaljerad förklaring med fina bilder kan lĂ€sas hĂ€r.

Utöka indexet

Den smarta utvecklaren förstĂ„r — man mĂ„ste göra indexnyckeln unik, och det enklaste sĂ€ttet Ă€r att utöka den med ett medvetet unikt fĂ€lt, dĂ€r primĂ€rnyckel (PK) passar utmĂ€rkt:

SKAPA UNIKT INDEX PÅ events(ts DESC, id DESC);

Och frÄgan muterar:

VÄLJ
  ...
ORDER BY
  ts DESC, id DESC
LIMIT 26 OFFSET $1;

#2. ĐŸĐ”Ń€Đ”Ń…ĐŸĐŽ ĐœĐ° «ĐșŃƒŃ€ŃĐŸŃ€Ń‹Â»

Ett tag senare kommer DBA:en till dig och "glÀder" dig med att dina frÄgor trycker ner servern med sina förskrÀckliga OFFSET, och överlag Àr det dags att övergÄ till navigering frÄn det senaste visade vÀrdet. Din frÄga muterar igen:

VÄLJ
  ...
DÄR
  (ts, id) < ($1, $2) -- senaste mottagna vÀrden frÄn föregÄende steg
ORDER BY
  ts DESC, id DESC
LIMIT 26;

Du suckade lÀttat, tills det hÀnde...

#3. ЧостĐșа ĐžĐœĐŽĐ”ĐșŃĐŸĐČ

För en dag lĂ€ste din DBA en artikel om ineffektiva index och insĂ„g att "inte sista" tidsstĂ€mpel — det Ă€r inte bra. Och han kom Ă„ter till dig — den hĂ€r gĂ„ngen med tanken att det indexet verkligen borde förvandlas tillbaka till (ts DESC).

Men vad ska man göra med det ursprungliga problemet med "hoppen" mellan sidorna?.. Jo, allt Ă€r enkelt — man mĂ„ste vĂ€lja block med obestĂ€mt antal poster!

Vem förbjuder oss att lĂ€sa inte "exakt 26", utan "minst 26"? Till exempel sĂ„ att i nĂ€sta block hamnar poster med medvetet andra vĂ€rden ts — dĂ„ kommer det inte att finnas nĂ„got problem med "hoppen" mellan blocken!

SÄ hÀr gör man för att uppnÄ det:

VÄLJ
  ...
DÄR
  ts = coalesce(
    VÄLJ
      ts
    FRÅN
      events
    DÄR
      ts < $1
    ORDER BY
      ts DESC
    LIMIT 1 OFFSET 25
  ), '-oÀndlighet')
ORDER BY
  ts DESC;

Vad hÀnder hÀr överhuvudtaget?

  1. Vi kliver ner 25 poster och fÄr det "grÀnsvÀrde" ts.
  2. Om det inte finns nÄgot dÀr, byter vi NULL-vÀrdet mot -oÀndlighet.
  3. Vi berÀknar hela segmentet av vÀrden mellan det erhÄllna vÀrdet ts och parametern $1 som skickats frÄn grÀnssnittet (det tidigare "senaste" renderade vÀrdet).
  4. Om blocket har fĂ€rre Ă€n 26 poster — Ă€r det det sista.

Eller samma sak med en bild:
PostgreSQL Antipatterns: Navigera i registret

Eftersom vi nu har urvalet har inget specifikt "början", hindrar inget oss frĂ„n att "utveckla" denna frĂ„ga bakĂ„t och genomföra dynamisk inlĂ€sning av datablokker frĂ„n "referenspunkt" i bĂ„da riktningar — bĂ„de nedĂ„t och uppĂ„t.

AnmÀrkning

  1. Ja, i sÄ fall kontaktar vi indexet tvÄ gÄnger, men allt sker "rent enligt index". DÀrför kommer den inbÀddade frÄgan att leda till endast en extra Index Only Scan.
  2. Det Àr ganska uppenbart att denna metodik bara kan anvÀndas nÀr ni har vÀrden ts kan korsa varandra endast av en slump, och det Àr inte mÄnga. Men om ert typiska fall Àr "en miljon poster pÄ 00:00:00.000", bör ni inte göra sÄ. Med andra ord, ni bör inte tillÄta ett sÄdant fall. Men om det nu har blivit sÄ, anvÀnd alternativet med det utvidgade indexet.

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