DBA: ügyesen megszervezzük a szinkronizálásokat és importálásokat

Nagy adathalmazok bonyolult feldolgozása során (különböző ETL-folyamatok: importálások, átalakítások és szinkronizálások külső forrással) gyakran felmerül az igény ideiglenesen „memorizálni”, majd gyorsan feldolgozni valami nagyméretűt.

Az ilyen típusú feladatok általában a következőképpen hangzanak: „Itt van, hogy a könyvelés letöltötte az ügyfélbankból a legutóbbi beérkezett kifizetéseket, ezeket gyorsan fel kell tölteni a weboldalra és hozzá kell rendelni a számlákhoz”

De amikor ennek a „valaminek” a mérete száz megabájtokban mérhető, és a szolgáltatásnak továbbra is 24×7 üzemmódban kell működnie, számos mellékhatás lép fel, amelyek megnehezítik az életét.
DBA: ügyesen megszervezzük a szinkronizálásokat és importálásokat
A PostgreSQL-ben (és nem csak benne) ezek kezelésére használhatunk bizonyos optimalizálási lehetőségeket, amelyek lehetővé teszik a gyorsabb feldolgozást, kevesebb erőforrás-felhasználással.

1. Hová töltsük?

Először is, határozzuk meg, hová tölthetjük fel azokat az adatokat, amelyeket „feldolgozni” szeretnénk.

1.1. Ideiglenes táblák (TEMPORARY TABLE)

Alapvetően a PostgreSQL-ben az ideiglenes táblák ugyanolyan táblák, mint bármely más. Ezért tévhit, hogy „minden csak a memóriában tárolódik, és az elfogyhat”. De van néhány lényeges különbség is.

Saját „névtér” minden adatbázis-kapcsolathoz

Ha két kapcsolat egyszerre próbálja végrehajtani CREATE TABLE x, akkor valamelyikük biztosan kapni fog egy egyediségsértési hibát az adatbázisobjektumokkal kapcsolatban.

De ha mindketten megpróbálják végrehajtani CREATE ÁTMENETI TÁBLA x, akkor mindketten normálisan végrehajthatják, és mindkettő megkapja a saját példányát a táblából. És semmi közös nem lesz közöttük.

Öngyilkosság disconnect esetén

A kapcsolat lezárásakor minden ideiglenes tábla automatikusan törlődik, így „kézzel” végrehajtani DROP TABLE x nincs értelme, kivéve…

Ha a pgbouncer-t tranzakciós módban használja, akkor az adatbázis továbbra is úgy gondolja, hogy ez a kapcsolat még mindig aktív, és ebben az ideiglenes táblának továbbra is léteznie kell.

Ezért a tábla második alkalommal való létrehozásának megkísérlése egy másik pgbouncer kapcsolatról hibát fog eredményezni. De ezt meg lehet kerülni, ha igénybe vesszük IDEIGLENES TÁBLÁZAT LÉTREHOZÁSA HA NEM LÉTEZIK x.

Azt viszont jobb elkerülni, mert így „váratlanul” felfedezhetjük, hogy ott maradtak a „korábbi tulajdonos” adatai. Ehelyett sokkal jobb lenne elolvasni a kézikönyvet, és megnézni, hogy a tábla létrehozásakor van lehetőség a kiegészítésére KÖTELEZETTSÉG ALAPJÁN DROP — azaz a tranzakció befejezésekor a táblázat automatikusan törlésre kerül.

Nem-replikáció

Csak egy bizonyos kapcsolathoz való tartozás miatt a ideiglenes táblák nem replikálódnak. Viszont ez megszabadít a kettős adatrögzítés szükségességétől heap + WAL, ezért az INSERT/UPDATE/DELETE jelentősen gyorsabb benne.

De mivel az ideiglenes táblázat, ha úgy tetszik, "majdnem egyszerű" táblázat, így a replikán sem hozható létre. Legalábbis most, bár a megfelelő patch már régóta kering.

1.2. Nemsorolt táblák (UNLOGGED TABLE)

