Vid komplex bearbetning av stora datamÀngder (olika : 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 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.

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 â , 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. :
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 .
Kort sagt, !
2. Hur skriver man?
Jag sĂ€ger helt enkelt â anvĂ€nd -ström istĂ€llet för "batch" INSĂTT, . 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 â 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 ().
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, , 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
TRUNCATEdet 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,
- 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
