Kui keerukalt töötleda suuri andmekogusid (erinevad : impordid, konverteerimised ja sĂŒnkroonimised vĂ€lise allikaga) tekib sageli vajadus ajaliselt "mĂ€luda" ja korraga kiiresti töödelda mĂ”nda suurust.
Selline tĂŒĂŒpiline ĂŒlesanne kĂ”lab tavaliselt nii: "Siin on viimased laekunud maksed, tuleb need kiiresti veebilehele laadida ja siduda kontodega"
Kuid kui selle "millegi" maht hakkab mÔÔtma sadu megabait, samas kui teenus peab töötama andmebaasiga reĆŸiimis 24Ă7, tekib palju kĂ”rvalmĂ”jusid, mis hakkavad teie elu halvendama.

Nende vastu vÔitlemiseks PostgreSQL-is (ja mitte ainult seal) saab kasutada teatavaid optsioone optimeerimiseks, mis vÔimaldavad kÔike töötleda kiiremini ja vÀiksema ressursikulu.
1. Kuhu laadida?
Esmalt laske meil mÔista, kuhu me saame laadida andmed, mida soovime "töödeldaks".
1.1. Ajutised tabelid (TEMPORARY TABLE)
PÔhimÔtteliselt on ajutised tabelid PostgreSQL-is samasugused tabelid nagu kÔik teised. SeetÔttu ei ole vale uskumine, et "seal hoitakse kÔike ainult mÀlus ja see vÔib otsa saada". Kuid on ka mitmeid olulisi erinevusi.
Igal DB ĂŒhendusel on oma "nimetamise ruum"
Kui kaks ĂŒhendust ĂŒritavad samal ajal kĂ€ivitada CREATE TABLE x, siis keegi saab kindlasti andmebaasi objektide ainulaadsuse viga. Aga kui mĂ”lemad ĂŒritavad kĂ€ivitada
, siis mĂ”lemad teevad seda normaalselt ja igaĂŒks saab CREATE AJUTINE TABEL xoma eksemplari tabelist. Ja neil ei ole ĂŒksteisega midagi ĂŒhist. "EnesehĂ€vitamine" sulgemisel
Ăhenduse sulgemisel kustutatakse kĂ”ik ajutised tabelid automaatselt, seega pole "kĂ€si" teostada
DROP TABLE x mingit mÔtet, vÀlja arvatud... Kui töötate
pgbouncer'i tehingureĆŸiimis , siis andmebaas arvab endiselt, et see ĂŒhendus on endiselt aktiivne, ja selles eksisteerib see ajutine tabel endiselt.Seega toob pĂŒĂŒe seda uuesti luua, juba teise ĂŒhenduse kaudu pgbouncerile, kaasa vea. Kuid seda saab vĂ€ltida, kasutades
Kuid parem on seda ikkagi mitte teha, sest siis vÔite "ootamatult" avastada seal "eelmistest omanikest" jÀÀne andmed. Selle asemel on palju parem lugeda juhendit ja nÀha, et tabeli loomisel on vÔimalus juurde kirjutada LOO TEMPORARY TABLE KUI EI OLE x.
TĂ”epoolest, parem on seda mitte teha, kuna siis vĂ”ib "ĂŒhtĂ€kki" avastada seal endiselt olemasolevaid andmeid "eelmiselt omanikult". Selle asemel on palju parem lugeda manuaali ja nĂ€ha, et tabeli loomisel on vĂ”imalus tĂ€iendavaid andmeid sisestada. REKISTERIMISES DROP â see, when the transaction is completed, the table will be automatically deleted.
Non-replication
Due to being assigned to a specific connection only, temporary tables are not replicated. However, this eliminates the need for double data writing in heap + WAL, hence INSERT/UPDATE/DELETE into it is significantly faster.
But since a temporary table is still "almost a regular" table, it can't be created on a replica either. At least, not yet, although the corresponding patch has been around for a long time.
1.2. Unlogged tables (UNLOGGED TABLE)
But what if, for example, you have some bulky ETL process that can't be implemented within a single transaction, and you indeed , siis andmebaas arvab endiselt, et see ĂŒhendus on endiselt aktiivne, ja selles eksisteerib see ajutine tabel endiselt.?..
Or the data stream is so large that the bandwidth of a single connection to the DB (read, one process on the CPU)?..
Or part of the operations go asynchronously in different connections?..
In this case, there is only one option â temporarily create a non-temporary table. A pun, indeed. That is:
- created "my" tables with maximally-random names to avoid any overlap
- Extract: loaded data from an external source into them
- Transform: transformed, filled key linking fields
- Laadi: poured the prepared data into target tables
- deleted "my" tables
And now â a teaspoon of tar. Essentially, all writing in PostgreSQL happens twice â , then in the bodies of tables/indices. All this is done to support ACID and correct data visibility between COMMITinner and ROLLBACKouter transactions.
But we donât need this! Our entire process must either have completely succeeded or not at all.It doesnât matter how many intermediate transactions there will be â we are not interested in "continuing the process from the middle", especially when itâs not clear where it was.
For this, PostgreSQL developers introduced a feature as early as version 9.1 called :
With this indication, the table is created as unlogged. Data written to unlogged tables does not go through the write-ahead log (see Chapter 29), resulting in such tables working much faster than regular ones.However, they are not crash-safe; in case of a crash or unexpected server shutdown, the unlogged table is automatically truncated.Moreover, the content of an unlogged table is not replicated. juhitud serverite jaoks. Iga indeksi, mis luuakse logimata tabeli jaoks, muutub automaatselt logimata.
LĂŒhidalt, on see oluliselt kiirem, kuid kui andmebaasi server "kukkub", on see ebameeldiv. Kuid kui sageli see juhtub ja kas teie ETL-protsess suudab seda Ă”igesti "keskelt" taastada pĂ€rast andmebaasi "taaselustamist"?..
Kui ei, ja eespool mainitud olukord sarnaneb teie omaga â kasutage UNLOGGED, kuid kunagi Ă€rge lĂŒlitage seda atribuuti sisse reaalsetele tabelitele, mille andmed on teile olulised.
1.3. ON COMMIT { KUSTUTA RIDA | KUSTUTA }
See konstruktsioon vÔimaldab tabeli loomisel mÀÀrata automaatse kÀitumise tehingu lÔpetamisel.
KĂŒsimus REKISTERIMISES DROP nagu ma juba eespool mainisin, genereerib see DROP TABLE, kuid siin on REKISTERIMISES KUSTUTA VREAD olukord huvitavam â siin genereeritakse TRUNCATE TABLE.
Kuna ajutise tabeli metaandmete salvestus infrastruktuur on sama, mis tavalistel, siis ajutiste tabelite pidev loomine-kustutamine toob kaasa sĂŒsteemsete tabelite tugeva "paisumise" pg_class, pg_attribute, pg_attrdef, pg_depend,âŠ
Kujutage nĂŒĂŒd ette, et teil on töötaja, kes on otseĂŒhenduses andmebaasiga, ja avab igal sekundil uue tehingu, loob, tĂ€idab, töötleb ja kustutab ajutise tabeli⊠SĂŒsteemsetesse tabelitesse koguneb liiga palju prĂŒgi, mis toob kaasa tarbetud lagged igas operatsioonis.
Ăldiselt, Ă€rge tehke nii! Sel juhul on palju tĂ”husam LOO AJUTINE TABEL x ... ON COMMIT KUSTUTA RIDA toimetada tehingu tsĂŒklist vĂ€lja â siis on iga uue tehingu alguseks tabelid juba olemas (sÀÀstame kutset CREATE), kuid olevad tĂŒhjad, tĂ€nu TRUNCATE (me sÀÀstsime ka selle kutse) eelmise tehingu lĂ”petamisel.
1.4. LIKE⊠SISALDAB âŠ
Mainisin alguses, et ĂŒks tĂŒĂŒpiline ajutiste tabelite kasutusjuht on erinevad impordid â ja arendaja kopeerib vĂ€sinult sihttabeli vĂ€ljade nimekirja oma ajutise kuulutusseâŠ
Kuid laiskus on progressi mootor! SeetĂ”ttu uue tabeli loomine âmudeleid pidiâ on palju lihtsam:
LOO AJUTINE TABEL import_table(
LIKE target_table
);Kuna sellesse tabelisse on vĂ”imalik hiljem genereerida palju andmeid, siis otsingud selles muutuvad ÀÀrmiselt aeglaseks. Kuid selle vastu on traditsiooniline lahendus â indeksid! Ja jah, ajutistel tabelitel vĂ”ivad samuti olla indeksid.
Kuna sageli jÀÀvad vajalikud indeksid kokku sihtpĂ€randi tabeli indeksitega, vĂ”ib lihtsalt kirjutada NAGU target_table KAASAM NĂIDIKUD.
Kui vajate veel ka DEFAULT-vÀÀrtusi (nĂ€iteks primaarsete vĂ”tmete vÀÀrtuste tĂ€itmiseks), saab kasutada NAGU target_table KAASIED SĂHRIT. Noh, vĂ”i lihtsalt â NAGU target_table KAAS KĂIK â kopeerib vaikeseaded, indeksid, piirangud,âŠ
Aga siin tuleb juba aru saada, et kui olete loonud impordi-tabeli kohe indeksitega, siis andmete ĂŒleslaadimine kestab kauem, kui kui kĂ”igepealt kogu sisu ĂŒles laadida ja alles seejĂ€rel indeksid rakendada â vaadake nĂ€iteks, kuidas seda teeb .
ĂhesĂ”naga, !
2. Kuidas kirjutada?
Ătlen lihtsalt â kasutage -voogu asemel âpartiidâ INSERT, . VĂ”ite isegi otse eelnevalt vormistatud failist.
3. Kuidas töödelda?
Oletame, et meie sisend nÀeb vÀlja umbes nii:
- teil on andmebaasis tabel kliendiandmetega 1M kirjet
- iga pĂ€ev saadab klient teile uue tĂ€ieliku âpildiâ
- kogenud inimesed teavad, et iga kord muutub vaid 10K kirjet
Klassikaline nĂ€ide taolisest olukorrast on â aadresse on palju, kuid iganĂ€dalasel andmevoolul on muutusi (asulate nimede muutmine, tĂ€navate ĂŒhendamine, uute majade ilmumine) vĂ€he isegi kogu riigi ulatuses.
3.1. TĂ€ieliku sĂŒnkroonimise algoritm
Lihtsuse huvides oletame, et andmete struktureerimist pole vaja â lihtsalt viige tabel soovitud vormi, st:
- kustutada kÔik, mida enam ei ole
- uuendada kÔik, mis oli, ja tuleb uuendada
- sisestada kÔik, mida polnud veel
Miks tuleb neid toiminguid tÀpselt niimoodi teha? Sest just nii kasvab tabeli suurus minimaalsetena ().
DELETE FROM dst
Ei, loomulikult saab hakkama ka vaid kahe toiminguga:
- kustutada (
DELETE) tegelikult kÔik - sisestada kÔik uuest pildist
Aga samas, MVCC tĂ”ttu, tabeli suurus kahekordistub! Saada +1M rekordite pilte tabelisse 10K uuendamise tĂ”ttu â pole just ideaalne ĂŒleliigsusâŠ
TRUNCATE dst
Kogenum arendaja teab, et kogu tabelit on vÔimalik rahaliselt odavalt puhastada:
- puhta (
TRUNCATE) kogu tabel - sisestada kÔik uuest pildist
Meetod on tĂ”hus, , kuid see toob kaasa probleemi⊠Me joonistame 1M kirjet ĂŒle vĂ€ga kaua, seega ei saa me endale lubada, et tabel oleks kogu selle aja tĂŒhjaks jĂ€etud (nagu juhtuks ilma ĂŒheainsa tehingu katmiseneta).
Seega:
- me alustame pikk tehing
TRUNCATEkehtestab AccessExclusive-lukku- teeme inserte pikka aega, kuid kÔik teised selle ajal ei saa isegi
SELECT
Midagi ei tundu hĂ€sti tulevatâŠ
ALTER TABLE⊠RENAME⊠/ DROP TABLE âŠ
VĂ”imalusena vĂ”iks kĂ”ik ĂŒle kanda uude eraldi tabelisse ja seejĂ€rel lihtsalt vana tabeli nime muuta. Paar ebameeldivat pisiasja:
- ka need AccessExclusive, kuigi oluliselt vÀhem ajakulu
- kustuvad kÔik pÀringute plaanid/statistika selle tabeli kohta,
- katkeb kÔik vÀlised vÔtmed (FK) sellele tabelile
Oli WIP-patch Simon Riggsilt, mis pakkus vÀlja ALTER-operatsiooni tabeli sisu asendamiseks failitasandil, mÔjutamata statistikat ja FK-d, kuid ei kogunud kvoori.
DELETE, UPDATE, INSERT
Nii et jÀÀme kolme operatsiooni mitteblokeeriva variandi juurde. Peaaegu kolme⊠Kuidas seda kÔige tÔhusamalt teha?
-- teeme kÔik tehingu raames, et keegi ei nÀeks "vahepealseid" olekuid
BEGIN;
-- loome ajutise tabeli imporditud andmetega
CREATE TEMPORARY TABLE tmp(
LIKE dst INCLUDING INDEXES -- sarnaselt, koos indeksitega
) ON COMMIT DROP; -- tehingu raames ei ole see meile vajalik
-- kiirelt viime uue sisu sisse lÀbi COPY
COPY tmp FROM STDIN;
-- ...
-- .
-- kustutame puuduvad
DELETE FROM
dst D
USING
dst X
LEFT JOIN
tmp Y
USING(pk1, pk2) -- primaarklahvi vÀljad
WHERE
(D.pk1, D.pk2) = (X.pk1, X.pk2) AND
Y IS NOT DISTINCT FROM NULL; -- "anti-join"
-- uuendame jÀÀnud
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); -- ei ole mÔtet uuendada kattuvaid
-- sisestame puuduvad
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. Impordi jÀrelprotsessimine
Samas KLADris tuleb kĂ”ik muudetud kirjed tĂ€iendavalt lĂ€bi viia jĂ€relprotsessimine â normaliseerida, eraldada mĂ€rksĂ”nad, viia sobivatesse struktuuridesse. Kuid kuidas teada â mida tĂ€pselt muudeti, keerukust sĂŒvenemata sĂŒnkroniseerimise koodi, ideaaljuhul, mitte puudutades seda ĂŒldse?
Kui kirjutamisĂ”igus sĂŒnkroniseerimise hetkel on ainult teie protsessil, siis vĂ”ib kasutada kĂ€ivitusklahvi, mis kogub kĂ”ik meie muudatused:
-- sihtlaud tabelid
CREATE TABLE kladr(...);
CREATE TABLE kladr_house(...);
-- muutuste ajalooga tabelid
CREATE TABLE kladr$log(
ro kladr, -- siin on vanade/uute kirgede tÀpsed koopiad
rn kladr
);
CREATE TABLE kladr_house$log(
ro kladr_house,
rn kladr_house
);
-- ĂŒldine funktsioon muudatuste logimiseks
CREATE OR REPLACE FUNCTION diff$log() RETURNS trigger AS $$
DECLARE
dst varchar = TG_TABLE_NAME || '$log';
stmt text = '';
BEGIN
-- kontrollime vajadust logimise jÀrel uuendamisel
IF TG_OP = 'UPDATE' THEN
IF NEW IS NOT DISTINCT FROM OLD THEN
RETURN NEW;
END IF;
END IF;
-- loome logikirje
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;
NĂŒĂŒd saame enne sĂŒnkroniseerimise algust kĂ€ivitada (vĂ”i aktiveerida 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();
SeejÀrel saame logitabelitest rahulikult vÀlja tÔmmata kÔik vajalikud muudatused ning saata need edasi tÀiendavatele töötlejatele.
3.3. Seotud komplektide import
Ălal arutlesime juhtumeid, kus allika ja sihtkoha andmestruktuurid kattuvad. Kuid mis juhul, kui vĂ€listest sĂŒsteemidest saadud eksport on formaadilt erinev meie andmebaasi ladustamisstruktuurist?
VĂ”tame nĂ€iteks klientide ja nende arvete hoidmise, klassikalise âĂŒhed-palatâ variandi:
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)
);Kuid vĂ€line allika eksport tuleb meile âkĂ”ik ĂŒhesâ kujul:
CREATE TEMPORARY TABLE invoice_import(
client_inn
varchar
, client_name
varchar
, invoice_number
varchar
, invoice_dt
date
, invoice_sum
numeric(32,2)
);Ilmselt vĂ”ivad klientide andmed sellises variandis dubleeruda, kuid peamine kirje on âarveâ:
0123456789;Vasily;A-01;2020-03-16;1000.00
9876543210;Peter;A-02;2020-03-16;666.00
0123456789;Vasily;B-03;2020-03-16;9999.00
Mudeli jaoks sisestame lihtsalt oma testandmed, kuid peame meeles pidama â COPY efektiivsemalt!
INSERT INTO invoice_import
VALUES
('0123456789', 'Vasily', 'A-01', '2020-03-16', 1000.00)
, ('9876543210', 'Peter', 'A-02', '2020-03-16', 666.00)
, ('0123456789', 'Vasily', 'B-03', '2020-03-16', 9999.00);Alustame nende âvĂ”tmeteâ esitlemisest, millele meie âfaktidâ viitavad. Meie puhul viitavad arved klientidele:
LOO TEMPORARY TABLE client_import AS
SELECT DISTINCT ON(client_inn)
-- vÔite lihtsalt kasutada SELECT DISTINCT, kui andmed on kindlasti vastuolulised
client_inn inn
, client_name "name"
FROM
invoice_import;Kuna soovime Ôigesti siduda arvete ID-d klientidega, peame esmalt need identifikaatorid vÀlja selgitama vÔi genereerima. Lisame nende jaoks vÀljad:
ALTER TABLE invoice_import ADD COLUMN client_id integer;
ALTER TABLE client_import ADD COLUMN client_id integer;Kasutame ĂŒlaltoodud viisi tabelite sĂŒnkroniseerimiseks, kuid vĂ€ikese muudatusega â me ei uuenda ega kustuta siht-tabletis, kuna klientide import on meil "append-only":
-- mÀÀrame imporditabelis olemasolevate kirjete ID-d
UPDATE
client_import T
SET
client_id = D.client_id
FROM
client D
WHERE
T.inn = D.inn; -- unikaalne vÔti
-- sisestame puuduvad kirjed ja mÀÀrame nende ID-d
WITH ins AS (
INSERT INTO client(
inn
, name
)
SELECT
inn
, name
FROM
client_import
WHERE
client_id IS NULL -- kui ID ei ole mÀÀratud
RETURNING *
)
UPDATE
client_import T
SET
client_id = D.client_id
FROM
ins D
WHERE
T.inn = D.inn; -- unikaalne vÔti
-- mÀÀrame klientide ID-d arvete kirjetes
UPDATE
invoice_import T
SET
client_id = D.client_id
FROM
client_import D
WHERE
T.client_inn = D.inn; -- rakenduslik vÔti
Sisuliselt on kĂ”ik â invoice_import nĂŒĂŒd on meil tĂ€idetud seose vĂ€li client_id, millega me ka arve sisestame.
Allikas: habr.com