De mi a helyzet, ha van egy terjedelmes ETL-folyamata, amit nem lehet megvalósítani egy tranzáción belül, és van pgbouncer-t tranzakciós módban használja?..

Vagy az adatfolyam olyan nagy, hogy egy kapcsolat sávszélessége nem elegendő az adatbázissal (értsd: egy CPU-n lévő folyamat)?..

Vagy egyes műveletek aszinkron módon különböző kapcsolatokban zajlanak?..

Itt csak egy lehetőség van — ideiglenes nem-ideiglenes táblát létrehozni. Szóvicc, igen. Azaz:

  • létrehoztam „saját” tábláimat a lehető legesetlegesebb nevekkel, hogy senkivel ne találkozzak
  • Extract: adatokat töltöttem fel belső forrásokból
  • Transform: átalakítottam, kitöltöttem a kulcsmeghatározó mezőket
  • Betöltés: az előkészített adatokat átmásoltam a cél táblázatokba
  • töröltem a „saját” tábláimat

És most — egy kanál kátrány. Valójában az összes rögzítés a PostgreSQL-ben kétszer történik — először a WAL-ba, majd a táblázatok/testekbe. Mindez az ACID támogatása és az adatok helyes láthatósága érdekében készült COMMITa folyamatok között és a ROLLBACKfolyamatok között.

De nekünk erre nincs szükségünk! A folyamatunk vagy teljes egészében sikeresen véget ért, vagy nem. Nem számít, hány közbenső tranzakció lesz benne — minket nem érdekel a „folytatás a folyamat közepétől”, különösen, ha nem világos, hol tartott.

Erre a PostgreSQL fejlesztői már a 9.1-es verzióban bevezettek egy dolgot, mint a nem naplózott (UNLOGGED) táblák:

Ezzel a megjelöléssel a táblázat nem naplózottként jön létre. A nem naplózott táblákba írt adatok nem mennek át a naplózott írás (lásd 29. fejezet) révén, ami miatt ezek a táblák jóval gyorsabban működnek, mint a normál. Azonban nem védettek a hibákkal szemben; áramkimaradás vagy vészhelyzeti leállás esetén a nem naplózott táblázat automatikusan eltűnik.. Ezen kívül a nem naplózott táblázat tartalma nem replikálódik. a vezére serverekre. Bármilyen index, amely a nem naplózott táblához létrejön, automatikusan nem naplózottá válik.

Röviden, nagyon sokkal gyorsabb lesz, de ha az adatbázis szerver "összeomlik" - kellemetlen lesz. De gyakran előfordul ez, és képes-e az ETL folyamatod helyesen kezelni a dolgokat "a közepéből" az adatbázis "újjáélesztése" után?..

Ha mégsem, és a fenti eset hasonlít a tiédre - használj UNLOGGED, de soha ne használd ezt a tulajdonságot valódi táblákra, melyek adatai fontosak számodra.

1.3. ON COMMIT { DELETE ROWS | DROP }

Ez a konstrukció lehetővé teszi, hogy a tábla létrehozásakor automatikus viselkedést adjunk meg a tranzakció befejezésekor.

A KÖTELEZETTSÉG ALAPJÁN DROP már írtam fent, ezt generálja DROP TABLE, de itt a KÖTELEZETTSÉG ALAPJÁN SOROZAT TÖRLÉSE helyzet érdekesebb - itt generálódik TRUNCATE TABLE.

Mivel a meta-adattároló ideiglenes táblája pontosan ugyanolyan infrastruktúrával rendelkezik, mint a normál táblák, ezért az ideiglenes táblák folyamatos létrehozása és törlése a rendszer táblák "megbővülését" eredményezi pg_class, pg_attribute, pg_attrdef, pg_depend,…

Most képzeld el, hogy van egy munkás, aki közvetlen kapcsolatban van az adatbázissal, és másodpercenként új tranzakciót nyit, létrehoz, feltölt, feldolgoz és töröl egy ideiglenes táblát... A szemét a rendszer táblákban bőségesen felhalmozódik, ez pedig felesleges lassításhoz vezet minden művelet során.

