DBA: kompetentno organizirati sinhronizacije i uvoze
Za složenu obradu velikih skupova podataka (razl ETL procesi: uvoz, konverzije i sinhronizacija sa eksternim izvorom) često postoji potreba privremeno "zapamti" i odmah brzo obraditi nešto obimno.
Tipičan zadatak ove vrste obično zvuči otprilike ovako: "Upravo ovdje računovodstvo iskrcano iz banke klijenta posljednje primljene uplate, morate ih brzo učitati na web stranicu i povezati sa svojim računima.”
Ali kada obim ovog „nečega“ počne da se meri stotinama megabajta, a servis mora da nastavi da radi sa bazom podataka 24x7, javljaju se mnoge nuspojave koje će vam uništiti život.
Da biste se pozabavili njima u PostgreSQL-u (i ne samo u njemu), možete koristiti neke optimizacije koje će vam omogućiti da sve obradite brže i uz manju potrošnju resursa.
1. Gdje poslati?
Prvo, odlučimo gdje možemo učitati podatke koje želimo "obraditi".
1.1. Privremene tabele (TEMPORARY TABLE)
U principu, za PostgreSQL privremene tabele su iste kao i sve druge. Stoga sujeverja poput "Sve tamo je pohranjeno samo u memoriji, i može završiti". Ali postoji i nekoliko značajnih razlika.
Vaš vlastiti “imenski prostor” za svaku vezu s bazom podataka
Ako se dvije veze pokušaju povezati u isto vrijeme CREATE TABLE x, onda će neko sigurno dobiti greška nejedinstvenosti objekti baze podataka.
Ali ako oboje pokušaju izvršiti CREATE TEMPORARY TABLE x, onda će oboje normalno raditi, i svi će dobiti tvoj primjerak stolovi. I između njih neće biti ništa zajedničko.
"Samouništenje" prilikom prekida veze
Kada se veza zatvori, sve privremene tabele se automatski brišu, dakle ručno DROP TABLE x nema svrhe osim...
Ako radite kroz pgbouncer u transakcijskom modu, onda baza podataka nastavlja vjerovati da je ova veza još uvijek aktivna, au njoj ova privremena tablica još uvijek postoji.
Stoga, pokušaj ponovnog kreiranja, s druge veze na pgbouncer, rezultirat će greškom. Ali ovo se može zaobići upotrebom CREATE TEMPORARY TABLE IF NOT EXISTS x.
Istina, bolje je to ipak ne raditi, jer tada možete "odjednom" tamo pronaći podatke koji su ostali od "prethodnog vlasnika". Umjesto toga, puno je bolje pročitati priručnik i vidjeti da je prilikom kreiranja tabele moguće dodati ON COMMIT DROP - odnosno kada se transakcija završi, tabela će se automatski izbrisati.
Nereplikacija
Budući da pripadaju samo određenoj vezi, privremene tablice se ne repliciraju. Ali ovo eliminiše potrebu za dvostrukim snimanjem podataka u hrpi + WAL, pa je INSERT/UPDATE/DELETE u njega znatno brži.
Ali pošto je privremena tabela i dalje „skoro obična“ tabela, ne može se kreirati ni na replici. Barem za sada, iako odgovarajući flaster kruži već duže vrijeme.
1.2. UNLOGGED TABLE
Ali šta biste trebali učiniti, na primjer, ako imate neku vrstu glomaznog ETL procesa koji se ne može implementirati unutar jedne transakcije, ali još uvijek imate pgbouncer u transakcijskom modu? ..
Ili je protok podataka toliko velik da Nema dovoljno propusnog opsega na jednoj vezi iz baze podataka (čitaj, jedan proces po CPU-u)?..
Ili su neke operacije u toku asinhrono u različitim vezama?..
Ovde postoji samo jedna opcija - privremeno kreirati neprivremenu tabelu. Igra igre, da. To je:
kreirao „svoje“ tabele sa maksimalno nasumičnim imenima kako se ne bi ukrštale ni sa kim
ekstrakt: ispunio ih podacima iz vanjskog izvora
Transformirati: pretvoreno, popunjena ključna polja za povezivanje
opterećenje: sipa gotove podatke u ciljne tabele
obrisali "moje" tabele
A sada - muva u masti. Zapravo, sva upisivanja u PostgreSQL se dešavaju dvaput - prvi u WAL-u, zatim u tijela tablice/indeksa. Sve ovo je urađeno kako bi se podržala ACID i ispravna vidljivost podataka između COMMIT'zabrinut i ROLLBACK'nulte transakcije.
Ali nama ovo ne treba! Imamo ceo proces Ili je bilo potpuno uspješno ili nije.. Nije bitno koliko će međutransakcija biti - nismo zainteresovani da „nastavljamo proces od sredine“, pogotovo kada nije jasno gde je to bilo.
Da bi to učinili, programeri PostgreSQL-a, još u verziji 9.1, predstavili su nešto kao UNLOGGED tables:
Sa ovom indikacijom, tabela se kreira kao neregistrovana. Podaci upisani u neregistrovane tabele ne prolaze kroz dnevnik upisa unapred (pogledajte Poglavlje 29), što dovodi do toga da takve tabele rade mnogo brže nego inače. Međutim, oni nisu imuni na neuspjeh; u slučaju kvara servera ili gašenja u nuždi, neregistrovana tabela automatski skraćeno. Dodatno, sadržaj neregistrovane tabele nije replicirano na slave servere. Svi indeksi kreirani na neregistrovanoj tabeli automatski postaju neregistrovani.
Ukratko, to će biti mnogo brže, ali ako server baze podataka “padne”, to će biti neugodno. Ali koliko često se to dešava i da li vaš ETL proces zna kako to ispravno ispraviti “iz sredine” nakon “revitalizacije” baze podataka?..
Ako nije, a gornji slučaj je sličan vašem, koristite UNLOGGEDali nikad nemojte omogućiti ovaj atribut na stvarnim tablicama, podaci iz kojih su Vam dragi.
1.3. ON COMMIT { IZBRIŠI REDOVE | DROP}
Ova konstrukcija vam omogućava da navedete automatsko ponašanje kada se transakcija završi prilikom kreiranja tabele.
na ON COMMIT DROP Već sam gore napisao, generiše DROP TABLE, ali sa ON COMMIT DELETE ROWS situacija je interesantnija - ona se generiše ovde TRUNCATE TABLE.
Budući da je cjelokupna infrastruktura za pohranjivanje meta-opisa privremene tablice potpuno ista kao i obične tablice, onda Konstantno kreiranje i brisanje privremenih tabela dovodi do ozbiljnog "oticanja" sistemskih tabela pg_class, pg_attribute, pg_attrdef, pg_depend,…
Sada zamislite da imate radnika na direktnoj vezi sa bazom podataka, koji svake sekunde otvara novu transakciju, kreira, popunjava, obrađuje i briše privremenu tabelu... U sistemskim tabelama će se nakupiti višak smeća i ovo će uzrokovati dodatne kočnice za svaku operaciju.
Generalno, nemojte ovo raditi! U ovom slučaju je mnogo efikasniji CREATE TEMPORARY TABLE x ... ON COMMIT DELETE ROWS izbacite ga iz ciklusa transakcije - tada su do početka svake nove transakcije tabele već postojat će (sačuvaj poziv CREATE), ali će biti prazan, Hvala za TRUNCATE (također smo sačuvali njegov poziv) prilikom završetka prethodne transakcije.
1.4. LIKE...UKLJUČUJUĆI...
Na početku sam spomenuo da je jedan od tipičnih slučajeva upotrebe privremenih tabela razne vrste uvoza - a programer umorno kopira-paste listu polja ciljne tabele u deklaraciju svog privremenog...
Ali lenjost je motor napretka! Zbog toga kreirajte novu tabelu "na osnovu uzorka" može biti mnogo jednostavnije:
CREATE TEMPORARY TABLE import_table(
LIKE target_table
);
Budući da tada možete generirati mnogo podataka u ovu tabelu, pretraživanje kroz nju nikada neće biti brzo. Ali postoji tradicionalno rješenje za ovo - indeksi! i da, privremena tabela takođe može imati indekse.
Pošto se, često, traženi indeksi poklapaju sa indeksima ciljne tabele, možete jednostavno pisati LIKE target_table INCLUDING INDEXES.
Ako i vama treba DEFAULT-vrijednosti (na primjer, za popunjavanje vrijednosti primarnog ključa), možete koristiti LIKE target_table INCLUDING DEFAULTS. Ili jednostavno - LIKE target_table INCLUDING ALL — kopira zadane postavke, indekse, ograničenja,...
Ali ovdje morate razumjeti da ako ste kreirali odmah uvezite tabelu sa indeksima, tada će podacima trebati duže da se učitajunego ako prvo sve popuniš, pa tek onda smotaš indekse - pogledaj na primjeru kako to radi pg_dump.
Samo da kažem - iskoristite COPY-tok umjesto "pakovati" INSERT, ubrzanje na momente. Možete čak i direktno iz unaprijed generiranog fajla.
3. Kako obraditi?
Dakle, neka naš uvod izgleda otprilike ovako:
imate tabelu sa podacima o klijentima pohranjenim u vašoj bazi podataka 1M zapisa
svaki dan vam klijent pošalje novu puna "slika"
iz iskustva znate da s vremena na vrijeme ne mijenja se više od 10K zapisa
Klasičan primjer takve situacije je baza KLADR — ukupno je mnogo adresa, ali u svakom nedeljnom upload-u ima vrlo malo promena (preimenovanje naselja, kombinovanje ulica, izgled novih kuća) čak i na nacionalnom nivou.
3.1. Potpuni algoritam sinhronizacije
Radi jednostavnosti, recimo da ne morate čak ni restrukturirati podatke - samo dovedite tabelu u željeni oblik, to jest:
ukloniti sve sto vise ne postoji
nadogradnja sve što je već postojalo i što je potrebno ažurirati
umetnite sve sto se jos nije desilo
Zašto bi se operacije radile ovim redoslijedom? Zato što će na ovaj način veličina stola minimalno rasti (zapamtite MVCC!).
IZBRIŠI IZ dst
Ne, naravno da možete proći sa samo dvije operacije:
ukloniti (DELETE) sve općenito
umetnite sve sa nove slike
Ali u isto vrijeme, zahvaljujući MVCC-u, Veličina stola će se povećati tačno dva puta! Dobivanje +1M slika zapisa u tabeli zbog ažuriranja od 10K je tako-tako redundantnost...
TRUNCATE dst
Iskusniji programer zna da se cijeli tablet može očistiti prilično jeftino:
jasno (TRUNCATE) cijelu tabelu
umetnite sve sa nove slike
Metoda je efikasna, ponekad sasvim primenljivo, ali postoji problem... Mi ćemo još dugo dodavati 1M zapisa, tako da ne možemo priuštiti da ostavimo tabelu praznu sve ovo vrijeme (kao što će se dogoditi bez umotavanja u jednu transakciju).
Što znači:
počinjemo dugotrajnu transakciju
TRUNCATE nameće Pristup ekskluzivno-blokiranje
umetanje radimo dugo, a svi ostali u ovom trenutku ne mogu čak SELECT
Nesto ne ide kako treba...
PROMIJENI TABELU… PREIMENUJ… / ISPUSTI TABELU…
Alternativa je da sve popunite u posebnu novu tabelu, a zatim je jednostavno preimenujete umjesto stare. Par gadnih sitnica:
još uvek Pristup ekskluzivno, iako znatno manje vremena
Postojala je WIP zakrpa od Simona Riggsa koja je sugerirala izradu ALTER-operacija zamene tela tabele na nivou fajla, bez dodirivanja statistike i FK, ali nije prikupio kvorum.
IZBRIŠI, AŽURIRAJ, UMETNI
Dakle, odlučujemo se o neblokirajućoj opciji od tri operacije. Skoro tri... Kako to najefikasnije uraditi?
-- все делаем в рамках транзакции, чтобы никто не видел "промежуточных" состояний
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. Uvozna naknadna obrada
U istom KLADR-u, svi izmenjeni zapisi moraju se dodatno proći kroz naknadnu obradu - normalizovani, istaknute ključne reči i svedene na potrebne strukture. Ali kako znaš - šta se tačno promenilobez kompliciranja koda za sinhronizaciju, idealno bez dodirivanja?
Ako samo vaš proces ima pristup za pisanje u vrijeme sinhronizacije, tada možete koristiti okidač koji će prikupiti sve promjene za nas:
-- целевые таблицы
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;
Sada možemo primijeniti okidače prije početka sinkronizacije (ili ih omogućiti putem 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 onda mirno izvlačimo sve promjene koje su nam potrebne iz tablica dnevnika i izvodimo ih kroz dodatne rukovaoce.
3.3. Uvoz povezanih skupova
Gore smo razmatrali slučajeve kada su strukture podataka izvora i odredišta iste. Ali šta ako otpremanje sa eksternog sistema ima format koji se razlikuje od strukture skladištenja u našoj bazi podataka?
Uzmimo kao primjer skladištenje klijenata i njihovih računa, klasičnu opciju „više na jedan“:
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)
);
Ali preuzimanje iz vanjskog izvora dolazi nam u obliku "sve u jednom":
Prvo, hajde da istaknemo one „rezove“ na koje se odnose naše „činjenice“. U našem slučaju, fakture se odnose na kupce:
CREATE TEMPORARY TABLE client_import AS
SELECT DISTINCT ON(client_inn)
-- можно просто SELECT DISTINCT, если данные заведомо непротиворечивы
client_inn inn
, client_name "name"
FROM
invoice_import;
Da bismo ispravno povezali račune s ID-ovima kupaca, prvo moramo saznati ili generirati ove identifikatore. Dodajmo polja ispod njih:
ALTER TABLE invoice_import ADD COLUMN client_id integer;
ALTER TABLE client_import ADD COLUMN client_id integer;
Koristimo gore opisanu metodu sinkronizacije tablice s malom izmjenom - nećemo ažurirati ili izbrisati ništa u ciljnoj tablici, jer uvozimo klijente "samo za dodavanje":
-- проставляем в таблице импорта 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; -- прикладной ключ
Zapravo, sve je unutra invoice_import Sada imamo popunjeno polje za kontakt client_id, uz koje ćemo umetnuti račun.