DBA: organizoni me kompetencë sinkronizimet dhe importet
Për përpunimin kompleks të grupeve të mëdha të të dhënave (të ndryshme Proceset ETL: importet, konvertimet dhe sinkronizimi me një burim të jashtëm) shpesh ka nevojë "mbani mend" përkohësisht dhe përpunoni menjëherë diçka voluminoze.
Por kur vëllimi i kësaj "diçkaje" fillon të matet në qindra megabajt dhe shërbimi duhet të vazhdojë të punojë me bazën e të dhënave 24x7, lindin shumë efekte anësore që do t'ju shkatërrojnë jetën.
Për t'u marrë me to në PostgreSQL (dhe jo vetëm në të), mund të përdorni disa optimizime që do t'ju lejojnë të përpunoni gjithçka më shpejt dhe me më pak konsum burimesh.
1. Ku të dërgohet?
Së pari, le të vendosim se ku mund të ngarkojmë të dhënat që duam të "përpunojmë".
1.1. Tabelat e përkohshme (TABELA E PËRKOHSHME)
Në parim, për PostgreSQL tabelat e përkohshme janë të njëjta si çdo tjetër. Prandaj, besëtytnitë si "Gjithçka atje ruhet vetëm në kujtesë dhe mund të përfundojë". Por ka edhe disa dallime të rëndësishme.
"Hapësira e emrit" tuaj për çdo lidhje me bazën e të dhënave
Nëse dy lidhje përpiqen të lidhen në të njëjtën kohë CREATE TABLE x, atëherë dikush patjetër do të marrë gabim jo-unikaliteti objektet e bazës së të dhënave.
Por nëse të dy përpiqen të ekzekutojnë CREATE TEMPORARY TABLE x, atëherë të dy do ta bëjnë normalisht, dhe të gjithë do ta marrin kopjen tuaj tabelat. Dhe nuk do të ketë asgjë të përbashkët mes tyre.
"Vetë-shkatërrojë" kur shkëputet
Kur lidhja mbyllet, të gjitha tabelat e përkohshme fshihen automatikisht, pra me dorë DROP TABLE x nuk ka kuptim pervec...
Nëse jeni duke punuar pgbouncer në modalitetin e transaksionit, atëherë baza e të dhënave vazhdon të besojë se kjo lidhje është ende aktive, dhe në të kjo tabelë e përkohshme ekziston ende.
Prandaj, përpjekja për ta krijuar atë përsëri, nga një lidhje tjetër me pgbouncer, do të rezultojë në një gabim. Por kjo mund të anashkalohet duke përdorur CREATE TEMPORARY TABLE IF NOT EXISTS x.
Vërtetë, është më mirë të mos e bëni këtë gjithsesi, sepse atëherë mund të gjeni "papritmas" atje të dhënat e mbetura nga "pronari i mëparshëm". Në vend të kësaj, është shumë më mirë të lexoni manualin dhe të shihni se kur krijoni një tabelë është e mundur të shtoni ON COMMIT DROP - domethënë, kur transaksioni të përfundojë, tabela do të fshihet automatikisht.
Mospërsëritje
Për shkak se ato i përkasin vetëm një lidhjeje specifike, tabelat e përkohshme nuk përsëriten. Por kjo eliminon nevojën për regjistrim të dyfishtë të të dhënave në grumbull + WAL, kështu që INSERT/UPDATE/DELETE në të është shumë më i shpejtë.
Por meqenëse një tabelë e përkohshme është ende një tabelë "pothuajse e zakonshme", ajo nuk mund të krijohet as në një kopje. Të paktën tani për tani, megjithëse patch-i përkatës qarkullon prej kohësh.
1.2. TABELA E SHKAKTUAR
Por çfarë duhet të bëni, për shembull, nëse keni një lloj procesi të rëndë ETL që nuk mund të zbatohet brenda një transaksioni, por ju ende keni pgbouncer në modalitetin e transaksionit? ..
Ose fluksi i të dhënave është aq i madh sa Nuk ka gjerësi bande të mjaftueshme në një lidhje nga një bazë të dhënash (lexo, një proces për CPU)?..
Ose disa operacione po zhvillohen në mënyrë asinkrone ne lidhje te ndryshme?..
Këtu ka vetëm një opsion - krijoni përkohësisht një tabelë jo të përkohshme. Puna, po. Kjo eshte:
krijoi tabela "të miat" me emra maksimalisht të rastësishëm në mënyrë që të mos kryqëzoheshin me askënd
Ekstrakt: i mbushi me të dhëna nga një burim i jashtëm
Transformoj: i konvertuar, i plotësuar në fushat kryesore lidhëse
Ngarkesë: derdhi të dhënat e gatshme në tabelat e synuara
fshiu tabelat "e mia".
Dhe tani - një mizë në vaj. Në fakt, të gjitha shkrimet në PostgreSQL ndodhin dy herë - së pari në WAL, pastaj në trupat e tabelës/indeksit. E gjithë kjo është bërë për të mbështetur ACID dhe për të korrigjuar dukshmërinë e të dhënave ndërmjet tyre COMMIT'i arra dhe ROLLBACK'transaksione të pavlefshme.
Por ne nuk kemi nevojë për këtë! Ne kemi të gjithë procesin Ose ishte plotësisht i suksesshëm ose jo.. Nuk ka rëndësi se sa transaksione të ndërmjetme do të ketë - ne nuk jemi të interesuar të "vazhdojmë procesin nga mesi", veçanërisht kur nuk është e qartë se ku ishte.
Për ta bërë këtë, zhvilluesit PostgreSQL, përsëri në versionin 9.1, prezantuan një gjë të tillë si tavolina të UNLOGGED:
Me këtë tregues, tabela krijohet si e paloguar. Të dhënat e shkruara në tabela të paloguara nuk kalojnë nëpër regjistrin e parashkrimit (shih Kapitullin 29), duke bërë që tabela të tilla të punoni shumë më shpejt se zakonisht. Megjithatë, ata nuk janë të imunizuar ndaj dështimit; në rast të dështimit të serverit ose mbylljes emergjente, një tabelë e paloguar shkurtohet automatikisht. Për më tepër, përmbajtja e tabelës së paloguar nuk përsëritet te serverët skllevër. Çdo indeks i krijuar në një tabelë të paloguar bëhet automatikisht i çloguar.
Me pak fjalë do të jetë shumë më shpejt, por nëse serveri i bazës së të dhënave "bie", do të jetë i pakëndshëm. Por sa shpesh ndodh kjo dhe a di procesi juaj ETL se si ta korrigjojë këtë saktë "nga mesi" pas "rigjallërimit" të bazës së të dhënave?..
Nëse jo, dhe rasti i mësipërm është i ngjashëm me tuajin, përdorni UNLOGGEDpor kurrë mos e aktivizoni këtë atribut në tabelat reale, të dhënat nga e cila janë të dashura për ju.
1.3. PËR KRYERJE { FSHI RRESHTAT | HIQ}
Ky konstrukt ju lejon të specifikoni sjelljen automatike kur një transaksion përfundon kur krijoni një tabelë.
në ON COMMIT DROP Tashmë kam shkruar më lart, ajo gjeneron DROP TABLE, por me ON COMMIT DELETE ROWS situata është më interesante - ajo krijohet këtu TRUNCATE TABLE.
Meqenëse e gjithë infrastruktura për ruajtjen e meta-përshkrimit të një tabele të përkohshme është saktësisht e njëjtë me atë të një tabele të rregullt, atëherë Krijimi dhe fshirja e vazhdueshme e tabelave të përkohshme çon në "ënjtje" të rëndë të tabelave të sistemit pg_class, pg_attribute, pg_attrdef, pg_vare,…
Tani imagjinoni që keni një punëtor në lidhje direkte me bazën e të dhënave, i cili hap një transaksion të ri çdo sekondë, krijon, mbush, përpunon dhe fshin një tabelë të përkohshme... Do të ketë një tepricë të mbeturinave të grumbulluara në tabelat e sistemit, dhe kjo do të shkaktojë frena shtesë për çdo operacion.
Në përgjithësi, mos e bëni këtë! Në këtë rast është shumë më efektive CREATE TEMPORARY TABLE x ... ON COMMIT DELETE ROWS hiqeni atë nga cikli i transaksionit - atëherë në fillim të çdo transaksioni të ri tabelat janë tashmë do të ekzistojë (ruaj një telefonatë CREATE), por do të jetë bosh, falë TRUNCATE (ne ruajtëm gjithashtu thirrjen e tij) kur përfunduam transaksionin e mëparshëm.
1.4. LIKE... PËRFSHIRË...
E përmenda në fillim se një nga rastet tipike të përdorimit të tabelave të përkohshme janë lloje të ndryshme importesh - dhe zhvilluesi i lodhur kopjon listën e fushave të tabelës së synuar në deklaratën e tij të përkohshme...
Por dembelizmi është motori i përparimit! Kjo është arsyeja pse krijoni një tabelë të re "bazuar në mostër" mund të jetë shumë më e thjeshtë:
CREATE TEMPORARY TABLE import_table(
LIKE target_table
);
Meqenëse më pas mund të gjeneroni shumë të dhëna në këtë tabelë, kërkimi në të nuk do të jetë kurrë i shpejtë. Por ekziston një zgjidhje tradicionale për këtë - indekset! Dhe po, një tabelë e përkohshme mund të ketë edhe indekse.
Meqenëse, shpesh, indekset e kërkuara përkojnë me indekset e tabelës së synuar, thjesht mund të shkruani LIKE target_table INCLUDING INDEXES.
Nëse keni nevojë gjithashtu DEFAULT-vlerat (për shembull, për të plotësuar vlerat kryesore të çelësit), mund të përdorni LIKE target_table INCLUDING DEFAULTS. Ose thjesht - LIKE target_table INCLUDING ALL — kopjon parazgjedhjet, indekset, kufizimet,...
Por këtu ju duhet të kuptoni se nëse keni krijuar importoni menjëherë tabelën me indekse, atëherë të dhënat do të marrin më shumë kohë për t'u ngarkuarsesa nëse së pari plotësoni gjithçka dhe vetëm atëherë grumbulloni indekset - shikoni se si e bën këtë si shembull pg_dump.
Më lejoni të them vetëm - përdorni atë COPY- rrjedh në vend të "paketë" INSERT, përshpejtimi nganjëherë. Mund edhe direkt nga një skedar i krijuar paraprakisht.
3. Si të përpunohet?
Pra, le ta lëmë hyrjen tonë të duket diçka si kjo:
ju keni një tabelë me të dhënat e klientit të ruajtura në bazën e të dhënave tuaja 1 milion rekorde
çdo ditë një klient ju dërgon një të re "imazhi" i plotë
nga përvoja ju e dini që herë pas here nuk ndryshohen më shumë se 10 mijë rekorde
Një shembull klasik i një situate të tillë është Baza e KLADR — gjithsej ka shumë adresa, por në çdo ngarkim javor ka shumë pak ndryshime (riemërtimi i vendbanimeve, kombinimi i rrugëve, shfaqja e shtëpive të reja) edhe në shkallë kombëtare.
3.1. Algoritmi i plotë i sinkronizimit
Për thjeshtësi, le të themi se nuk keni nevojë as të ristrukturoni të dhënat - thjesht sillni tabelën në formën e dëshiruar, domethënë:
heq gjithçka që nuk ekziston më
azhurnimi gjithçka që ekzistonte dhe duhet të përditësohet
për të futur gjithçka që nuk ka ndodhur ende
Pse duhet të kryhen operacionet në këtë mënyrë? Sepse kjo është mënyra se si madhësia e tryezës do të rritet minimalisht (mbani mend MVCC!).
FSHIJE NGA dst
Jo, sigurisht që mund t'ia dilni me vetëm dy operacione:
heq (DELETE) gjithçka në përgjithësi
për të futur të gjitha nga imazhi i ri
Por në të njëjtën kohë, falë MVCC, Madhësia e tabelës do të rritet saktësisht dy herë! Marrja e +1 milion imazhe të të dhënave në tabelë për shkak të një përditësimi 10K është kaq e tepërt...
PRUNKOJ dst
Një zhvillues më me përvojë e di se i gjithë tableti mund të pastrohet mjaft lirë:
i pastër (TRUNCATE) të gjithë tabelën
për të futur të gjitha nga imazhi i ri
Metoda është efektive, ndonjëherë mjaft i zbatueshëm, por ka një problem... Ne do të shtojmë rekorde 1 milion për një kohë të gjatë, kështu që nuk mund të përballojmë ta lëmë tryezën bosh gjatë gjithë kësaj kohe (siç do të ndodhë pa e mbështjellë atë në një transaksion të vetëm).
Që do të thotë:
po fillojmë transaksion afatgjatë
TRUNCATE imponon AccessExclusive- bllokim
ne e bëjmë futjen për një kohë të gjatë, dhe të gjithë të tjerët në këtë kohë as nuk mundet SELECT
Diçka nuk po shkon mirë...
NDRYSHO TABELA… RIEMËROJ… / HIQ TABELA…
Një alternativë është të plotësoni gjithçka në një tabelë të re të veçantë dhe më pas thjesht ta riemërtoni në vend të asaj të vjetër. Disa gjëra të vogla të këqija:
ende gjithashtu AccessExclusive, edhe pse dukshëm më pak kohë
të gjithë çelësat e huaj janë të prishur (FK) në tryezë
Kishte një patch WIP nga Simon Riggs që sugjeronte krijimin ALTER- një operacion për zëvendësimin e trupit të tabelës në nivel skedari, pa prekur statistikat dhe FK, por nuk ka mbledhur kuorum.
FSHI, PËRDITËSO, FAQE
Pra, ne vendosemi në opsionin e mosbllokimit të tre operacioneve. Pothuajse tre... Si ta bëni këtë në mënyrë më efektive?
-- все делаем в рамках транзакции, чтобы никто не видел "промежуточных" состояний
BEGIN;
-- создаем временную таблицу с импортируемыми данными
CREATE TEMPORARY TABLE tmp(
LIKE dst INCLUDING INDEXES -- по образу и подобию, вместе с индексами
) ON COMMIT DROP; -- за рамками транзакции она нам не нужна
-- быстро-быстро вливаем новый образ через COPY
COPY tmp FROM STDIN;
-- ...
-- .
-- удаляем отсутствующие
DELETE FROM
dst D
USING
dst X
LEFT JOIN
tmp Y
USING(pk1, pk2) -- поля первичного ключа
WHERE
(D.pk1, D.pk2) = (X.pk1, X.pk2) AND
Y IS NOT DISTINCT FROM NULL; -- "антиджойн"
-- обновляем оставшиеся
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); -- незачем обновлять совпадающие
-- вставляем отсутствующие
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. Importi pas përpunimit
Në të njëjtin KLADR, të gjitha regjistrimet e ndryshuara duhet të ekzekutohen shtesë përmes përpunimit pas - të normalizohen, fjalët kyçe të theksohen dhe të reduktohen në strukturat e kërkuara. Por si e dini - çfarë ka ndryshuar saktësishtpa e komplikuar kodin e sinkronizimit, në mënyrë ideale pa e prekur fare?
Nëse vetëm procesi juaj ka akses shkrimi në kohën e sinkronizimit, atëherë mund të përdorni një shkas që do të mbledhë të gjitha ndryshimet për ne:
-- целевые таблицы
CREATE TABLE kladr(...);
CREATE TABLE kladr_house(...);
-- таблицы с историей изменений
CREATE TABLE kladr$log(
ro kladr, -- тут лежат целые образы записей старой/новой
rn kladr
);
CREATE TABLE kladr_house$log(
ro kladr_house,
rn kladr_house
);
-- общая функция логирования изменений
CREATE OR REPLACE FUNCTION diff$log() RETURNS trigger AS $$
DECLARE
dst varchar = TG_TABLE_NAME || '$log';
stmt text = '';
BEGIN
-- проверяем необходимость логгирования при обновлении записи
IF TG_OP = 'UPDATE' THEN
IF NEW IS NOT DISTINCT FROM OLD THEN
RETURN NEW;
END IF;
END IF;
-- создаем запись лога
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;
Tani mund të aplikojmë nxitës përpara se të fillojmë sinkronizimin (ose t'i aktivizojmë ato nëpërmjet 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();
Dhe më pas nxjerrim me qetësi të gjitha ndryshimet që na nevojiten nga tabelat e regjistrave dhe i drejtojmë ato përmes mbajtësve shtesë.
3.3. Importimi i grupeve të lidhura
Më sipër kemi shqyrtuar rastet kur strukturat e të dhënave të burimit dhe destinacionit janë të njëjta. Por, çka nëse ngarkimi nga një sistem i jashtëm ka një format të ndryshëm nga struktura e ruajtjes në bazën tonë të të dhënave?
Le të marrim si shembull ruajtjen e klientëve dhe llogarive të tyre, opsionin klasik "shumë-për-një":
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)
);
Por shkarkimi nga një burim i jashtëm na vjen në formën e "të gjitha në një":
Së pari, le të theksojmë ato "prerje" të cilave u referohen "faktet" tona. Në rastin tonë, faturat u referohen klientëve:
CREATE TEMPORARY TABLE client_import AS
SELECT DISTINCT ON(client_inn)
-- можно просто SELECT DISTINCT, если данные заведомо непротиворечивы
client_inn inn
, client_name "name"
FROM
invoice_import;
Për të lidhur saktë llogaritë me ID-të e klientëve, së pari duhet të zbulojmë ose gjenerojmë këta identifikues. Le të shtojmë fushat nën to:
ALTER TABLE invoice_import ADD COLUMN client_id integer;
ALTER TABLE client_import ADD COLUMN client_id integer;
Le të përdorim metodën e sinkronizimit të tabelës të përshkruar më sipër me një ndryshim të vogël - ne nuk do të përditësojmë ose fshijmë asgjë në tabelën e synuar, sepse ne importojmë klientët "vetëm shtojca":
-- проставляем в таблице импорта ID уже существующих записей
UPDATE
client_import T
SET
client_id = D.client_id
FROM
client D
WHERE
T.inn = D.inn; -- unique key
-- вставляем отсутствовавшие записи и проставляем их ID
WITH ins AS (
INSERT INTO client(
inn
, name
)
SELECT
inn
, name
FROM
client_import
WHERE
client_id IS NULL -- если ID не проставился
RETURNING *
)
UPDATE
client_import T
SET
client_id = D.client_id
FROM
ins D
WHERE
T.inn = D.inn; -- unique key
-- проставляем ID клиентов у записей счетов
UPDATE
invoice_import T
SET
client_id = D.client_id
FROM
client_import D
WHERE
T.client_inn = D.inn; -- прикладной ключ
Në fakt, gjithçka është brenda invoice_import Tani kemi të plotësuar fushën e kontaktit client_id, me të cilin do të fusim faturën.