Összességében, ne csináld ezt! Ebben az esetben sokkal hatékonyabb CREATE TEMPORARY TABLE x ... ON COMMIT DELETE ROWS kivinni a tranzakciós cikluson kívül - akkor az új tranzakció kezdete előtt a táblák már létezni fognak (megtakarítjuk a hívást CREATE), de üres lesz, hála TRUNCATE (ennek a hívása is megtakarítottuk) a korábbi tranzakció befejezésekor.

1.4. LIKE… INCLUDING …

Az elején említettem, hogy az ideiglenes táblák tipikus felhasználási esete - különböző importok - és a fejlesztő fáradtan másolja a cél tábla mezőlistáját az ideiglenes tábla deklarációjába...

De a lustaság a fejlődés mozgatórugója! Ezért egy új táblát "mint minta" sokkal egyszerűbben létrehozhatsz:

CREATE TEMPORARY TABLE import_table(
  LIKE target_table
);

Mivel ebbe a táblába később nagyon sok adatot be lehet generálni, a keresés benne nem lesz éppen gyors. De erre van egy hagyományos megoldás - indexek! Igen, az ideiglenes táblának is lehetnek indexei.

Mivel gyakran a szükséges indexek megegyeznek a cél tábla indexeivel, egyszerűen megírhatod HASONLÓ target_table TARTALMAZZA AZ INDEXEKET.

Ha további DEFAULT-értékek (például a primer kulcs értékeinek kitöltéséhez) használhatók HASONLÓ target_table TARTALMAZZA AZ ALAPÉRTÉKEKET. Vagy csak — HASONLÓ target_table MINDEN BELEÉRTVE — másolja az alapértelmezett értékeket, indexeket, kényszereket,…

De itt már meg kell érteni, hogy ha létrehoztál import táblát az indexekkel, akkor az adatok betöltése hosszabb ideig tart, mint ha először minden adatot betöltesz, és csak később alkalmazod az indexeket — nézd meg példaként, hogyan csinálja pg_dump.

Összességében, OLVASD EL A HASZNÁLATI ÚTMUTATÓT!

2. Hogyan írjunk?

Egyszerűen mondom — használj COPY-folyamot a "csomag" helyett INSERT, a sebesség jelentős növelésére. Akár közvetlenül a előre elkészített fájlból is.

3. Hogyan dolgozzunk fel?

Tegyük fel, hogy a bevezetőnk körülbelül így néz ki:

  • a rendszeredben van egy tábla az ügyféladatokkal 1M rekorddal
  • minden nap az ügyfél új teljes "képét"
  • tapasztalataid alapján tudod, hogy az esetek többségében maximálisan 10K rekord változik

A klasszikus példa egy ilyen helyzetre a KLDAR adatbázis — rengeteg cím található, de minden heti kiadásban a változások között (településnevek átnevezése, utcák egyesítése, új házak megjelenése) nagyon kevés van az egész ország méretéhez képest.

3.1. Teljes szinkronizálási algoritmus

Egyszerűség kedvéért tegyük fel, hogy még az adatok átalakítására sincs szükség — egyszerűen csak a táblázatot kell a kívánt formára hozni, így:

  • törölni minden, ami már nincs
  • a rendszert a p9 ágon a "Sisyphus" tartományból. Az új vállalati felhasználók számára lehetőség van tesztverziók beszerzésére, míg a magánszemélyek számára a "Basalt SPO" weboldaláról vagy az új letöltő oldalról a "getalt.ru" webhelyről hagyományosan ingyenesen letölthető a szükséges "Alt" operációs rendszer verzió. minden, ami már megvolt, és frissíteni kell
  • beszúrni minden, ami még nem volt

Miért éppen ilyen sorrendben kell a műveleteket végezni? Mert így növekszik a táblázat mérete minimálisan (ne feledd a MVCC-t!).

TÖRÖLD FROM dst

Nem, természetesen át lehet ugrani két műveletet:

  • törölni (DELETE) tényleg minden
  • beszúrni minden a új képben

De ezzel együtt, a MVCC-nek köszönhetően, a táblázat mérete pontosan duplájára nő! Kapni +1M rekordot a táblázatban a 10K frissítése miatt nem éppen optimális redundancia...

TRUNCATE dst

A tapasztalt fejlesztők tudják, hogy az egész táblázat elég olcsón tisztítható:

  • clear (TRUNCATE) Az egész táblát
  • beszúrni minden a új képben

Hatékony módszer, néha teljes mértékben alkalmazható, de van egy probléma… 1M rekord bevitele sok időt vesz igénybe, ezért nem engedhetjük meg magunknak, hogy a táblázat üres maradjon az összes ebben az időben (ahogy történne, ha nem csomagolnánk egy tranzakcióba).

Ezért:

  • kezdődik egy hosszú tranzakció
  • TRUNCATE rávonatkozik Kizáró hozzáférés-zárolás
  • hosszú ideig tárolunk, miközben minden más még azt sem tudja SELECT

Valami nem áll jól…

ALTER TABLE… RENAME… / DROP TABLE …

Egy alternatíva, hogy mindent feltöltenénk egy új táblába, majd egyszerűen átnevezzük a régit. Néhány kellemetlen apróság:

  • ez is Kizáró hozzáférés, bár időben lényegesen kevesebb
  • minden lekérdezési terv/tiltott táblázati statisztika nullázódik, futtatni kell az ANALYZE-t
  • megszakadnak az összes külső kulcs (FK) a táblára

Volt egy WIP-patch Simon Riggs-től, amely azt javasolta, hogy ALTER-művelet a tábla fájlszintű felcserélésére, anélkül, hogy a statisztikát és a FK-t megérintené, de nem gyűjtötte össze a szükséges szavazati kört.

DELETE, UPDATE, INSERT

Így a három művelet blokkolás nélküli változatánál maradunk. Szinte hármat… Hogyan tudjuk ezt a leghatékonyabban megtenni?

-- mindent tranzakciókon belül végzünk, hogy senki ne láthassa a "köztes" állapotokat
BEGIN;

-- létrehozunk egy ideiglenes táblát a betöltött adatokkal
CREATE TEMPORARY TABLE tmp(
  LIKE dst INCLUDING INDEXES -- hasonlóan a régi táblához, indexekkel együtt
) ON COMMIT DROP; -- a tranzakción kívül már nincs szükségünkre

-- gyorsan betöltjük az új képet a COPY segítségével
COPY tmp FROM STDIN;
-- ...
-- .

-- eltávolítjuk a hiányzókat
DELETE FROM
  dst D
USING
  dst X
LEFT JOIN
  tmp Y
    USING(pk1, pk2) -- elsődleges kulcs mezők
WHERE
  (D.pk1, D.pk2) = (X.pk1, X.pk2) AND
  Y IS NOT DISTINCT FROM NULL; -- "anti join"

-- frissítjük a megmaradtakat
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); -- nincs értelme frissíteni a megegyezőket

-- beszúrjuk a hiányzókat
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. Az import utófeldolgozása

Ugyanabban a KML-ben minden megváltozott rekordot további post-processzálásnak kell alávetni – normalizálni, kiemelni a kulcsszavakat, megfelelő struktúrákba hozni. De hogyan lehet megtudni— mi változott, anélkül, hogy bonyolítanánk a szinkronizálási kódot, lényegében ne is értsünk hozzá?

Ha a szinkronizálás alatt csak az Ön folyamata rendelkezik írási jogosultsággal, akkor egy trigger segítségével összegyűjthetjük az összes változást:

-- céltáblák
CREATE TABLE kladr(...);
CREATE TABLE kladr_house(...);

-- történeti változások táblái
CREATE TABLE kladr$log(
  ro kladr, -- itt állnak az állandó rekordok régi/új másolatai
  rn kladr
);

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

-- általános változásnaplózó függvény
CREATE OR REPLACE FUNCTION diff$log() RETURNS trigger AS $$
DECLARE
  dst varchar = TG_TABLE_NAME || '$log';
  stmt text = '';
BEGIN
  -- ellenőrizzük a naplózás szükségességét a rekord frissítésekor
  IF TG_OP = 'UPDATE' THEN
    IF NEW IS NOT DISTINCT FROM OLD THEN
      RETURN NEW;
    END IF;
  END IF;
  -- létrehozzuk a naplóbejegyzést
  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;

Most már a szinkronizálás megkezdése előtt trigger-eket alkalmazhatunk (vagy bekapcsolhatjuk azokat 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();

Aztán nyugodtan kivonhatunk az log táblákból minden szükséges változást, és átvezethetjük azokat a további feldolgozókra.

3.3. Kapcsolódó készletek importálása

Fent azt a helyzetet vizsgáltuk, amikor a forrás és a céladatok szerkezete megegyezik. De mi történik, ha a külső rendszertől való exportálás formátuma eltér a nálunk tárolt struktúrától?

Vegyünk példaként egy ügyfél és a hozzájuk kapcsolódó számlák tárolását, klasszikus «sok-az-egyhez» esetként:

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

Azonban a külső forrástól érkező export «minden egyben» formában érkezik:

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

Nyilvánvaló, hogy az ügyfelek adatai duplázódhatnak ilyen formátumban, és a fő bejegyzés a «számla»:

0123456789;Vasya;A-01;2020-03-16;1000.00
9876543210;Petya;A-02;2020-03-16;666.00
0123456789;Vasya;B-03;2020-03-16;9999.00

A modellhez egyszerűen beszúrjuk a tesztadatainkat, de emlékezzünk — COPY hatékonyabban!

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

Először kiemeljük azokat a «szeleteket», amelyekre a mi «tények» hivatkoznak. Ebben az esetben a számlák az ügyfelekre hivatkoznak:

Hozzunk létre egy ideiglenes táblázatot az ügyfélimporthoz
Kiválasztva az ügyfél-inn-ek egyedi rekordjait
-- egyszerűen használhatjuk a SELECT DISTINCT-ot, ha az adatok biztosan nem ellentmondásosak
  client_inn inn
, client_name "name"
FROM
  invoice_import;

Ahhoz, hogy a számlákat helyesen összekapcsoljuk az ügyfél-azonosítókkal, először meg kell ismernünk vagy generálnunk ezeket az azonosítókat. Adjunk hozzá mezőket hozzájuk:

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

A fent leírt táblázat-szinkronizálási módszert fogjuk alkalmazni egy kis változtatással — nem frissítünk és nem távolítunk el semmit a cél táblázatban, hiszen az ügyfelek importja számunkra „csak hozzáadás”:

-- megadjuk az import táblázatban a már létező rekordok ID-ját
UPDATE
  client_import T
SET
  client_id = D.client_id
FROM
  client D
WHERE
  T.inn = D.inn; -- egyedi kulcs

-- beszúrjuk a hiányzó rekordokat és megadjuk az ID-jukat
WITH ins AS (
  INSERT INTO client(
    inn
  , name
  )
  SELECT
    inn
  , name
  FROM
    client_import
  WHERE
    client_id IS NULL -- ha az ID nem lett megadva
  RETURNING *
)
UPDATE
  client_import T
SET
  client_id = D.client_id
FROM
  ins D
WHERE
  T.inn = D.inn; -- egyedi kulcs

-- megadjuk az ügyfél ID-ját a számla rekordoknál
UPDATE
  invoice_import T
SET
  client_id = D.client_id
FROM
  client_import D
WHERE
  T.client_inn = D.inn; -- alkalmazási kulcs

Tulajdonképpen ennyi — a invoice_import most már ki van töltve a kapcsolat mező client_id, amellyel be fogjuk illeszteni a számlát.

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster