DBA: urganizà cumpetente sincronizazioni è impurtazioni
Per u trattamentu cumplessu di grande setti di dati (differenti I prucessi ETL: impurtazioni, cunversione è sincronizazione cù una fonte esterna) spessu ci hè bisognu temporaneamente "ricordate" è subitu subitu prucessu qualcosa di voluminoso.
Un compitu tipicu di stu tipu di solitu sona qualcosa cusì: "Propriu quì dipartimentu di cuntabilità scaricatu da a banca di u cliente l'ultimi pagamenti ricivuti, avete bisognu di caricalli rapidamente à u situ web è ligami cù i vostri cunti "
Ma quandu u voluminu di questu "qualcosa" cumencia à misurà in centinaie di megabytes, è u serviziu deve cuntinuà à travaglià cù a basa di dati 24x7, parechji effetti latu chì arruvinanu a vostra vita.
Per trattà cun elli in PostgreSQL (è micca solu in questu), pudete aduprà qualchi ottimisazioni chì vi permettenu di processà tuttu più veloce è cun menu cunsumu di risorse.
1. Induve spedinu ?
Prima, decidemu induve pudemu cullà i dati chì vulemu "prucessa".
1.1. Tavule Temporary (TABLE TEMPORARY)
In principiu, per PostgreSQL e tavule tempuranee sò listessi cum'è qualsiasi altru. Dunque, superstizioni cum'è "Tuttu ci hè guardatu solu in memoria, è pò finisce". Ma ci sò ancu parechje differenzi significati.
U vostru propiu "namespace" per ogni cunnessione à a basa di dati
Sì duie cunnessione pruvate à cunnette à u stessu tempu CREATE TABLE x, allora qualcunu certamenti uttene errore di non-unicità oggetti di basa di dati.
Ma si tutti dui pruvate à eseguisce CREATE TEMPORARY TABLE x, allura tramindui a feranu nurmale, è tutti utteneranu a vostra copia tavule. È ùn ci sarà nunda in cumunu trà elli.
"Autodistruzzione" quandu si disconnette
Quandu a cunnessione hè chjusa, tutte e tavule tempuranee sò automaticamente sguassate, cusì manualmente DROP TABLE x ùn ci hè nunda eccettu ...
Sè vo site à travaglià pgbouncer in modu di transazzione, allura a basa di dati cuntinueghja à crede chì sta cunnessione hè sempre attiva, è in questu sta table temporale esiste sempre.
Per quessa, pruvà à creà di novu, da una cunnessione diversa à pgbouncer, hà da esse un errore. Ma questu pò esse evitata cù l'usu CREATE TEMPORARY TABLE IF NOT EXISTS x.
True, hè megliu micca di fà questu in ogni modu, perchè allora pudete "subbitu" truvà quì i dati chì restanu da u "pruprietariu precedente". Invece, hè assai megliu per leghje u manuale è vede chì quandu crea una tavola hè pussibule aghjunghje ON COMMIT DROP - vale à dì, quandu a transazzione cumpleta, a tavula serà automaticamente sguassata.
Non-replicazione
Perchè appartenenu solu à una cunnessione specifica, i tavule tempurane ùn sò micca replicati. Ma questu elimina a necessità di doppia registrazione di dati in heap + WAL, cusì INSERT/UPDATE/DELETE in questu hè significativamente più veloce.
Ma postu chì una tavula temporale hè sempre una tavola "quasi ordinaria", ùn pò micca esse creatu ancu nantu à una replica. Almenu per avà, ancu s'è u patch currispundente hè in circulazione per un bellu pezzu.
1.2. TABELLA UNLOGGED
Ma chì duvete fà, per esempiu, sè avete qualchì tipu di prucessu ETL ingombrante chì ùn pò micca esse implementatu in una transazzione, ma avete sempre pgbouncer in modu di transazzione? ...
O u flussu di dati hè cusì grande chì Ùn ci hè micca abbastanza larghezza di banda in una cunnessione da una basa di dati (lettu, un prucessu per CPU) ?...
O certe operazioni sò in corso in modu asincronu in diverse cunnessione?...
Ci hè solu una opzione quì - temporaneamente creà una tavola non-tempuranea. Pun, sì. Hè:
criatu "u mo propiu" tavulinu cù nomi massimu casuale per ùn intersecà cù nimu
Extract: li pieni di dati da una fonte esterna
Trasfurmà: cunvertitu, cumpletu in i campi di ligame chjave
Load: versò dati pronti in tavule di destinazione
sguassati "i mo" tavule
È avà - una mosca in l'unguentu. In fattu, tutte e scritture in PostgreSQL succede duie volte - prima in WAL, poi in i corpi di tabella / indici. Tuttu chistu hè fattu per sustene l'ACID è a visibilità di dati curretta trà COMMIT'noce è ROLLBACK'transazzione nulla.
Ma ùn avemu micca bisognu di questu! Avemu tuttu u prucessu O era cumplettamente successu o ùn era micca.. Ùn importa micca quantu transazzione intermedie seranu - ùn avemu micca interessatu à "cuntinuà u prucessu da u mità", soprattuttu quandu ùn hè micca chjaru induve era.
Per fà questu, i sviluppatori di PostgreSQL, in a versione 9.1, anu introduttu una cosa cum'è tabelle UNLOGGED:
Cù sta indicazione, a tavula hè creata cum'è unlogged. I dati scritti in e tabelle unlogged ùn passanu micca in u log di scrittura anticipata (vede u Capitulu 29), facendu chì tali tabelle travaglià assai più veloce di u solitu. Tuttavia, ùn sò micca immune à fallimentu; in casu di fallimentu di u servitore o arrestu di emergenza, una tavola unlogged truncatu automaticamente. Inoltre, u cuntenutu di a tavola unlogged micca replicatu à i servitori slave. Ogni indici creati nantu à una tabella unlogged diventanu automaticamente unlogged.
In brevi, serà assai più veloce, ma se u servitore di basa di dati "caduta", serà dispiacevule. Ma quantu spessu succede questu, è u vostru prucessu ETL sapi cumu corregge questu currettamente "da u mità" dopu à "rivitalizza" a basa di dati?...
Se no, è u casu sopra hè simile à u vostru, aduprate UNLOGGEDma mai ùn attivate micca stu attributu nantu à e tavule veri, i dati da quale hè caru per voi.
1.3. ON COMMIT { ELIMINA RIGHE | DROP}
Questa custruzzione permette di specificà u cumpurtamentu automaticu quandu una transazzione hè cumpleta quandu crea una tavola.
nantu ON COMMIT DROP Aghju digià scrittu sopra, genera DROP TABLE, ma cun ON COMMIT DELETE ROWS a situazione hè più interessante - hè generatu quì TRUNCATE TABLE.
Siccomu tutta l'infrastruttura per almacenà a meta-descrizzione di una tavola temporale hè esattamente uguale à quella di una tavola regulare, allora A creazione constante è l'eliminazione di e tavule tempuranee porta à una "inflazione" severa di e tavule di u sistema pg_class, pg_attribute, pg_attrdef, pg_depend,...
Avà imaginate chì avete un travagliadore nantu à una cunnessione diretta à a basa di dati, chì apre una nova transazzione ogni secondu, crea, riempie, processa è sguassate una tavola temporale... Ci sarà un eccessu di basura accumulatu in i tavule di u sistema, è questu pruvucarà freni extra per ogni operazione.
In generale, ùn fate micca questu! In questu casu, hè assai più efficace CREATE TEMPORARY TABLE x ... ON COMMIT DELETE ROWS piglià fora di u ciculu di transazzione - dopu à u principiu di ogni nova transazzione i tavule sò digià esisterà (Salvà una chjama CREATE), ma sarà viotu, grazie à TRUNCATE (avemu ancu salvatu a so chjama) quandu compie a transazzione precedente.
1.4. LIKE... INCLUSI...
Aghju menzionatu à u principiu chì unu di i casi d'usu tipici per i tavulini tempuranee hè varii tipi di impurtazioni - è u sviluppatore stancu copià-incollà a lista di campi di a tavola di destinazione in a dichjarazione di u so tempurale ...
Ma a pigrizia hè u mutore di u prugressu ! Hè perchè creà una nova tavola "basata nantu à u sample" pò esse assai più simplice:
CREATE TEMPORARY TABLE import_table(
LIKE target_table
);
Dapoi vi ponu tandu generà assai di dati in sta tavula, a ricerca à traversu ùn sarà mai prestu. Ma ci hè una suluzione tradiziunale à questu - indici! È, sì, una tavula tempuranea pò ancu avè indici.
Siccomu, spessu, l'indici necessarii coincidenu cù l'indici di a tavola di destinazione, pudete simplificà scrive LIKE target_table INCLUDING INDEXES.
Sè ancu avete bisognu DEFAULT-valori (per esempiu, per riempie i valori di chjave primaria), pudete aduprà LIKE target_table INCLUDING DEFAULTS. O simpricimenti - LIKE target_table INCLUDING ALL - copia predefiniti, indici, restrizioni, ...
Ma quì avete bisognu di capisce chì si avete creatu tavula impurtà subitu cù l'indici, allura i dati pigghianu più tempu per caricachè s'è tù prima riempie tuttu, è solu dopu roll up l'indici - fighjate cumu fà questu cum'è un esempiu pg_dump.
Lasciami dì solu - aduprà COPY-flow invece di "pack" INSERT, accelerazione a volte. Pudete ancu direttamente da un schedariu pre-generatu.
3. Cumu prucessu?
Allora, lasciamu chì a nostra intro pare cusì cusì:
avete un tavulu cù dati di u cliente guardatu in a vostra basa di dati 1 M records
ogni ghjornu un cliente vi manda un novu piena "imagine"
da a sperienza sapete chì da u tempu à u tempu micca più di 10K records sò cambiati
Un esempiu classicu di una tale situazione hè Base KLADR - ci sò assai indirizzi in tuttu, ma in ogni carica settimanale ci sò assai pochi cambiamenti (rinominazione di l'insediamenti, cumminzioni di strade, apparenza di case novi) ancu à una scala naziunale.
3.1. Algoritmu di sincronizazione cumpleta
Per a simplicità, dicemu chì ùn avete mancu bisognu di ristrutturazione di dati - basta à purtà a tavola in a forma desiderata, vale à dì:
rimuovi tuttu ciò chì ùn esiste più
aghjurnamentu tuttu ciò chì esiste digià è deve esse aghjurnatu
inserisce tuttu ciò chì ùn hè ancu accadutu
Perchè l'operazione deve esse fatta in questu ordine? Perchè hè cusì chì a dimensione di a tavola cresce minimamente (ricordate MVCC!).
ELIMINA DA dst
Innò, di sicuru, pudete piglià solu cù duie operazioni:
rimuovi (DELETE) tuttu in generale
inserisce tuttu da a nova imagine
Mais en même temps, grâce à MVCC, A dimensione di a tavula cresce esattamente duie volte! Ottene + 1M d'imaghjini di registri in a tavula per via di una aghjurnazione di 10K hè cusì cusì ridondante ...
TRUNCATE dst
Un sviluppatore più espertu sapi chì tutta a tableta pò esse pulita à pocu pressu:
per liberà (TRUNCATE) tutta a tavola
inserisce tuttu da a nova imagine
U metudu hè efficace, a volte abbastanza applicabile, ma ci hè un prublema... Avemu da aghjunghje 1M records per un bellu pezzu, cusì ùn pudemu micca permette di lascià a tavula viota per tuttu questu tempu (cum'è accaderà senza imballà in una sola transazzione).
Chì significa:
avemu principiatu transazzione di longa durata
TRUNCATE impone Accessu Esclusivu- bluccatu
facemu l'inserzione per un bellu pezzu, è tutti l'altri in questu tempu ùn pò mancu SELECT
Qualcosa ùn va micca bè...
ALTER TABLE... RENAME... / DROP TABLE...
Una alternativa hè di chjappà tuttu in una nova tavola separata, è dopu solu rinominallu in u locu di u vechju. Un paru di picculi cose brutte:
ancu ancu Accessu Esclusivu, anche se significativamente menu tempu
Ci era un patch WIP da Simon Riggs chì suggeria di fà ALTER-un funziunamentu di rimpiazzà u corpu di a tavula à u livellu di u schedariu, senza toccu statistiche è FK, ma ùn hà micca cullatu quorum.
ELIMINA, AGGIORNARE, INSERIRE
Dunque, avemu stallatu nantu à l'opzione senza bloccu di trè operazioni. Quasi trè... Cumu fà questu più efficace?
-- все делаем в рамках транзакции, чтобы никто не видел "промежуточных" состояний
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. Import post-processamentu
In u stessu KLADR, tutti i registri cambiati devenu esse ancu eseguiti per post-processamentu - nurmalizzati, e parolle chjave evidenziate, è ridotte à e strutture richieste. Ma cumu sapete - ciò chì hà cambiatu esattamentesenza cumplicà u codice di sincronizazione, idealmente senza toccu à tuttu?
Se solu u vostru prucessu hà accessu à scrittura à u mumentu di a sincronizazione, pudete aduprà un attivatore chì raccoglie tutti i cambiamenti per noi:
-- целевые таблицы
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;
Avà pudemu applicà triggers prima di inizià a sincronizazione (o attivà 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();
E poi estrattemu tranquillamente tutti i cambiamenti chì avemu bisognu da e tavule di log è eseguimu attraversu manipulatori supplementari.
3.3. Importazione di Sets Linked
Sopra avemu cunsideratu casi quandu e strutture di dati di a fonte è di destinazione sò listessi. Ma chì se u upload da un sistema esternu hà un formatu sfarente da a struttura di almacenamiento in a nostra basa di dati?
Pigliemu per esempiu l'almacenamiento di i clienti è i so cunti, l'opzione classica "parechje à unu":
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)
);
Ma u scaricamentu da una fonte esterna vene à noi in a forma di "all in one":
Prima, mettemu in risaltu quelli "tagli" à quale si riferiscenu i nostri "fatti". In u nostru casu, e fatture si riferiscenu à i clienti:
CREATE TEMPORARY TABLE client_import AS
SELECT DISTINCT ON(client_inn)
-- можно просто SELECT DISTINCT, если данные заведомо непротиворечивы
client_inn inn
, client_name "name"
FROM
invoice_import;
Per associà currettamente i cunti cù l'ID di i clienti, avemu prima bisognu di scopre o generà questi identificatori. Aghjunghjite campi sottu à elli:
ALTER TABLE invoice_import ADD COLUMN client_id integer;
ALTER TABLE client_import ADD COLUMN client_id integer;
Utilizemu u metudu di sincronizazione di a tavola descritta sopra cù una piccula emenda - ùn aghjurneremu micca o eliminemu nunda in a tavola di destinazione, perchè impurtate i clienti "append-only":
-- проставляем в таблице импорта 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; -- прикладной ключ
In fatti, tuttu hè in invoice_import Avà avemu u campu di cuntattu cumpletu client_id, cù quale inseriremu a fattura.