DBA: vi organiserar synkroniseringar och importer pÄ ett smart sÀtt

Vid komplex bearbetning av stora datamÀngder (olika ETL-processer: importer, konverteringar och synkronisering med externa kÀllor) uppstÄr ofta behovet att temporÀrt "komma ihÄg", och snabbt bearbeta nÄgot omfattande.

En typisk uppgift av detta slag lÄter oftast sÄ hÀr: "HÀr har bokföringen laddat ner senaste betalningar frÄn kundens bank som snabbt mÄste laddas upp pÄ webbplatsen och kopplas till kontona".

Men nÀr volymen av detta "nÄgot" börjar mÀtas i hundratals megabyte, och tjÀnsten mÄste fortsÀtta att arbeta med databasen dygnet runt, uppstÄr mÄnga bieffekter som kommer att förstöra din tillvaro.
DBA: vi organiserar synkroniseringar och importer pÄ ett smart sÀtt
För att hantera dem i PostgreSQL (och Àven i andra system) kan man anvÀnda vissa möjligheter för optimeringar som gör att allt bearbetas snabbare och med mindre resursförbrukning.

1. Var ska vi ladda?

Först, lÄt oss klargöra vart vi kan ladda data som vi vill "bearbeta".

1.1. TemporÀra tabeller (TEMPORARY TABLE)

I princip Àr temporÀra tabeller i PostgreSQL precis som vanliga tabeller. DÀrför Àr vidskepligheten om att "de lagras bara i minnet, och det kan ta slut"felaktig. Men det finns nÄgra betydande skillnader.

Ett eget "namnrum" för varje anslutning till databasen

Om tvÄ anslutningar försöker samtidigt utföra CREATE TABLE x, sÄ kommer nÄgon garanterat fÄ en unikhetsfel för databaskonton.

Men om bĂ„da försöker utföra TĂ€nk pĂ„ förkortningar. TILLFÄLIG TABELL x, sĂ„ kommer bĂ„da att lyckas, och varje kommer att fĂ„ sin egen instans av tabellen. Och ingenting kommer att vara gemensamt mellan dem.

"SjÀlvförstörelse" vid avbrott

Vid stĂ€ngning av anslutningen tas alla temporĂ€ra tabeller automatiskt bort, sĂ„ det finns ingen mening med att "manuellt" utföra DROP TABLE x utom


Om du arbetar genom pgbouncer i transaktionslÀge, sÄ anser databasen att anslutningen fortfarande Àr aktiv, och i denna anslutning existerar fortfarande denna temporÀra tabell.

DĂ€rför kommer ett försök att skapa den igen, redan frĂ„n en annan anslutning till pgbouncer, att leda till ett fel. Men detta kan omgĂ„s genom att anvĂ€nda SKAPA TILLFÄLLIG TABELL OM DET INTE FINNS x.

Men det Ă€r bĂ€ttre att inte göra sĂ„, eftersom man dĂ„ kan "ovĂ€ntat" upptĂ€cka data dĂ€r, som tillhörde "föregĂ„ende Ă€gare". IstĂ€llet Ă€r det mycket bĂ€ttre att lĂ€sa manualen och se att nĂ€r man skapar en tabell finns det möjlighet att skriva till. PÅ ÅTAGANDE DROP — det vill sĂ€ga vid avslutandet av transaktionen kommer tabellen att tas bort automatiskt.

Icke-replikering

Eftersom de endast tillhör en viss anslutning, replikeras inte temporÀra tabeller. Men detta eliminerar behovet av dubbel skrivning av data till heap + WAL, sÄ INSERT/UPDATE/DELETE till dem Àr avsevÀrt snabbare.

Men eftersom en temporĂ€r tabell fortfarande Ă€r en "nĂ€stan vanlig" tabell, kan den inte skapas pĂ„ replikan heller. Åtminstone inte Ă€nnu, Ă€ven om en passande patch har funnits lĂ€nge.

1.2. Icke-journaliserade tabeller (UNLOGGED TABLE)

Men vad gör man om man till exempel har en krÄnglig ETL-process som inte kan genomföras i en enda transaktion, och ni har faktiskt pgbouncer i transaktionslÀge?..

Eller datastreamen Àr sÄ stor att en enda anslutning inte har tillrÀcklig kapacitet till DB (lÀs, en process per CPU)?..

Eller en del operationer gÄr asynkront i olika kopplingar?..

HĂ€r finns bara ett alternativ — att tillfĂ€lligt skapa en icke-temporĂ€r tabell. En kalambur, visst. Det vill sĂ€ga:

  • skapa "egna" tabeller med maximalt slumpmĂ€ssiga namn, sĂ„ att man inte krockar med nĂ„gon annan
  • Extrahera: ladda upp data frĂ„n en extern kĂ€lla
  • Transformera: omvandla, fylla i nyckelkopplingsfĂ€lt
  • Ladda: hĂ€lla fĂ€rdiga data i mĂ„l-tabeller
  • ta bort "egna" tabeller

Och nu — en sked av bitterhet. I princip skriver hela tiden data i PostgreSQL tvĂ„ gĂ„nger — först till WAL, och sedan till tabellernas/katalogernas kroppar. Allt detta görs för att stödja ACID och korrekt datavisning mellan COMMITtransaktioner och ROLLBACKinterna och

externa transaktioner. Men vi behöver inte detta! Hela processenantingen slutfördes framgÄngsrikt, eller sÄ gjorde den inte.

Det spelar ingen roll hur mĂ„nga mellanliggande transaktioner som behövs — vi Ă€r inte intresserade av att "föra processen vidare frĂ„n mitten", sĂ€rskilt inte nĂ€r det Ă€r oklart var den befann sig. För detta införde PostgreSQL:s utvecklare redan i version 9.1 nĂ„got som kallas:

icke-journaliserade (UNLOGGED) tabeller. Med denna angivelse skapas tabellen som en icke-journaliserad. Data som skrivs till icke-journaliserade tabeller gÄr inte igenom förskottsjournalen (se Kapitel 29), vilket gör att sÄdana tabellerfungerar mycket snabbare Àn vanliga. Men de Àr inte skyddade mot fel; vid ett fel eller ovÀntad strömavbrott skÀrs den icke-journaliserade tabellenautomatiskt av. Dessutom replikeras innehÄllet i en icke-journaliserad tabell inte. pÄ skrivna servrar. Alla index som skapas för en icke-journalförd tabell blir automatiskt icke-journalförda.

Kort sagt, blir det betydligt snabbare, men om databasservern "gĂ„r ner" — blir det obehagligt. Men hur ofta hĂ€nder det, och kan din ETL-process hantera det korrekt "frĂ„n mitten" efter "Ă„terstĂ€llningen" av databasen?..

Om inte, och om ovanstĂ„ende scenario liknar ditt — anvĂ€nd UNLOGGED, men aktivera aldrig denna attribut pĂ„ verkliga tabeller, vars data Ă€r viktiga för dig.

1.3. ON COMMIT { DELETE ROWS | DROP }

Denna konstruktion gör det möjligt att definiera automatiskt beteende vid transaktionsavslut nÀr tabellen skapas.

Om PÅ ÅTAGANDE DROP Jag har redan nĂ€mnt det ovan, det genererar DROP TABLE, men hĂ€r med PÅ ÅTAGANDE TA BORT RADER Ă€r situationen intressantare — hĂ€r genereras TRUNCATE TABLE.

Eftersom hela infrastrukturen för lagring av metadata för temporĂ€ra tabeller Ă€r exakt densamma som för vanliga, sĂ„ den stĂ€ndiga skapelsen och borttagningen av temporĂ€ra tabeller leder till kraftig "utvidgning" av systemtabeller pg_class, pg_attribute, pg_attrdef, pg_depend,


Nu kan du förestĂ€lla dig att du har en arbetare med direktanslutning till databasen som varje sekund öppnar en ny transaktion, skapar, fyller i, bearbetar och tar bort en temporĂ€r tabell
 SkrĂ€pet i systemtabellerna kommer att samlas i överflöd, och detta ger extra fördröjningar vid varje körning.

Sammanfattningsvis, gör inte sĂ„! I det hĂ€r fallet Ă€r det mycket effektivare CREATE TEMPORARY TABLE x ... ON COMMIT DELETE ROWS att flytta utanför transaktionscykeln — dĂ„ kommer tabellerna i början av varje ny transaktion redan att existera (-spara anropet TĂ€nk pĂ„ förkortningar.), men kommer att vara tom, tack vare TRUNCATE (-detta anrop har vi ocksĂ„ sparat) vid avslutningen av den föregĂ„ende transaktionen.

1.4. LIKE
 INCLUDING 


Jag nĂ€mnde i början att ett typiskt anvĂ€ndningsfall för temporĂ€ra tabeller Ă€r olika typer av importer — och utvecklaren kopierar trött det mĂ„ltabellens fĂ€ltlista till sitt tillkĂ€nnagivande för den temporĂ€ra 


Men lathet Àr framstegens motor! SÄ, att skapa en ny tabell "efter mall" kan göras mycket enklare:

CREATE TEMPORARY TABLE import_table(
  LIKE target_table
);

Eftersom det kommer att genereras en hel del data till denna tabell sĂ„ kommer sökningarna i den att inte vara snabba. Men det finns en traditionell lösning mot detta — index! Och ja, en temporĂ€r tabell kan ocksĂ„ ha index.

Eftersom de nödvÀndiga indexerna ofta överensstÀmmer med indexerna för mÄl-tabellen kan man helt enkelt skriva LIKE mÄltavla INKLUDERAR INDEX.

Om du ocksĂ„ behöver DEFAULT-vĂ€rden (till exempel för att fylla i vĂ€rdena för primĂ€rnyckeln) kan man anvĂ€nda LIKE mĂ„ltavla INKLUDERAR STANDARDINSTÄLLNINGAR. Eller bara — LIKE mĂ„ltavla INNEHÅLLER ALLT — kopierar standardvĂ€rdena, index, begrĂ€nsningar,


Men hĂ€r mĂ„ste man förstĂ„ att om du skapade import-tabellen direkt med index, sĂ„ kommer datan att laddas lĂ€ngre, Ă€n om man först laddar allt, och sedan lĂ€gger pĂ„ index — se som exempel hur detta görs av pg_dump.

Kort sagt, RTFM!

2. Hur skriver man?

Jag sĂ€ger helt enkelt — anvĂ€nd COPY-ström istĂ€llet för "batch" INSÄTT, accelerationen Ă€r flera gĂ„nger. Man kan till och med direkt frĂ„n den förbereda filen.

3. Hur bearbetar man?

SÄ, lÄt oss sÀga att vÄr indata ser ut ungefÀr sÄ hÀr:

  • du har en tabell med kunddata i 1M poster
  • varje dag skickar kunden dig en ny full "bild"
  • utifrĂ„n erfarenhet vet du att det frĂ„n gĂ„ng till gĂ„ng Ă€ndras inte mer Ă€n 10K poster

Ett klassiskt exempel pĂ„ en sĂ„dan situation Ă€r KLRAD-databasen — det finns mĂ„nga adresser, men i varje veckolastning av Ă€ndringar (omdöpnings av bosĂ€ttningar, sammanslagningar av gator, nya hus som dyker upp) Ă€r det vĂ€ldigt fĂ„ Ă€ven i hela landet.

3.1. Algoritm för fullstÀndig synkronisering

För enkelhetens skull antar vi att du inte ens behöver omstrukturera datan — bara anpassa tabellen till rĂ€tt format, vilket innebĂ€r:

  • radera allt som inte lĂ€ngre finns
  • uppgradera allt som redan funnits och behöver uppdateras
  • införa allt som Ă€nnu inte funnits

