DBA: korraldame sĂŒnergiate ja importide haldamise

Suure andmehulkade keerulise töötlemise korral (erinevad ETL-protsessid: impordid, konvertimised ja sĂŒnkroonimised vĂ€lise allikaga) tekib sageli vajadus ajutiselt "mĂ€letada" ja kohe kiiresti töödelda midagi mahukat.

Kliiniline ĂŒlesanne sellistel juhtudel kĂ”lab tavaliselt umbes nii: "Siin raamatupidamine ekstrakteeris kliendipangast viimased sisse laekunud maksed, tuleb need kiiresti saidile ĂŒles laadida ja siduda kontodega"

Kuid kui selle "midagi" maht hakkab mÔÔtma sadu megabaite ning teenus peab sellegipoolest töötama andmebaasiga reĆŸiimis 24×7, siis tekib palju kĂ”rvalmĂ”jusid, mis rikuvad teie elu.
DBA: korraldame sĂŒnergiate ja importide haldamise
Et nendega toime tulla PostgreSQL-is (ja mitte ainult seal), saab kasutada teatud optimeerimise vÔimalusi, mis vÔimaldavad kÔik kiiremini töödelda ja vÀhem ressursse tarbida.

1. Kuhu laadida?

Esiteks, mÀÀratleme, kuhu me saame laadida andmeid, mida me tahame "töödelda".

1.1. Ajutised tabelid (TEMPORARY TABLE)

PÔhimÔtteliselt on PostgreSQL-is ajutised tabelid samasugused tabelid nagu kÔik teised. Seega on vale arvamus, et «seal kÔik on ainult mÀlus, ja see vÔib otsa lÔppeda». Kuid on ka mÔned olulised erinevused.

Iga andmebaasiĂŒhenduse jaoks oma «nimetust»

Kui kaks ĂŒhendust ĂŒritavad samaaegselt teostada CREATE TABLE x, siis keegi peab kindlasti saama unikaalsuse viga andmebaasiobjektide osas.

Aga kui mĂ”lemad ĂŒritavad teostada CREATE AJUTINE TABEL x, siis mĂ”lemad teevad seda normaalselt, ja igaĂŒks saab oma eksemplari tabelist. Ja neil ei ole omavahel midagi ĂŒhist.

«Enesetapu» disconnecti puhul

Ühenduse sulgemisel kustutatakse kĂ”ik ajutised tabelid automaatselt, seega pole mĂ”tet «kĂ€sitsi» teostada DROP TABLE x vĂ€lja arvatud


Kui te töötate pgbounceri tehingureĆŸiimis, siis andmebaas arvab ikkagi, et see ĂŒhendus on endiselt aktiivne, ja selles ajutises tabelis on endiselt olemas.

Seega toob selle uuesti loomine, juba teisest pgbounceri ĂŒhendusest, kaasa veateate. Kuid seda saab ringiga minna, kasutades LOO TEMPORARY TABLE KUI EI OLE x.

TĂ”si, parem oleks seda siiski mitte teha, kuna vĂ”id hiljem „jĂ€rsku“ avastada seal endiselt „eelmise omaniku“ andmed. Selle asemel on palju parem lugeda juhendit ja nĂ€ha, et tabeli loomisel on vĂ”imalik juurde kirjutada. KOHUSTUSEGA DROP — see tĂ€hendab, et tehingu lĂ”petamisel tabel eemaldatakse automaatselt.

Mitte-replikatsioon

Ajutiste tabelite replikatsiooni ei toimu, kuna need kuuluvad ainult kindlale ĂŒhendusele. Kuid see vabastab vajadusest andmete kahekordse salvestamise jĂ€rele heap + WAL, seetĂ”ttu on INSERT/UPDATE/DELETE selles tunduvalt kiirem.

Kuna ajutine tabel on siiski „peaaegu tavaline“ tabel, ei saa seda ka replikatsiooni luua. VĂ€hemalt praegu, kuigi vastav patch on juba ammu olemas.

1.2. Mitte-logitud tabelid (UNLOGGED TABLE)

Aga mis siis, kui teil on mĂ”ni mahukas ETL-protsess, mida ei Ă”nnestu teostada ĂŒhe tehingu raames, ja teil on tegelikult pgbounceri tehingureĆŸiimis?..

VĂ”i andmevoog on nii suur, et ĂŒhe ĂŒhenduse lĂ€bilaskevĂ”ime ei piisa andmebaasi (loe: ĂŒhe CPU protsessi)?..

VĂ”i osa operatsioonidest toimub asĂŒnkroonselt erinevates ĂŒhendustes?..

Siin on vaid ĂŒks vĂ”imalus — ajutiselt luua mitte-ajutisi tabeleid. MĂ€nguline vĂ€ljend, eks. See tĂ€hendab:

  • loodud „oma” tabelitega maksimaalselt juhuslike nimedega, et mitte kellegagi kokku puutuda
  • Ekstraheerimine: laaditi neisse andmed vĂ€lisest allikast
  • Transformeerimine: muundatud, tĂ€ideti vĂ”tme sidumise vĂ€ljad
  • Load: valmitud andmed kanti sihttabelitesse
  • kustutas „oma” tabelid

Aga nĂŒĂŒd — tĂ”rvatilk. Tegelikult, kogu salvestus PostgreSQL-is toimub kahel korral — esmakordselt WAL-is, seejĂ€rel juba tabelite/indeksite kehad. KĂ”ik see on tehtud ACID toe ja andmete korrektse nĂ€htavuse toetamiseks COMMIT‘sisenenud ja RULLI TAGASI‘sisenenud tehingute vahel.

Aga me ei vaja seda! Meie kogu protsess kas toimis tĂ€iesti vĂ”i mitte.Pole tĂ€htis, kui palju keskseid tehinguid selle sees on — meid ei huvita „protsessi jĂ€tkamine keskelt”, eriti kui ei ole selge, kus see oli.

Selle jaoks tutvustasid PostgreSQL arendajad juba versioonis 9.1 sellist asja nagu mittelogitud (UNLOGGED) tabelid:

Selle juhisega luuakse tabel mittelogitud tabelina. Mittelogitud tabelitesse kirjutatavad andmed ei lĂ€bi eeljĂ€lgimislogi (vt peatĂŒkk 29), mille tulemuseks on sellised tabelid töötavad palju kiiremini kui tavaliselt. Kuid nad ei ole kaitstud rike eest; kui server kukub vĂ”i selline katkestus toimub, jÀÀb logimata tabel automaatiliselt lĂŒhendatud. Lisaks sellele, logimata tabeli sisu ei replitseerita aluselserveritesse. KĂ”ik indeksid, mis luuakse logimata tabeli jaoks, muutuvad automaatselt logimata.

LĂŒhidalt, on see palju kiirem, kuid kui andmebaasi server „kukub” – vĂ”ib see olla ebameeldiv. Kuid kui sageli see juhtub ja kas teie ETL-protsess suudab seda pĂ€rast andmebaasi „elustamist” Ă”igesti kohandada?...

Kui ei, ja ĂŒlaltoodud juhtum sarnaneb teie omaga – kasutage UNLOGGED, kuid Ă€rge kandke seda attribuuti tĂ”elistele tabelitele, mille andmed on teile kallid.

1.3. ON COMMIT { DELETE ROWS | DROP }

Seda konstruktsiooni saab tabeli loomisel kasutada automaatse kÀitumise mÀÀramiseks tehingu lÔpetamisel.

Umbes KOHUSTUSEGA DROP nagu ma varem mainisin, genereerib ta DROP TABLE, kuid siin on KOHUSTUSEGA KUSTUTA RIDADE huvitavam olukord — siinkohal genereeritakse TRUNCATE TABLE.

Kuna kogu ajutise tabeli metaandmete salvestamise infrastruktuur on tĂ€pselt sama, mis tavalisel tabelil, siis ajutiste tabelite pidev loomine ja kustutamine viib sĂŒsteemitabelite tugeva "paisumiseni" pg_class, pg_attribute, pg_attrdef, pg_depend,


NĂŒĂŒd kujutage ette, et teil on töötaja, kes on otseĂŒhenduses andmebaasiga, ja avab iga sekundi jĂ€rel uue tehingu, loob, tĂ€idab, töötleb ja kustutab ajutise tabeli
 SĂŒsteemitabelites koguneb liigset prahti, mis toob kaasa igasuguseid viivitusi iga toimingu juures.

ÜhesĂ”naga, nii ei tohiks teha! Sellisel juhul on palju efektiivsem LOO AJUTINE TABEL x ... KOMITTEERIMISEL KUSTUTA RÜNDAD viia vĂ€lja tehingute tsĂŒklist — nii on igas uues tehingus tabelid juba olemas (sÀÀstame vĂ€ljakutse CREATE), kuid on tĂŒhi, tĂ€nu TRUNCATE (selle vĂ€ljakutse me sÀÀstsime ka) eelmine tehing lĂ”petades.

1.4. LIKE
 KAAASAARVATUD 


Ma mainisin alguses, et ĂŒks tĂŒĂŒpiline kasutusjuht ajutiste tabelite jaoks on erinevad impordid — ja arendaja vĂ€sib, kopeerides sihttabeli vĂ€lja vĂ€ljade loetelu oma ajutisse tabelisse 


Aga laiskus on edasiviiv jÔud! SeetÔttu uus tabel "mooduli" jÀrgi on palju lihtsam luua:

LOO AJUTINE TABEL import_table(
  NAGU sihttabel
);

Kuna sellele tabelisse genereerida saab vĂ€ga palju andmeid, nende otsimine ei ole sugugi kiire. Kuid sellele on traditsiooniline lahendus — indeksid! Ja jah, ajutistel tabelitel vĂ”ivad samuti olla indeksid.

Kuna sageli vajavad vajalikud indeksid kokku langema sihttabeli indeksitega, saab lihtsalt kirjutada NAGU target_table KAASAS INDEKSD.

Kui vajate veel ka DEFAULT-vÀÀrtused (nĂ€iteks esmase vĂ”tme vÀÀrtuste tĂ€itmiseks), saab kasutada NAGU target_table KAASAR KOHANDAMINE. VĂ”i lihtsalt — NAGU target_table KAASUTADES KÕIKI — kopeerib vaikeseaded, indeksid, piirangud,


Kuid siin tuleb juba mĂ”ista, et kui olete loonud imporditabeli kohe indeksitega, siis andmete laadimine vĂ”tab kauem aega, kui kĂ”igepealt kĂ”ik laadida ja alles pĂ€rast seda indekseid lisada — vaadake nĂ€iteks, kuidas seda teeb pg_dump.

Üldiselt, RTFM!

2. Kuidas kirjutada?

Ütlen lihtsalt — kasutage COPY-voogu, mitte "partiide" INSERT, kiirus mitmekordne. Saate isegi otse eelnevalt vormistatud failist.

3. Kuidas töödelda?

Nii, oletame, et meie sisend nÀeb vÀlja ligikaudu nii:

  • teil on andmebaasis tabel kliendiandmetega 1M kirje
  • iga pĂ€ev saadab klient teile uue tĂ€ieliku "pildi"
  • kogemuse pĂ”hjal teate, et kord-korralt muudetakse mitte rohkem kui 10 000 kirjet

Klassikaline nĂ€ide sellisest olukorrast on KLADR andmebaas — aadresside kogus on suur, kuid igas nĂ€dalas esitatavas muudatuste (asulate nimede muutmine, tĂ€navate ĂŒhendamine, uute majade ilmumine) vĂ€ljavĂ”ttes on neid lĂ€bi kogu riigi vĂ€ga vĂ€he.

3.1. TĂ€issĂŒnkroonimise algoritm

Lihtsuse huvides oletame, et te ei pea andmeid restruktureerima — lihtsalt viige tabel soovitud vormi, see tĂ€hendab:

  • kustutage kĂ”ik, mis enam ei eksisteeri
  • uuendada kĂ”ik, mis juba olemas oli, ja vajab uuendamist
  • sisestage kĂ”ik, mida veel ei olnud

Miks just sellises jÀrjestuses tuleks operatsioone teha? Sest just nii kasvab tabeli suurus minimaalselt (pea meeles MVCC-d!).

DELETE FROM dst

Jah, loomulikult saab hakkama ka ainult kahe operatsiooniga:

  • kustutage (DELETE) ĂŒldiselt kĂ”ik
  • sisestage kĂ”ik uue kujutise seast

Kuid tĂ€nu MVCC-le tabeli suurus kahekordistub! Saada +1M kirjeid tabelisse 10K uuendamise tĂ”ttu — see on kĂŒllaltki liig.

TRUNCATE dst

Kogenum arendaja teab, et kogu tabeli saab ĂŒsna odavalt puhastada:

  • puhastage (TRUNCATE) kogu tabel
  • sisestage kĂ”ik uue kujutise seast

TĂ”hus meetod, mĂ”nikord tĂ€iesti rakendatav, aga on ĂŒks probleem
 1M kirjeid lisades lĂ€heb meil kaua aega, seega ei saa me endale lubada, et tabel jÀÀb kogu selle aja jooksul tĂŒhi (nagu juhtub, kui ei kasuta ĂŒhte tehingut).

See tÀhendab:

  • meie jaoks algab pikk tehing
  • TRUNCATE kehtestab AccessExclusive-lukustuse
  • me teeme kaua lisamist, samal ajal ei saa kĂ”ik teised isegi SELECT

Ei nÀe vÀlja, et see hÀsti lÀheb


ALTER TABLE
 RENAME
 / DROP TABLE 


Üks variant on kĂ”ik eraldi uude tabelisse laadida ja seejĂ€rel lihtsalt vana tabeli kohale ĂŒmber nimetada. Paar ebameeldivat asja:

  • see on ikka ka samasugune AccessExclusive, kuigi mĂ€rgatavalt vĂ€hem aega
  • kĂ”ik pĂ€ringute plaanid/statistika selle tabeli kohta kustutatakse, peab jooksma ANALYZE
  • kĂ”ik vĂ€lisvĂ”tmed (FK) tabeli suhtes katkestatakse Olemas oli WIP-patch Simon Riggsilt, mis pakkus

ALTER -operatsiooni, et asendada tabeli keha failitasemel, kahjustamata statistikat ja FK, kuid ei saavutanud vajalikku toetust.DELETE, UPDATE, INSERT

Nii et jÀÀme kolme operatsiooni mitte-blokeerimise variandi juurde. Peaaegu kolme
 Kuidas seda kĂ”ige tĂ”husamalt teha?

Nii et, peatume kolme toimingu mitte-blokeeriva valiku peal. Peaaegu kolme
 Kuidas seda kÔige tÔhusamalt teha?

-- teeme kÔik tehingu raames, et keegi ei nÀeks "vahepealseid" olekuid
BEGIN;

-- loome ajutise tabeli imporditud andmete jaoks
CREATE TEMPORARY TABLE tmp(
  LIKE dst INCLUDING INDEXES -- sarnane, koos indeksitega
) ON COMMIT DROP; -- tehingu raames ei ole meil seda vaja

-- kiirelt laadime uue kujundi lÀbi COPY
COPY tmp FROM STDIN;
-- ...
-- .

-- eemaldame puuduvad
DELETE FROM
  dst D
USING
  dst X
LEFT JOIN
  tmp Y
    USING(pk1, pk2) -- primaarkeele vÀljad
WHERE
  (D.pk1, D.pk2) = (X.pk1, X.pk2) AND
  Y IS NOT DISTINCT FROM NULL; -- "anti-join"

-- uuendame jÀÀnud
UPDATE
  dst D
SET
  (f1, f2, f3) = (T.f1, T.f2, T.f3)
FROM
  tmp T
WHERE
  (D.pk1, D.pk2) = (T.pk1, T.pk2) AND
  (D.f1, D.f2, D.f3) IS DISTINCT FROM (T.f1, T.f2, T.f3); -- ei ole mÔtet uuendada kokku langevaid

-- lisame puuduvad
INSERT INTO
  dst
SELECT
  T.*
FROM
  tmp T
LEFT JOIN
  dst D
    USING(pk1, pk2)
WHERE
  D IS NOT DISTINCT FROM NULL;

COMMIT;

3.2. Postitöötlus impordist

Samas KADRi kĂ”iki muudetud kirjeid tuleb lisaks lĂ€bi viia postitöötlus — normaliseerida, eristada mĂ€rksĂ”nu, viia vajalikesse struktuuridesse. Aga kuidas teada — mida tĂ€pselt on muudetud, keerukust suurendamata sĂŒnkroniseerimise koodis, ideaalis, mitte puutuda sellesse ĂŒldse?

Kui kirjutamisĂ”igus on sĂŒnkroniseerimise ajal ainult teie protsessil, saate kasutada kĂ€ivitusmehhanismi, mis kogub kĂ”ik muudatused kokku:

-- siht tabelid
CREATE TABLE kladr(...);
CREATE TABLE kladr_house(...);

-- muudatuste ajalooga tabelid
CREATE TABLE kladr$log(
  ro kladr, -- siin on vanade/uutest kirjetest tÀis kujutised
  rn kladr
);

CREATE TABLE kladr_house$log(
  ro kladr_house,
  rn kladr_house
);

-- muudatuste logimise ĂŒldine funktsioon
CREATE OR REPLACE FUNCTION diff$log() RETURNS trigger AS $$
DECLARE
  dst varchar = TG_TABLE_NAME || '$log';
  stmt text = '';
BEGIN
  -- kontrollime, kas logimine on vajalik kirje uuendamisel
  IF TG_OP = 'UPDATE' THEN
    IF NEW IS NOT DISTINCT FROM OLD THEN
      RETURN NEW;
    END IF;
  END IF;
  -- loome logikirje
  stmt = 'INSERT INTO ' || dst::text || '(ro,rn) VALUES(';
  CASE TG_OP
    WHEN 'INSERT' THEN
      EXECUTE stmt || 'NULL,$1)' USING NEW;
    WHEN 'UPDATE' THEN
      EXECUTE stmt || '$1,$2)' USING OLD, NEW;
    WHEN 'DELETE' THEN
      EXECUTE stmt || '$1,NULL)' USING OLD;
  END CASE;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

NĂŒĂŒd saame enne sĂŒnkroniseerimise alustamist kĂ€ivitusmehhanismid aktiveerida (vĂ”i lubada nende kaudu ALTER TABLE ... ENABLE TRIGGER ...):

CREATE TRIGGER log
  AFTER INSERT OR UPDATE OR DELETE
  ON kladr
    FOR EACH ROW
      EXECUTE PROCEDURE diff$log();

CREATE TRIGGER log
  AFTER INSERT OR UPDATE OR DELETE
  ON kladr_house
    FOR EACH ROW
      EXECUTE PROCEDURE diff$log();

Ja vÔime rahulikult logitabelitest vÀlja vÔtta kÔik vajalikud muudatused ja edastada need tÀiendavatele töötlejatele.

3.3. Seotud komplektide importimine

Ülalpool kĂ€sitlesime juhtumeid, kus sisendi ja vĂ€ljundi andmestruktuurid on samad. Kuid mis juhtub, kui vĂ€listest sĂŒsteemidest saadud eksport on struktuurilt erinev meie andmebaasi salvestamisest?

VĂ”tame nĂ€iteks klientide ja nende arvete sĂ€ilitamise, klassikalise „palju-ĂŒhele” variandi:

CREATE TABLE client(
  client_id
    serial
      PRIMARY KEY
, inn
    varchar
      UNIQUE
, name
    varchar
);

CREATE TABLE invoice(
  invoice_id
    serial
      PRIMARY KEY
, client_id
    integer
      REFERENCES client(client_id)
, number
    varchar
, dt
    date
, sum
    numeric(32,2)
);

Kuid vĂ€listest allikatest saadud eksport on meil vormis „ kĂ”ik ĂŒhes”:

CREATE TEMPORARY TABLE invoice_import(
  client_inn
    varchar
, client_name
    varchar
, invoice_number
    varchar
, invoice_dt
    date
, invoice_sum
    numeric(32,2)
);

On ilmne, et klientide andmed vĂ”ivad sellisel juhul dubleerida, kusjuures pĂ”hikirje on „arve”:

0123456789; Vasilij; A-01; 2020-03-16; 1000.00
9876543210; Peeter; A-02; 2020-03-16; 666.00
0123456789; Vasilij; B-03; 2020-03-16; 9999.00

Mudeli jaoks sisestame lihtsalt meie testandmed, kuid peame meeles pidama — COPY efektiivsem!

INSERT INTO invoice_import
VALUES
  ('0123456789', 'Vassilis', 'A-01', '2020-03-16', 1000.00)
, ('9876543210', 'Petja', 'A-02', '2020-03-16', 666.00)
, ('0123456789', 'Vassilis', 'B-03', '2020-03-16', 9999.00);

Esimene samm on tuvastada need 'kategooriad', millele meie 'faktid' viitavad. Meie puhul viitavad arved klientidele:

CREATE TEMPORARY TABLE client_import AS
SELECT DISTINCT ON(client_inn)
-- vÔib kasutada lihtsalt SELECT DISTINCT, kui andmed on eelnevalt kooskÔlas
  client_inn inn
, client_name "name"
FROM
  invoice_import;

Kuna peame arved Ôigesti seostama klientide ID-dega, peame kÔigepealt need identifikaatorid vÀlja selgitama vÔi genereerima. Lisame nende jaoks vÀljad:

ALTER TABLE invoice_import ADD COLUMN client_id integer;
ALTER TABLE client_import ADD COLUMN client_id integer;

Kasutame eespool kirjeldatud tabelite sĂŒnkroniseerimise meetodit vĂ€ikese muudatusega — me ei uuenda ega kustuta midagi sihttabelis, kuna klientide importimine on meil 'append-only':

-- impordime tabelisse juba olemasolevate kirje ID-d
UPDATE
  client_import T
SET
  client_id = D.client_id
FROM
  client D
WHERE
  T.inn = D.inn; -- unikaalne vÔti

-- lisame puuduvad kirjed ja mÀÀrame nende ID-d
WITH ins AS (
  INSERT INTO client(
    inn
  , name
  )
  SELECT
    inn
  , name
  FROM
    client_import
  WHERE
    client_id IS NULL -- kui ID pole mÀÀratud
  RETURNING *
)
UPDATE
  client_import T
SET
  client_id = D.client_id
FROM
  ins D
WHERE
  T.inn = D.inn; -- unikaalne vÔti

-- mÀÀrame klientide ID-d arvete kirjestes
UPDATE
  invoice_import T
SET
  client_id = D.client_id
FROM
  client_import D
WHERE
  T.client_inn = D.inn; -- rakendatav vÔti

KokkuvĂ”ttes, kĂ”ik — invoice_import nĂŒĂŒd on meil seotud vĂ€lja tĂ€idetud client_id, millega me ka arve sisestame.

Allikas: habr.com

Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster