ProHoster > Blog > Uprava > DBA: kompetentno organizirati sinhronizacije in uvoze
DBA: kompetentno organizirati sinhronizacije in uvoze
Za kompleksno obdelavo velikih nizov podatkov (razl ETL procesi: uvozi, pretvorbe in sinhronizacija z zunanjim virom) pogosto obstaja potreba začasno »zapomni« in takoj hitro obdela nekaj obsežnega.
Tipična tovrstna naloga običajno zveni nekako takole: "Točno tukaj računovodstvo raztovorjeno iz banke stranke zadnja prejeta plačila, jih morate hitro naložiti na spletno stran in jih povezati s svojimi računi.«
A ko se obseg tega »nečesa« začne meriti v stotinah megabajtov in mora storitev še naprej delovati z bazo 24x7, se pojavijo številni stranski učinki, ki vam bodo uničili življenje.
Da bi se z njimi spopadli v PostgreSQL (in ne samo v njem), lahko uporabite nekaj optimizacij, ki vam bodo omogočile, da boste vse obdelali hitreje in z manjšo porabo virov.
1. Kam poslati?
Najprej se odločimo, kam lahko naložimo podatke, ki jih želimo »obdelati«.
1.1. Začasne tabele (TEMPORARY TABLE)
Načeloma so začasne tabele za PostgreSQL enake kot vse druge. Zato vraževerje kot "Vse tam je shranjeno samo v spominu in lahko se konča". Obstaja pa tudi več pomembnih razlik.
Vaš lasten »imenski prostor« za vsako povezavo z bazo podatkov
Če se dve povezavi poskušata povezati hkrati CREATE TABLE x, potem bo nekdo zagotovo dobil napaka needinstvenosti predmeti baze podatkov.
Če pa oba poskušata izvršiti CREATE TEMPORARY TABLE x, potem bosta oba to storila normalno in vsi bodo dobili vaš izvod mize. In med njima ne bo nič skupnega.
"Samouničenje" ob odklopu
Ko se povezava prekine, se vse začasne tabele samodejno izbrišejo, torej ročno DROP TABLE x ni smisla razen...
Če delate skozi pgbouncer v načinu transakcije, potem baza podatkov še naprej verjame, da je ta povezava še vedno aktivna in v njej ta začasna tabela še vedno obstaja.
Zato bo poskus ponovnega ustvarjanja iz druge povezave s pgbouncer povzročil napako. Toda to je mogoče zaobiti z uporabo CREATE TEMPORARY TABLE IF NOT EXISTS x.
Res je, da je bolje, da tega vseeno ne storite, ker potem lahko "nenadoma" najdete podatke, ki so ostali od "prejšnjega lastnika". Namesto tega je veliko bolje prebrati priročnik in videti, da je pri ustvarjanju tabele mogoče dodajati ON COMMIT DROP - to pomeni, da bo tabela samodejno izbrisana, ko bo transakcija končana.
Nepodvajanje
Ker pripadajo le določeni povezavi, se začasne tabele ne podvajajo. Ampak to odpravlja potrebo po dvojnem zapisovanju podatkov v kupu + WAL, zato je INSERT/UPDATE/DELETE vanj veliko hitrejši.
Ker pa je začasna tabela še vedno »skoraj navadna« tabela, je tudi na replici ni mogoče ustvariti. Vsaj za zdaj, čeprav ustrezen popravek kroži že dolgo.
1.2. NEPRIJAVLJENA TABELA
Toda kaj storiti, na primer, če imate nekakšen okoren postopek ETL, ki ga ni mogoče implementirati v eni transakciji, vendar imate še vedno pgbouncer v načinu transakcije? ..
Ali pa je pretok podatkov tako velik, da Na eni povezavi ni dovolj pasovne širine iz baze podatkov (beri, en proces na CPU)?..
Ali pa potekajo kakšne operacije asinhrono v različnih povezavah?..
Tukaj je samo ena možnost - začasno ustvarite nezačasno tabelo. Pun, ja. To je:
ustvaril "moje" tabele z maksimalno naključnimi imeni, da se ne bi križal z nikomer
Izvleček: napolnil jih je s podatki iz zunanjega vira
Transform: pretvorjeno, izpolnjena ključna povezovalna polja
Obremenitev: prelil pripravljene podatke v ciljne tabele
izbrisal "moje" tabele
In zdaj - muha v mazilu. Pravzaprav, vsi zapisi v PostgreSQL se zgodijo dvakrat - prvi v WAL, nato pa v telo tabele/indeksa. Vse to je narejeno za podporo ACID in pravilno vidnost podatkov med COMMIToreh in ROLLBACK'nične transakcije.
Ampak tega ne potrebujemo! Imamo celoten postopek Ali je bilo popolnoma uspešno ali pa ne.. Ni pomembno, koliko vmesnih transakcij bo - ne zanima nas "nadaljevanje postopka od sredine", še posebej, če ni jasno, kje je bil.
Da bi to naredili, so razvijalci PostgreSQL v različici 9.1 predstavili nekaj takega, kot je NEPRIJAVLJENE tabele:
S to indikacijo je tabela ustvarjena kot neprijavljena. Podatki, zapisani v nezabeležene tabele, ne gredo skozi dnevnik vnaprejšnjega pisanja (glejte 29. poglavje), zaradi česar se takšne tabele deluje veliko hitreje kot običajno. Vendar pa niso imuni na neuspeh; v primeru okvare strežnika ali zaustavitve v sili, neprijavljena tabela samodejno okrnjena. Poleg tega vsebina nezabeležene tabele ni ponovljeno podrejenim strežnikom. Vsi indeksi, ustvarjeni v nezabeleženi tabeli, samodejno postanejo odjavljeni.
Skratka, bo veliko hitreje, če pa strežnik baze podatkov »pade«, bo neprijetno. Toda kako pogosto se to zgodi in ali zna vaš ETL proces to pravilno popraviti "od sredine" po "revitalizaciji" baze podatkov?..
Če ne in je zgornji primer podoben vašemu, uporabite UNLOGGEDampak nikoli ne omogočite tega atributa na pravih tabelah, podatki iz katerega so vam dragi.
1.3. ON COMMIT { DELETE ROWS | DROP}
Ta konstrukcija vam omogoča, da pri ustvarjanju tabele določite samodejno vedenje, ko je transakcija zaključena.
na ON COMMIT DROP Zgoraj sem že napisal, ustvarja DROP TABLE, ampak z ON COMMIT DELETE ROWS situacija je bolj zanimiva - tukaj se ustvari TRUNCATE TABLE.
Ker je celotna infrastruktura za shranjevanje metaopisa začasne tabele popolnoma enaka infrastrukturi običajne tabele, potem Nenehno ustvarjanje in brisanje začasnih tabel povzroči močno "nabrekanje" sistemskih tabel pg_class, pg_attribute, pg_attrdef, pg_depend,…
Zdaj pa si predstavljajte, da imate delavca na direktni povezavi z bazo, ki vsako sekundo odpre novo transakcijo, ustvari, polni, obdeluje in briše začasno tabelo... V sistemskih tabelah se bo nabralo preveč smeti in to bo povzročilo dodatne zavore za vsako operacijo.
Na splošno tega ne počnite! V tem primeru je veliko bolj učinkovito CREATE TEMPORARY TABLE x ... ON COMMIT DELETE ROWS vzeti iz transakcijskega cikla - potem so do začetka vsake nove transakcije mize že bo obstajal (shranite klic CREATE), ampak bo prazna, zahvale gredo TRUNCATE (shranili smo tudi njegov klic) ob zaključku prejšnje transakcije.
1.4. VŠEČ ... VKLJUČNO ...
Na začetku sem omenil, da je eden od tipičnih primerov uporabe začasnih tabel različne vrste uvozov - in razvijalec utrujeno kopira-prilepi seznam polj ciljne tabele v deklaracijo svoje začasne ...
Toda lenoba je motor napredka! Zato ustvarite novo tabelo "na podlagi vzorca" lahko je veliko bolj preprosto:
CREATE TEMPORARY TABLE import_table(
LIKE target_table
);
Ker lahko nato v to tabelo ustvarite veliko podatkov, iskanje po njej nikoli ne bo hitro. Toda za to obstaja tradicionalna rešitev - indeksi! In ja, začasna tabela ima lahko tudi indekse.
Ker pogosto zahtevani indeksi sovpadajo z indeksi ciljne tabele, lahko preprosto pišete LIKE target_table INCLUDING INDEXES.
Če tudi potrebujete DEFAULT-vrednosti (na primer za izpolnjevanje vrednosti primarnega ključa), lahko uporabite LIKE target_table INCLUDING DEFAULTS. Ali preprosto - LIKE target_table INCLUDING ALL — kopira privzete vrednosti, indekse, omejitve,...
Ampak tukaj morate razumeti, da če ste ustvarili uvozite tabelo takoj z indeksi, potem se bodo podatki nalagali dljekot če najprej vse izpolniš in šele nato zviješ indekse - poglej kako to počne na primeru pg_dump.
Naj samo rečem - uporabite ga COPY-tok namesto "pack" INSERT, pospešek na trenutke. Lahko celo neposredno iz vnaprej ustvarjene datoteke.
3. Kako obdelati?
Torej, naj bo naš uvod videti nekako takole:
v vaši zbirki podatkov imate shranjeno tabelo s podatki o strankah 1 milijon zapisov
vsak dan vam stranka pošlje novega polna "slika"
iz izkušenj veš, da od časa do časa ne spremeni se več kot 10K zapisov
Klasičen primer takšne situacije je Baza KLADR — naslovov je skupaj veliko, vendar je v vsakem tedenskem nalaganju zelo malo sprememb (preimenovanje naselij, združevanje ulic, pojav novih hiš) tudi v državnem merilu.
3.1. Algoritem popolne sinhronizacije
Za poenostavitev povejmo, da vam ni treba niti prestrukturirati podatkov - samo prinesite tabelo v želeno obliko, to je:
odstranite vse kar ne obstaja več
Nadgradnja vse, kar je že obstajalo in ga je treba posodobiti
vstaviti vse kar se še ni zgodilo
Zakaj je treba operacije izvajati v tem vrstnem redu? Ker bo tako velikost mize minimalno narasla (zapomni si MVCC!).
IZBRIŠI IZ dst
Ne, seveda lahko preživite samo z dvema operacijama:
odstranite (DELETE) vse na splošno
vstaviti vse iz nove slike
Toda hkrati, zahvaljujoč MVCC, Velikost mize se bo povečala natanko dvakrat! Pridobivanje +1 milijona slik zapisov v tabeli zaradi posodobitve 10K je tako zelo odvečno ...
OKREŽI dst
Bolj izkušen razvijalec ve, da je mogoče celotno tablico očistiti precej poceni:
jasno (TRUNCATE) celotno tabelo
vstaviti vse iz nove slike
Metoda je učinkovita, včasih kar uporabno, vendar obstaja težava ... Dolgo časa bomo dodajali 1 milijon zapisov, zato si ne moremo privoščiti, da bi tabela ves ta čas ostala prazna (kot se bo zgodilo, ne da bi jo zavili v eno transakcijo).
Kar pomeni:
začenjamo dolgotrajna transakcija
TRUNCATE vsiljuje Ekskluziven dostop-blokiranje
mi delamo vstavljanje dolgo časa, vsi ostali pa v tem času niti ne morem SELECT
Nekaj ne gre dobro ...
SPREMENI TABELO… PREIMENI…/IZPUSTI TABELO…
Druga možnost je, da vse izpolnite v ločeno novo tabelo in jo nato preprosto preimenujete namesto stare. Par neprijetnih malenkosti:
še vedno tudi Ekskluziven dostop, čeprav bistveno manj časa
Obstajal je WIP popravek Simona Riggsa, ki je predlagal izdelavo ALTER-operacija za zamenjavo telesa tabele na ravni datoteke, brez dotika statistike in FK, vendar ni zbrala kvoruma.
IZBRIŠI, POSODOBI, VSTAVI
Torej smo se odločili za možnost treh operacij brez blokiranja. Skoraj tri ... Kako to narediti najbolj učinkovito?
-- все делаем в рамках транзакции, чтобы никто не видел "промежуточных" состояний
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. Naknadna obdelava uvoza
V istem KLADR-u je treba vse spremenjene zapise dodatno speljati v naknadno obdelavo – normalizirati, poudariti ključne besede in reducirati na zahtevane strukture. Ampak kako veš - kaj točno se je spremenilobrez kompliciranja sinhronizacijske kode, idealno ne da bi se je sploh dotaknili?
Če ima samo vaš proces dostop za pisanje v času sinhronizacije, potem lahko uporabite sprožilec, ki bo zbral vse spremembe namesto nas:
-- целевые таблицы
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;
Zdaj lahko uporabimo sprožilce pred začetkom sinhronizacije (ali jih omogočimo prek 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();
Nato mirno izvlečemo vse spremembe, ki jih potrebujemo, iz dnevniških tabel in jih poženemo skozi dodatne obdelovalce.
3.3. Uvažanje povezanih nizov
Zgoraj smo obravnavali primere, ko sta podatkovni strukturi vira in cilja enaki. Kaj pa, če ima nalaganje iz zunanjega sistema obliko, ki se razlikuje od strukture shranjevanja v naši bazi podatkov?
Vzemimo za primer shranjevanje strank in njihovih računov, klasično možnost »več proti enemu«:
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)
);
Toda prenos iz zunanjega vira pride k nam v obliki »vse v enem«:
Najprej izpostavimo tiste “reze”, na katere se nanašajo naša “dejstva”. V našem primeru se računi nanašajo na stranke:
CREATE TEMPORARY TABLE client_import AS
SELECT DISTINCT ON(client_inn)
-- можно просто SELECT DISTINCT, если данные заведомо непротиворечивы
client_inn inn
, client_name "name"
FROM
invoice_import;
Da lahko pravilno povežemo račune z ID-ji strank, moramo najprej poiskati ali ustvariti te identifikatorje. Pod njimi dodamo polja:
ALTER TABLE invoice_import ADD COLUMN client_id integer;
ALTER TABLE client_import ADD COLUMN client_id integer;
Uporabimo zgoraj opisano metodo sinhronizacije tabel z majhno spremembo - ne bomo posodobili ali izbrisali ničesar v ciljni tabeli, ker uvozimo odjemalce »samo za dodajanje«:
-- проставляем в таблице импорта 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; -- прикладной ключ
Pravzaprav je vse noter invoice_import Sedaj imamo kontaktno polje izpolnjeno client_id, s katerim bomo vstavili račun.