Varför exakt i den ordningen bör man utföra operationer? För att det Àr sÄ storleken pÄ tabellen vÀxer minimalt (kom ihÄg MVCC!).

DELETE FROM dst

Nej, man kan sjÀlvklart klara sig med bara tvÄ operationer:

  • radera (TA BORT) hela
  • införa allt frĂ„n den nya bilden

Men samtidigt, tack vare MVCC, kommer storleken pĂ„ tabellen att öka exakt dubbelt sĂ„ mycket! Att fĂ„ +1M poster i tabellen pĂ„ grund av att uppdatera 10K — Ă€r en sĂ„dĂ€r överflödighet


TRUNCATE dst

En mer erfaren utvecklare vet att man kan rensa hela tabellen ganska billigt:

  • rensa (TRUNCATE) hela tabellen
  • införa allt frĂ„n den nya bilden

Metoden Ă€r effektiv, ibland helt tillĂ€mplig, men det finns ett problem
 Att föra in 1M poster kommer att ta lĂ„ng tid, sĂ„ vi kan inte tillĂ„ta att lĂ€mna tabellen tom under hela denna tid (som skulle hĂ€nda utan att kapsla in i en enda transaktion).

Det betyder att:

  • vi inleder en lĂ„ngvarig transaktion
  • TRUNCATE det lĂ€gger pĂ„ AccessExclusive-lĂ„sningen
  • vi gör lĂ„ngsamma insĂ€ttningar, och alla andra kan under tiden inte ens VÄLJ

Det kÀnns inte bra


ALTER TABLE
 RENAME
 / DROP TABLE 


Ett alternativ Àr att ladda upp allt i en ny separat tabell och sedan helt enkelt döpa om den till den gamla. NÄgra jobbiga smÄsaker:

  • ocksĂ„ det AccessExclusive, om Ă€n betydligt mindre i tid
  • alla förfrĂ„gningsplaner/statistik för denna tabell Ă„terstĂ€lls, man mĂ„ste köra ANALYZE
  • alla externa nycklar (FK) till tabellen bryts

Det fanns en WIP-patch frÄn Simon Riggs som föreslog att göra ALTER-operationen för att ersÀtta tabellens kropp pÄ filnivÄ, utan att röra statistiken och FK, men han fick inte ihop tillrÀckligt med röster.

DELETE, UPDATE, INSERT

SĂ„, vi stannar vid den icke-blockerande varianten av de tre operationerna. NĂ€stan tre
 Hur gör vi det mest effektivt?

-- vi gör allt inom ramen för en transaktion, sÄ att ingen ser "mellan" tillstÄnd
BEGIN;

-- skapar en temporÀr tabell med importerade data
CREATE TEMPORARY TABLE tmp(
  LIKE dst INCLUDING INDEXES -- som en mall, Àven med index
) ON COMMIT DROP; -- vi behöver den inte utanför transaktionen

-- snabbt slÄr vi in den nya bilden via COPY
COPY tmp FROM STDIN;
-- ...
-- .

-- tar bort de som saknas
DELETE FROM
  dst D
USING
  dst X
LEFT JOIN
  tmp Y
    USING(pk1, pk2) -- fÀlt för primÀrnyckel
WHERE
  (D.pk1, D.pk2) = (X.pk1, X.pk2) AND
  Y IS NOT DISTINCT FROM NULL; -- "anti-join"

-- uppdaterar de som Àr kvar
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); -- ingen anledning att uppdatera matchande

-- sÀtter in de som saknas
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. Efterbehandling av importen

I samma KLDRe mĂ„ste alla Ă€ndrade poster dessutom köras igenom efterbehandling — normaliseras, nyckelord identifieras, och struktureranpassas. Men hur vet man — vad som faktiskt Ă€ndrades, utan att komplicera synkroniseringskoden, helst utan att röra den överhuvudtaget?

Om skrivÄtkomst under synkroniseringen endast finns för din process kan du anvÀnda en trigger som sammanstÀller alla Àndringar för oss:

-- mÄltavlor
CREATE TABLE kladr(...);
CREATE TABLE kladr_house(...);

-- tabeller med Àndringshistorik
CREATE TABLE kladr$log(
  ro kladr, -- hÀr ligger hela versioner av gamla/nya poster
  rn kladr
);

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

-- generell Funktion för loggning av Àndringar
CREATE OR REPLACE FUNCTION diff$log() RETURNS trigger AS $$
DECLARE
  dst varchar = TG_TABLE_NAME || '$log';
  stmt text = '';
BEGIN
  -- kontrollerar om loggning behövs vid uppdatering av post
  IF TG_OP = 'UPDATE' THEN
    IF NEW IS NOT DISTINCT FROM OLD THEN
      RETURN NEW;
    END IF;
  END IF;
  -- skapar en loggpost
  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;

Nu kan vi applicera triggers innan synkroniseringen (eller aktivera via 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();

Sedan kan vi enkelt hÀmta alla nödvÀndiga Àndringar frÄn loggtabellerna och köra dem genom ytterligare processorer.

3.3. Import av relaterade uppsÀttningar

Ovan har vi diskuterat fall dÀr datakÀllor och mottagare har samma struktur. Men vad ska vi göra om export frÄn ett externt system har ett format som skiljer sig frÄn vÄr databasstruktur?

Vi tar som exempel lagring av kunder och deras fakturor, en klassisk "mÄnga-till-en"-variant:

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

Och nu kommer exporten frÄn en extern kÀlla till oss som "allt-i-ett":

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

Det Àr uppenbart att kunddata kan dupliceras i detta fall, och huvudposten Àr "faktura":

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

För modellen kommer vi helt enkelt att infoga vĂ„ra testdata, men kom ihĂ„g — COPY effektivare!

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

Först utvalda de "skivor" som vÄra "fakta" hÀnvisar till. I vÄrt fall hÀnvisar fakturorna till kunder:

SKAPA EN TILLFÄLLIG TABELL client_import SOM
VÄLJ DISTINCT PÅ(client_inn)
-- man kan helt enkelt VÄLJA DISTINCT om data redan Ă€r kĂ€nda som korrekta
  client_inn inn
, client_name "name"
FRÅN
  invoice_import;

För att korrekt koppla fakturor till kundernas ID mÄste vi först fÄ reda pÄ eller generera dessa identifierare. Vi lÀgger till fÀlt för dem:

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

Vi anvĂ€nder den beskrivna metoden för att synkronisera tabeller med en liten justering — vi kommer inte att uppdatera eller ta bort nĂ„got i mĂ„ltabellen, eftersom importen av kunder Ă€r "endast tillĂ€gg":

-- vi sÀtter ID:n för redan befintliga poster i importtabellen
UPDATE
  client_import T
SET
  client_id = D.client_id
FRÅN
  client D
VARFÖR
  T.inn = D.inn; -- unik nyckel

-- vi lÀgger in saknade poster och sÀtter deras ID:n
MED ins SOM (
  INSERT INTO client(
    inn
  , name
  )
  SELECT
    inn
  , name
  FRÅN
    client_import
  VARFÖR
    client_id ÄR NULL -- om ID:n inte har satts
  RETURNING *
)
UPDATE
  client_import T
SET
  client_id = D.client_id
FRÅN
  ins D
VARFÖR
  T.inn = D.inn; -- unik nyckel

-- vi sÀtter kundernas ID:n pÄ fakturarekorden
UPDATE
  invoice_import T
SET
  client_id = D.client_id
FRÅN
  client_import D
VARFÖR
  T.client_inn = D.inn; -- tillÀmpad nyckel

Det Ă€r faktiskt allt — i invoice_import Ă€r nu fĂ€ltet för kopplingen ifyllt client_id, som vi ska anvĂ€nda för att införa fakturan.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster