DBA: správne organizujeme synchronizácie a importy

Pri zložitej spracovaní veľkých súborov dát (rôzne ETL procesy: importy, konverzie a synchronizácie s externým zdrojom) často nastáva potreba dočasne 'zapamätať' a rýchlo spracovať niečo objemné.

Typická úloha tohto druhu znie zvyčajne takto: 'Tu účtovníctvo exportovalo z klient-banku posledné prijaté platby, treba ich rýchlo nahrať na web a spojiť s účtami.'

Ale keď objem tohto 'niečoho' začína dosahovať stovky megabajtov a služba musí naďalej fungovať s databázou v režime 24×7, vzniká množstvo vedľajších efektov, ktoré vám znepríjemnia život.
DBA: správne organizujeme synchronizácie a importy
Aby sme sa s nimi vysporiadali v PostgreSQL (a nielen tam), môžeme využiť niektoré možnosti optimalizácie, ktoré umožnia rýchlejšie spracovanie všetkého s menšou spotrebou zdrojov.

1. Kam nahrávať?

Najprv si určíme, kam môžeme nahrať dáta, ktoré chceme 'spracovať'.

1.1. Dočasné tabuľky (TEMPORARY TABLE)

V zásade sú dočasné tabuľky v PostgreSQL rovnaké ako akékoľvek iné tabuľky. Preto sú povrchné povesti ako 'tam sa všetko uchováva len v pamäti a tá môže skončiť.'Ale existujú aj niektoré kľúčové rozdiely.

Svoj 'namespace' pre každé pripojenie k DB

Ak sa dva pripojenia pokúsia súčasne vykonať CREATE TABLE x, tak niekto určite dostane chybu nejedinečnosti objektov DB.

Ale ak sa obaja pokúsia vykonať VYTVORIŤ DOČASNÁ TABUĽKA x, tak obaja to urobia bez problémov a každý dostane svoj vlastný exemplár tabuľky. A medzi nimi nebude nič spoločné.

'Sebazničenie' pri odpojení

Pri uzavretí pripojenia sa všetky dočasné tabuľky automaticky odstránia, preto 'manuálne' vykonávať DROP TABLE x nemá zmysel, okrem...

Ak pracujete cez pgbouncer v transakčnom režime, databáza síce naďalej považuje toto pripojenie za aktívne a dočasná tabuľka v ňom stále existuje.

Preto pokus o jej opätovné vytvorenie z iného pripojenia k pgbouncer povedie k chybe. Ale to sa dá obísť použítím VYTVOŘIT DOČASNOU TABULKU POKUD NEEXISTUJE x.

Pravda, ospravedlnenie, aby sa to nemalo robiť, pretože neskôr môžete 'náhodou' objaviť údaje zostávajúce od 'predchádzajúceho vlastníka'. Namiesto toho je oveľa lepšie prečítať si manuál a vidieť, že pri vytváraní tabuľky existuje možnosť dopísať PRI PRIHLÁSENÍ DROP — to znamená, že po skončení transakcie sa tabuľka automaticky odstráni.

Ne-archivácia

Pretože patrí iba k určitému pripojeniu, dočasné tabuľky sa nearchivujú. Namiesto toho to zbavuje potreby dvojakého zápisu dát do heap + WAL, takže INSERT/UPDATE/DELETE do nej sú podstatne rýchlejšie.

Ale pretože dočasná tabuľka je predsa len „takmer bežná“ tabuľka, ani na replikácii ju nie je možné vytvoriť. Aspoň zatiaľ, aj keď zodpovedajúci patch už dávno existuje.

1.2. Nezaznamenávané tabuľky (UNLOGGED TABLE)

Ale čo robiť, ak máte nejaký objemný ETL proces, ktorý nie je možné realizovať v rámci jednej transakcie, a máte pgbouncer v transakčnom režime?..

Alebo dátový tok je taký veľký, že kapacita jedného pripojenia k DB (t.j. jedného procesu na CPU) nie je dostatočná?.. Alebo časť operácií prebieha

asynchrónne v rôznych pripojeniach?.. Tu je len jedna možnosť —

dočasne vytvoriť ne-dočasnú tabuľku . Hračka, že? Takže:vytvoril „svoje“ tabuľky s maximálne náhodnými názvami, aby sa s nikým neprekrývali

  • Extrahovať
  • : nahral do nich dáta z externého zdrojaTransformovať
  • : upravil, vyplnil kľúčové prepojené polia: preniesol pripravené dáta do cieľových tabuliek
  • Loadvymazal „svoje“ tabuľky
  • A teraz — lyžica dechtu. V podstate,

celý zápis v PostgreSQL prebieha dvakrát najskôr do WAL — , potom už do telies tabuliek/ indexov. Toto všetko bolo vykonané na podporu ACID a správnej viditeľnosti dát medzivnútornými a COMMITvnútornými transakciami. ROLLBACKAle nám to nie je potrebné! Náš celý proces

buď úspešne prebehol, alebo nie . Nezáleží na tom, koľko bude mať medziľahlých transakcií — nezaujíma nás „pokračovať v procese od polovice“, najmä keď nie je jasné, kde bola.Na to vývojári PostgreSQL ešte vo verzii 9.1 zaviedli takú vec, ako

nezaznamenávané (UNLOGGED) tabuľky S týmto označením sa tabuľka vytvorí ako nezaznamenávaná. Dáta zapisované do nezaznamenávaných tabuliek neprechádzajú cez predzáznamový záznam (pozri Kapitolu 29), v dôsledku čoho takéto tabuľky:

fungujú oveľa rýchlejšie ako bežné . Avšak, nie sú chránené pred poruchou; pri zlyhaní alebo havarijnom vypnutí servera sa nezaznamenávaná tabuľkaautomaticky odstredí. . Okrem toho, obsah nezaznamenávanej tabuľkysa nearchivuje. не реплицируется na riadené servery. Akékoľvek indexy vytvorené pre nezaúčtovanú tabuľku sa automaticky stávajú nezaúčtovanými.

Krátko, bude výrazne rýchlejšie, ale ak server DB "spadne" — bude to nepríjemné. Ale stáva sa to často a vie váš ETL proces správne dokončiť "z prostriedku" po "oživení" DB?..

Ak nie, a váš prípad je podobný vyššie uvedenému — použite UNLOGGED, ale nikdy nezapínajte tento atribút na reálnych tabuľkách, z ktorých sú dáta pre vás cenné.

1.3. ON COMMIT { DELETE ROWS | DROP }

Táto konštrukcia umožňuje pri vytváraní tabuľky nastaviť automatické správanie pri ukončení transakcie.

O PRI PRIHLÁSENÍ DROP už som napísal vyššie, generuje to DROP TABLE, ale situácia s PRI PRIHLÁSENÍ ZMAZAŤ RIADKY je zaujímavejšia — tu sa generuje TRUNCATE TABLE.

Keďže celá infraštruktúra ukladania metapopisov dočasnej tabuľky je presne rovnaká ako u bežnej, neustále vytváranie a odstraňovanie dočasných tabuliek vedie k výraznému "nabubreniu" systémových tabuliek pg_class, pg_attribute, pg_attrdef, pg_depend,…

Teraz si predstavte, že máte pracovníka na priamom spojení s DB, ktorý každú sekundu otvára novú transakciu, vytvára, napĺňa, spracováva a odstraňuje dočasnú tabuľku… Odpadu v systémových tabuľkách sa nahromadí dostatok, a to spôsobuje zbytočné spomalenie pri každej operácii.

Celkovo, nerobte to! V tomto prípade je oveľa efektívnejšie CREATE TEMPORARY TABLE x ... ON COMMIT DELETE ROWS vyniesť to mimo cyklus transakcií — vtedy na začiatku každej novej transakcie tabuľky už budú existovať (šetríme volanie VYTVORIŤ), ale bude prázdna, vďaka TRUNCATE (jeho volanie sme tiež ušetrili) pri ukončení predchádzajúcej transakcie.

1.4. LIKE… INCLUDING …

Už som spomenul na začiatku, že jedným z typických praktických využití pre dočasné tabulky sú rôzne druhy importov — a vývojár unavene kopíruje zoznam polí cieľovej tabuľky do vyhlásenia svojej dočasnej…

Ale lenivosť je motor pokroku! Preto vytvoriť novú tabuľku "podľa vzoru" je oveľa jednoduchšie:

CREATE TEMPORARY TABLE import_table(
  LIKE target_table
);

Keďže do tejto tabuľky je možné generovať naozaj veľa dát, hľadanie v nej sa stane rozhodne nepríjemným. Ale proti tomu existuje tradičné riešenie — indexy! A, áno, dočasná tabuľka môže mať tiež indexy.

Keďže často potrebné indexy sú rovnaké ako indexy cieľovej tabuľky, jednoducho môžete napísať PODOBNÉ ako target_table ZAHŔŇAJÚC INDEXY.

Ak potrebujete aj DEFAULT-hodnoty (napríklad na vyplnenie hodnôt primárneho kľúča) môžete využiť PODOBNÉ ako target_table VRÁTANE VYHODNOCOV. Alebo jednoducho — PODOBNÉ ako target_table VRÁTANE VŠETKÉHO — skopíruje predvolené hodnoty, indexy, obmedzenia,…

Ale tu už treba chápať, že ak ste vytvorili importnú tabuľku rovno s indexmi, dáta sa budú vkladať dlhšie, ako keby ste najprv vložili všetky dáta a potom aplikovali indexy — pozrite sa na príklad, ako to robí pg_dump.

V podstate, RTFM!

2. Ako písať?

Poviem to jednoducho — používajte KOPÍROVAŤ-potok namiesto „balíka“ INSERT, urýchlenie v násobkoch. Môžete dokonca priamo z predtým vytvoreného súboru.

3. Ako spracovávať?

Takže, nech je naša vstupná situácia približne takáto:

  • máte v databáze uloženú tabuľku s údajmi o klientoch na 1M záznamov
  • každý deň vám klient posiela nový úplný „obraz“
  • na základe skúsenosti viete, že sa zakaždým mení maximálne 10K záznamov

Klasickým príkladom takejto situácie je databáza KĽADR — adresov je veľa, ale v každej týždennej exporte zmien (prezývanie obcí, zlúčenie ulíc, vznik nových domov) je veľmi málo, dokonca aj v meradle celej krajiny.

3.1. Algoritmus úplnej synchronizácie

Pre jednoduchosť predpokladajme, že vám ani nemusí preštruktúrovať dáta — stačí upraviť tabuľku do požadovaného tvaru, to znamená:

  • vymazať všetko, čo už neexistuje
  • aktualizovať všetko, čo už existovalo a treba aktualizovať
  • vložiť všetko, čo ešte neexistovalo

Prečo robiť operácie práve v tomto poradí? Pretože práve tak sa veľkosť tabuľky zvýši minimálne (nezabúdajte na MVCC!).

DELETE FROM dst

Nie, samozrejme, môžete sa obísť len dvoma operáciami:

  • vymazať (DELETE) celkom všetko
  • vložiť všetko z nového obrazu

Ale pritom, vďaka MVCC, sa veľkosť tabuľky zvýši presne na dvojnásobok! Získať +1M obrazov záznamov v tabuľke kvôli aktualizácii 10K — to je dosť nadbytočné…

TRUNCATE dst

Skúsenejší vývojár vie, že celú tabuľku možno veľmi lacno vyčistiť:

  • vyčistiť (TRUNCATE) celú tabuľku
  • vložiť všetko z nového obrazu

Metóda je účinná, niekedy veľmi aplikovateľná, ale je tu problém… Vkladať 1M záznamov budeme dlho, preto si nemôžeme dovoliť mať tabuľku prázdnu počas celého tohto času (ako sa stane bez zahalenia do jednej transakcie).

To znamená:

  • začíname dlhú transakciu
  • TRUNCATE uloží AccessExclusive-blokovanie
  • dlho vykonávame vloženie, a všetci ostatní zatiaľ ani nemôžu SELECT

Niečo nevychádza dobre…

ALTER TABLE… RENAME… / DROP TABLE …

Ako variant — nahrať všetko do novej tabuľky a potom ju jednoducho premenovať na miesto starej. Niekoľko nepríjemných drobností:

  • takisto aj AccessExclusive, aj keď podstatne menej časovo náročné
  • vymažú sa všetky plány dopytov/statistika tejto tabuľky, musíme spustiť ANALYZE
  • rozpadnú sa všetky cudzí kľúče (FK) na tabuľku

Bol WIP-patch od Simona Riggsa, ktorý navrhol urobiť ALTER-operáciu na výmenu tela tabuľky na úrovni súboru, bez zasahovania do štatistiky a FK, ale nezískal kvórum.

DELETE, UPDATE, INSERT

Takže sa zastavíme na neblokovacej variante z troch operácií. Takmer troch… Ako to urobiť čo najefektívnejšie?

-- všetko robíme v rámci transakcie, aby nikto nevidel "medzistavy"
BEGIN;

-- vytvoríme dočasnú tabuľku s importovanými údajmi
CREATE TEMPORARY TABLE tmp(
  LIKE dst INCLUDING INDEXES -- podľa vzoru, spolu s indexmi
) ON COMMIT DROP; -- mimo transakcie ju nepotrebujeme

-- rýchlo vložíme nový obraz cez COPY
COPY tmp FROM STDIN;
-- ...
-- .

-- vymažeme chýbajúce
DELETE FROM
  dst D
USING
  dst X
LEFT JOIN
  tmp Y
    USING(pk1, pk2) -- polia primárneho kľúča
WHERE
  (D.pk1, D.pk2) = (X.pk1, X.pk2) AND
  Y IS NOT DISTINCT FROM NULL; -- "antijoin"

-- aktualizujeme zvyšné
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); -- nie je potrebné aktualizovať zhodujúce sa

-- vkladáme chýbajúce
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. Postprocessing importu

Vo všetkých zmenených záznamoch v KLADRi je potrebné dodatočne prejsť postprocessingom — normalizovať, vyzdvihnúť kľúčové slová, doniesť do požadovaných štruktúr. Ale ako zistiť — čo presne sa menilo, pričom kód synchronizácie sa týmto zbytočne nezložití, ideálne ho vôbec nezasahujeme?

Ak je v čase synchronizácie prístup na zápis len vo vašom procese, môžeme využiť spúšťač, ktorý zbierajú všetky zmeny:

-- ciele tabulky
CREATE TABLE kladr(...);
CREATE TABLE kladr_house(...);

-- tabulky s historií změn
CREATE TABLE kladr$log(
  ro kladr, -- zde leží celé obrazy záznamů staré/nové
  rn kladr
);

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

-- obecná funkce pro logování změn
CREATE OR REPLACE FUNCTION diff$log() RETURNS trigger AS $$
DECLARE
  dst varchar = TG_TABLE_NAME || '$log';
  stmt text = '';
BEGIN
  -- kontrolujeme potřebu logování při aktualizaci záznamu
  IF TG_OP = 'UPDATE' THEN
    IF NEW IS NOT DISTINCT FROM OLD THEN
      RETURN NEW;
    END IF;
  END IF;
  -- vytváříme záznam logu
  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;

Teraz môžeme pred začiatkom synchronizácie nasadiť (alebo aktivovať cez 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();

A potom pokojne z log-tabúľ vyberáme všetky potrebné zmeny a spracovávame ich cez dodatočné spracovatele.

3.3. Import súvisiacich množín

Vyššie sme sa zaoberali prípadmi, keď štruktúry dát zdroja a príjemcu sú identické. Ale čo robiť, ak export z vonkajšieho systému má formát odlišný od štruktúry, ktorú máme v databáze?

Vezmime si za príklad ukladanie klientov a ich faktúr, klasický prípad „mnohí-k-jednému“:

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)
);

A export z externého zdroja prichádza v podobe „všetko v jednom“:

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

Je zrejmé, že údaje o klientoch sa môžu v takomto variante dupliciť, a základný záznam predstavuje „faktúra“:

0123456789;Václav;A-01;2020-03-16;1000.00
9876543210;Petr;A-02;2020-03-16;666.00
0123456789;Václav;B-03;2020-03-16;9999.00

Pre model jednoducho vložíme naše testovacie údaje, ale pamätajme — KOPÍROVAŤ efektívnejšie!

INSERT INTO invoice_import
VALUES
  ('0123456789', 'Václav', 'A-01', '2020-03-16', 1000.00)
, ('9876543210', 'Petr', 'A-02', '2020-03-16', 666.00)
, ('0123456789', 'Václav', 'B-03', '2020-03-16', 9999.00);

Najprv vyberieme tie „rezne“, na ktoré naše „fakty“ odkazujú. V našom prípade faktúry odkazujú na klientov:

VYTVORTE DOČASNÚ TABUĽKU client_import AS
SELECT DISTINCT ON(client_inn)
-- je možné jednoducho SELECT DISTINCT, ak sú údaje jednoznačne nezrozumiteľné
  client_inn inn
, client_name "name"
FROM
  invoice_import;

Aby sme správne prepojili faktúry s ID klientov, musíme najprv tieto identifikátory získať alebo vygenerovať. Pridáme pre ne polia:

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

Využijeme vyššie opísaný spôsob synchronizácie tabuliek s malou úpravou — nebudeme nič aktualizovať a odstraňovať v cielovej tabuľke, pretože import klientov máme „append-only”:

-- priraďujeme v tabuľke importu ID už existujúcich záznamov
UPDATE
  client_import T
SET
  client_id = D.client_id
FROM
  client D
WHERE
  T.inn = D.inn; -- unikátne kľúč

-- vložíme chýbajúce záznamy a priraďujeme ich ID
WITH ins AS (
  INSERT INTO client(
    inn
  , name
  )
  SELECT
    inn
  , name
  FROM
    client_import
  WHERE
    client_id IS NULL -- ak ID nebolo priradené
  RETURNING *
)
UPDATE
  client_import T
SET
  client_id = D.client_id
FROM
  ins D
WHERE
  T.inn = D.inn; -- unikátne kľúč

-- priraďujeme ID klientov k záznamom faktúr
UPDATE
  invoice_import T
SET
  client_id = D.client_id
FROM
  client_import D
WHERE
  T.client_inn = D.inn; -- aplikovaný kľúč

Vlastne, všetko — v invoice_import teraz máme vyplnené pole väzby client_id, s ktorým vložíme faktúru.

Zdroj: habr.com

Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster