{"id":74953,"date":"2020-03-22T08:42:22","date_gmt":"2020-03-22T05:42:22","guid":{"rendered":"https:\/\/prohoster.info\/blog\/administrirovanie\/dba-gramotno-organizovyvaem-sinhronizaczii-i-importy"},"modified":"2020-03-22T08:42:22","modified_gmt":"2020-03-22T05:42:22","slug":"dba-gramotno-organizovyvaem-sinhronizaczii-i-importy","status":"publish","type":"post","link":"https:\/\/prohoster.info\/nl\/blog\/administrirovanie\/dba-gramotno-organizovyvaem-sinhronizaczii-i-importy","title":{"rendered":"DBA: we organiseren synchronisaties en imports op een nette manier","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<p>Bij de complexe verwerking van grote datasets (verschillende <noindex><a rel=\"nofollow\" href=\"https:\/\/ru.wikipedia.org\/wiki\/ETL\">ETL-processen<\/a><\/noindex>: imports, conversies en synchronisaties met een externe bron) ontstaat vaak de noodzaak <b>om tijdelijk iets te 'onthouden', en snel te verwerken<\/b> iets omvangrijks.<\/p>\n<p>Een typische taak van deze aard klinkt meestal ongeveer zo: <i>\"Hier heeft de boekhouding recent ontvangen betalingen uit het klantensysteem ge\u00ebxporteerd, we moeten deze snel naar de website uploaden en aan rekeningen koppelen\" <noindex><a rel=\"nofollow\" href=\"https:\/\/sbis.ru\/accounting\">Maar wanneer de omvang van dit \"iets\" begint te meten in honderden megabytes, en de service tegelijkertijd 24\u00d77 met de database moet blijven werken, ontstaan er veel side-effecten die je het leven moeilijk maken.<\/a><\/noindex> Om hiermee om te gaan in PostgreSQL (en niet alleen daar), kunnen we gebruikmaken van verschillende optimalisatiemogelijkheden die het mogelijk maken om alles sneller en met minder middelen te verwerken.<\/i><\/p>\n<p>Maar wanneer de hoeveelheid van dit \"iets\" begint te worden gemeten in honderden megabytes, en de service ondertussen 24\/7 met een database moet blijven werken, ontstaan er veel bijeffecten die je leven kunnen verpesten.<br \/>\n<img decoding=\"async\" alt=\"DBA: we organiseren synchronisaties en imports op een nette manier\" src=\"\/wp-content\/uploads\/2020\/03\/f74afb2cd6f5f8de26a0932166933c95.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\nLaten we eerst vaststellen waar we de gegevens kunnen uploaden die we willen 'verwerken'.<br \/>\n<noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><\/p>\n<h2>1.1. Tijdelijke tabellen (TEMPORARY TABLE)<\/h2>\n<p>\nIn principe zijn tijdelijke tabellen in PostgreSQL net zo'n tabel als elke andere. Daarom zijn bijgeloof zoals<\/p>\n<h3>\"het wordt alleen in het geheugen opgeslagen, en dat kan op raken\"<\/h3>\n<p>\nniet correct. <i><b>Maar er zijn ook enkele belangrijke verschillen.<\/b><\/i>Een eigen 'naamruimte' voor elke databaseverbinding<\/p>\n<h4>Als twee verbindingen tegelijkertijd proberen uit te voeren<\/h4>\n<p>\nCREATE TABLE x <code>, dan krijgt iemand zeker een<\/code>uniquenessfout <b>van databaseobjecten.<\/b> Maar als beide proberen uit te voeren,<\/p>\n<p>, dan kunnen beiden dit normaal doen, en krijgt elke verbinding <code>CREATE <b>Tijdelijk<\/b> TABEL x<\/code>zijn eigen instantie <b>van de tabel. En er is niets gemeen tussen hen.<\/b> \"Zelfvernietiging\" bij disconnect<\/p>\n<h4>Bij het sluiten van de verbinding worden alle tijdelijke tabellen automatisch verwijderd, dus heeft het geen zin om<\/h4>\n<p>\nDROP TABLE x <code>handmatig uit te voeren, behalve\u2026<\/code> Als je werkt via<\/p>\n<p>pgbouncer in transaction mode <b>, dan denkt de database dat deze verbinding nog steeds actief is, en dan bestaat deze tijdelijke tabel daar nog steeds.<\/b>Dus de poging om deze opnieuw te maken, al vanuit een andere pgbouncer-verbinding, zal resulteren in een fout. Maar dit kan worden omzeild door gebruik te maken van<\/p>\n<p>Een poging om het opnieuw te cre\u00ebren vanuit een andere verbinding naar pgbouncer zal leiden tot een fout. Maar dit kan worden omzeild door gebruik te maken van <code>CRE\u00cbER TUSSENTIJDELIJKE TABEL <b>ALS HET NOG NIET BESTAAT<\/b> x<\/code>.<\/p>\n<p>Het is beter om dit niet te doen, omdat je dan plotseling gegevens kunt ontdekken die van de 'vorige eigenaar' zijn. In plaats daarvan is het veel beter om de handleiding te lezen en te zien dat er bij het aanmaken van een tabel de mogelijkheid is om <code>Bij commit <b>DROP<\/b><\/code> \u2014 dat wil zeggen dat de tabel automatisch zal worden verwijderd bij het afronden van de transactie.<\/p>\n<h4>Niet-replicatie<\/h4>\n<p>\nVanwege de toewijzing aan een specifiek verband worden tijdelijke tabellen niet gerepliceerd. Maar <b>dit elimineert de noodzaak voor dubbele gegevensopslag<\/b> in heap + WAL, waardoor INSERT\/UPDATE\/DELETE daarin aanzienlijk sneller zijn.<\/p>\n<p>Maar omdat een tijdelijke tabel toch 'bijna een gewone' tabel is, kan deze ook niet op de replica worden aangemaakt. Tenminste, voorlopig niet, hoewel de bijbehorende patch al lang in omloop is.<\/p>\n<h3>1.2. Niet-logbare tabellen (UNLOGGED TABLE)<\/h3>\n<p>\nMaar wat te doen als je bijvoorbeeld een zwaar ETL-proces hebt dat niet binnen \u00e9\u00e9n transactie kan worden uitgevoerd en dat je toch <b>, dan denkt de database dat deze verbinding nog steeds actief is, en dan bestaat deze tijdelijke tabel daar nog steeds.<\/b>?..<\/p>\n<p>Of de gegevensstroom is zo groot dat <b>de bandbreedte van \u00e9\u00e9n verbinding niet voldoende is<\/b> met de database (lees: \u00e9\u00e9n proces op de CPU)?..<\/p>\n<p>Of sommige operaties verlopen <b>asynchroon<\/b> via verschillende verbindingen?..<\/p>\n<p>Hier is er maar \u00e9\u00e9n optie \u2014 <b>tijdelijk een niet-tijdelijke tabel aanmaken<\/b>. Een woordgrap, h\u00e8. Dat wil zeggen:<\/p>\n<ul>\n<li>je hebt 'jouw' tabellen met maximaal willekeurige namen aangemaakt, zodat er geen overlap is met anderen.<\/li>\n<li><b>Extract<\/b>: je hebt gegevens uit een externe bron in die tabellen geladen.<\/li>\n<li><b>Transform<\/b>: geconfigureerd en de sleutelverbindingvelden ingevuld.<\/li>\n<li><b>Load<\/b>: je hebt de voltooide gegevens in de doelstellingen getransfereerd.<\/li>\n<li>je hebt 'jouw' tabellen verwijderd.<\/li>\n<\/ul>\n<p>\nEn nu \u2014 de lepel teer. In wezen <b>vindt alle opslag in PostgreSQL twee keer plaats<\/b> \u2014 <noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/postgrespro\/blog\/461523\/\">eerst in WAL<\/a><\/noindex>, daarna in de lichaam van de tabellen\/indexen. Dit is allemaal gedaan voor de ondersteuning van ACID en een correcte zichtbaarheid van gegevens tussen <code>COMMIT<\/code>begrepen en <code>ROLLBACK<\/code>begrepen transacties.<\/p>\n<p>Maar dat hebben we niet nodig! Ons hele proces <b>is ofwel volledig succesvol verlopen, of niet.<\/b>. Het maakt niet uit hoeveel tussentijdse transacties erin zitten \u2014 we zijn niet ge\u00efnteresseerd in 'het proces voortzetten vanaf het midden', vooral niet als onduidelijk is waar het was.<\/p>\n<p>Om dit te doen hebben de ontwikkelaars van PostgreSQL al in versie 9.1 zoiets ge\u00efmplementeerd als <noindex><a rel=\"nofollow\" href=\"https:\/\/postgrespro.ru\/docs\/postgresql\/12\/sql-createtable#SQL-CREATETABLE-UNLOGGED\">niet-logbare (UNLOGGED) tabellen.<\/a><\/noindex>:<\/p>\n<blockquote><p>Met deze aanwijzing wordt de tabel aangemaakt als niet-logbaar. Gegevens die in niet-logbare tabellen worden opgeslagen, verlopen niet via het oplopend logboek (zie Hoofdstuk 29), waardoor deze tabellen <b>werken veel sneller dan gewone<\/b>. Echter, ze zijn niet beschermd tegen uitval; bij uitval of een onverwachte serverstop wordt de niet-geloggde tabel <b>automatisch afgebroken<\/b>. Bovendien wordt de inhoud van de niet-geloggde tabel <b>niet gerepliceerd<\/b> naar de secundaire servers. Alle indexen die voor de niet-geloggde tabel worden gemaakt, worden automatisch niet-geloggde.<\/p><\/blockquote>\n<p>Kortom, <b>zal het veel sneller zijn<\/b>, maar als de DB-server 'crasht' - zal dat vervelend zijn. Maar hoe vaak gebeurt dat, en kan uw ETL-proces dit correct afhandelen vanaf het 'midden' na het 'herleven' van de DB?..<\/p>\n<p>Als dat niet het geval is, en de bovenstaande case lijkt op de uwe - gebruik dan <code>UNLOGGED<\/code>, maar schakel deze attribuut nooit <b>in voor echte tabellen<\/b>, waarvan u de gegevens belangrijk zijn.<\/p>\n<h3>1.3. ON COMMIT { DELETE ROWS | DROP }<\/h3>\n<p>\nDeze constructie stelt u in staat om bij het aanmaken van een tabel automatisch gedrag op te geven bij het be\u00ebindigen van de transactie.<\/p>\n<p>Over <code>Bij commit <b>DROP<\/b><\/code> heb ik hierboven al geschreven, het genereert <code>DROP TABLE<\/code>, maar met <code>Bij commit <b>Rijen verwijderen<\/b><\/code> is de situatie interessanter - hier wordt gegenereerd <code>TRUNCATE TABLE<\/code>.<\/p>\n<p>Aangezien de hele infrastructuur voor het opslaan van de meta-beschrijving van de tijdelijke tabel precies hetzelfde is als die van de gewone, leidt <b>voortdurend aanmaken-verwijderen van tijdelijke tabellen tot sterke 'uitbreiding' van de systeemtabellen<\/b> pg_class, pg_attribute, pg_attrdef, pg_depend,\u2026<\/p>\n<p>Stel je nu voor dat je een worker hebt met een directe verbinding met de DB, die elke seconde een nieuwe transactie opent, een tijdelijke tabel aanmaakt, vult, verwerkt en verwijdert\u2026 Er zal teveel rommel in de systeemtabellen ophopen, wat onnodige vertragingen bij elke operatie met zich meebrengt.<\/p>\n<p>In het algemeen, zo moet je het niet doen! In dit geval is het veel effici\u00ebnter om <code>CREATE TEMPORARY TABLE x ... ON COMMIT DELETE ROWS<\/code> buiten de transactiecyclus te plaatsen - dan zal de tabel aan het begin van elke nieuwe transactie <b>al bestaan<\/b> (we besparen aanroep <code>CREATE<\/code>), maar <b>zal leeg zijn<\/b>, dankzij <code>TRUNCATE<\/code> (die aanroep hebben we ook bespaard) bij het be\u00ebindigen van de vorige transactie.<\/p>\n<h3>1.4. NET ALS\u2026 INCLUSIEF \u2026<\/h3>\n<p>\nIk heb aan het begin genoemd dat een van de typische use cases voor tijdelijke tabellen allerlei soorten importen zijn - en de ontwikkelaar kopieert vermoeid de lijst met velden van de doeltabel naar de declaratie van zijn tijdelijke\u2026<\/p>\n<p>Maar luiheid is de motor van vooruitgang! Daarom <b>kun je een nieuwe tabel 'naar voorbeeld' maken<\/b> veel eenvoudiger:<\/p>\n<pre><code class=\"sql\">CREATE TEMPORARY TABLE import_table(\n  LIKE target_table\n);<\/code><\/pre>\n<p>\nOmdat je in deze tabel behoorlijk veel gegevens kunt genereren, zal het zoeken daarin allesbehalve snel zijn. Maar er is een traditioneel oplossing hiervoor - indexen! En ja, <b>de tijdelijke tabel kan ook indexen hebben<\/b>.<\/p>\n<p>Omdat de benodigde indexen vaak overeenkomen met de indexen van de doeltabel, kun je eenvoudig schrijven <code>LIKE target_table <b>INCLUSIEF INDEXEN<\/b><\/code>.<\/p>\n<p>Als je ook nog eens <code>DEFAULT<\/code>-waarden nodig hebt (bijvoorbeeld om de waarden van de primaire sleutel in te vullen), kun je gebruikmaken van <code>LIKE target_table <b>INCLUSIEF STANDAARDINSTELLINGEN<\/b><\/code>. Of gewoon - <code>LIKE target_table <b>INCLUSIEF ALLES<\/b><\/code> zal de defaults, indexen, constraints,\u2026 kopi\u00ebren<\/p>\n<p>Maar hier moet je wel begrijpen dat als je de <b>importtabel meteen met indexen hebt gemaakt, de gegevens langer zullen laden<\/b>, dan als je eerst alles laadt en vervolgens de indexen aanbrengt - kijk als voorbeeld hoe dit werkt bij <noindex><a rel=\"nofollow\" href=\"https:\/\/postgrespro.ru\/docs\/postgresql\/12\/app-pgdump\">pg_dump<\/a><\/noindex>.<\/p>\n<p>Kortom, <noindex><a rel=\"nofollow\" href=\"https:\/\/postgrespro.ru\/docs\/postgresql\/12\/sql-createtable\">RTFM<\/a><\/noindex>!<\/p>\n<h2>2. Hoe te schrijven?<\/h2>\n<p>\nIk zeg gewoon - gebruik <code><noindex><a rel=\"nofollow\" href=\"https:\/\/postgrespro.ru\/docs\/postgresql\/12\/sql-copy\">COPY<\/a><\/noindex><\/code>-stream in plaats van \"batch\" <code>INSERT<\/code>, <noindex><a rel=\"nofollow\" href=\"https:\/\/www.citusdata.com\/blog\/2017\/11\/08\/faster-bulk-loading-in-postgresql-with-copy\/\">versnelling met factor meerdere<\/a><\/noindex>. Je kunt zelfs direct vanuit een vooraf gevormd bestand werken.<\/p>\n<h2>3. Hoe te verwerken?<\/h2>\n<p>\nDus, laten we zeggen dat onze invoer er ongeveer zo uitziet:<\/p>\n<ul>\n<li>je hebt een tabel met klantgegevens in je database met <b>1M records<\/b><\/li>\n<li>iedere dag stuurt de klant je een nieuwe <b>volledige 'afbeelding'<\/b><\/li>\n<li>uit ervaring weet je dat er niet meer dan <b>10K records per keer veranderen<\/b><\/li>\n<\/ul>\n<p>\nEen klassiek voorbeeld van een dergelijke situatie is <noindex><a rel=\"nofollow\" href=\"https:\/\/www.gnivc.ru\/technical_support\/classifiers_reference\/kladr\/\">de KLADR-database<\/a><\/noindex> - er zijn veel adressen, maar in elke wekelijkse export van wijzigingen (hernoemingen van plaatsen, samenvoegen van straten, verschijnen van nieuwe huizen) is er heel weinig, zelfs op nationaal niveau.<\/p>\n<h3>3.1. Algoritme voor volledige synchronisatie<\/h3>\n<p>\nOm het eenvoudig te houden, laten we aannemen dat je de gegevens zelfs niet hoeft te herstructureren - einfach de tabel in de juiste vorm brengen, namelijk:<\/p>\n<ul>\n<li><b>te verwijderen<\/b> alles wat er al niet meer is<\/li>\n<li><b>bijwerken<\/b> alles wat er al was en vernieuwd moet worden<\/li>\n<li><b>invoegen<\/b> alles wat er nog niet was<\/li>\n<\/ul>\n<p>\nWaarom zou je de operaties in deze volgorde uitvoeren? Omdat de grootte van de tabel zo minimaal toeneemt (<noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/tensor\/blog\/491366\/\">vergeet MVCC niet!<\/a><\/noindex>).<\/p>\n<h4>DELETE FROM dst<\/h4>\n<p>\nNee, je kunt natuurlijk volstaan met slechts twee operaties:<\/p>\n<ul>\n<li><b>te verwijderen<\/b> (<code>HEAD<\/code>) alles<\/li>\n<li><b>invoegen<\/b> alles uit de nieuwe afbeelding<\/li>\n<\/ul>\n<p>\nMaar hierdoor zal de grootte van de tabel dankzij MVCC precies verdubbelen <b>! Het krijgen van +1M records in de tabel door 10K te updaten - dat is een behoorlijke overbodigheid\u2026<\/b>TRUNCATE dst<\/p>\n<h4>Een meer ervaren ontwikkelaar weet dat je de hele tabel relatief goedkoop kunt opschonen:<\/h4>\n<p>\nopschonen<\/p>\n<ul>\n<li><b>) de hele tabel<\/b> (<code>TRUNCATE<\/code>Een effectieve methode,<\/li>\n<li><b>invoegen<\/b> alles uit de nieuwe afbeelding<\/li>\n<\/ul>\n<p>\nsoms heel goed toepasbaar <noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/tensor\/blog\/481866\/\">en soms heel goed toepasbaar<\/a><\/noindex>, maar er is een probleem\u2026 Het importeren van 1 miljoen records zal heel lang duren, dus we kunnen ons niet veroorloven om de tabel gedurende deze tijd leeg te laten (zoals het zal gebeuren zonder het in \u00e9\u00e9n transactie te wikkelen).<\/p>\n<p>Dat betekent:<\/p>\n<ul>\n<li>we beginnen met <b>een langdurige transactie<\/b><\/li>\n<li><code>TRUNCATE<\/code> legt een <b>AccessExclusive<\/b>-lock op<\/li>\n<li>we zijn lang bezig met invoegen, en ondertussen <b>kunnen anderen zelfs niet <code>SELECT<\/code><\/b><\/li>\n<\/ul>\n<p>\nDit gaat niet goed\u2026<\/p>\n<h4>ALTER TABLE\u2026 RENAME\u2026 \/ DROP TABLE \u2026<\/h4>\n<p>\nEen optie is om alles in een nieuwe tabel te plaatsen en deze vervolgens gewoon te hernoemen naar de oude. Een paar vervelende details:<\/p>\n<ul>\n<li>dat gaat ook <b>AccessExclusive<\/b>, zij het veel sneller<\/li>\n<li>al de query-plannen\/statistieken van deze tabel worden gereset, <noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/tensor\/blog\/479656\/\">we moeten ANALYZE uitvoeren<\/a><\/noindex><\/li>\n<li><b>alle externe sleutels (FK) naar de tabel zijn defect<\/b> Er was een WIP-patch van Simon Riggs, die voorstelde om een<\/li>\n<\/ul>\n<p>\nALTER <code>-operatie te maken voor het vervangen van de tabelstructuur op bestandniveau, zonder de statistieken en FK aan te raken, maar hij kwam niet tot een quorum.<\/code>DELETE, UPDATE, INSERT<\/p>\n<h4>Dus, we gaan voor de niet-blokkerende optie uit de drie bewerkingen. Bijna drie\u2026 Hoe doen we dit het meest effici\u00ebnt?<\/h4>\n<p>\n-- alles doen binnen een transactie, zodat niemand \"tussentijdse\" toestanden ziet\nBEGIN;\n\n-- cre\u00ebren van een tijdelijke tabel met de te importeren gegevens\nCREATE TEMPORARY TABLE tmp(\n  LIKE dst INCLUDING INDEXES -- naar het voorbeeld, inclusief indexen\n) ON COMMIT DROP; -- buiten de transactie hebben we deze niet meer nodig\n\n-- snel de nieuwe structuur importeren via COPY\nCOPY tmp FROM STDIN;\n-- ...\n-- .\n\n-- ontbrekende records verwijderen\nDELETE FROM\n  dst D\nUSING\n  dst X\nLEFT JOIN\n  tmp Y\n    USING(pk1, pk2) -- primaire sleutels\nWHERE\n  (D.pk1, D.pk2) = (X.pk1, X.pk2) AND\n  Y IS NOT DISTINCT FROM NULL; -- \"anti-join\"\n\n-- de overblijvende records bijwerken\nUPDATE\n  dst D\nSET\n  (f1, f2, f3) = (T.f1, T.f2, T.f3)\nFROM\n  tmp T\nWHERE\n  (D.pk1, D.pk2) = (T.pk1, T.pk2) AND\n  (D.f1, D.f2, D.f3) IS DISTINCT FROM (T.f1, T.f2, T.f3); -- geen reden om overlappende records bij te werken\n\n-- ontbrekende records invoegen\nINSERT INTO\n  dst\nSELECT\n  T.*\nFROM\n  tmp T\nLEFT JOIN\n  dst D\n    USING(pk1, pk2)\nWHERE\n  D IS NOT DISTINCT FROM NULL;\n\nCOMMIT;<\/p>\n<pre><code class=\"sql\">-- alles gebeurt binnen een transactie, zodat niemand de \"tussen\"-toestanden ziet\nBEGIN;\n\n-- maak een tijdelijke tabel met de ge\u00efmporteerde gegevens\nCREATE TEMPORARY TABLE tmp(\n  LIKE dst INCLUDING INDEXES -- naar analogie, samen met indexen\n) ON COMMIT DROP; -- buiten de transactie hebben we deze niet meer nodig\n\n-- snel de nieuwe afbeelding invoegen via COPY\nCOPY tmp FROM STDIN;\n-- ...\n-- .\n\n-- verwijder ontbrekende\nDELETE FROM\n  dst D\nUSING\n  dst X\nLEFT JOIN\n  tmp Y\n    USING(pk1, pk2) -- velden van de primaire sleutel\nWHERE\n  (D.pk1, D.pk2) = (X.pk1, X.pk2) AND\n  Y IS NOT DISTINCT FROM NULL; -- \"anti-join\"\n\n-- werk de overgebleven bij\nUPDATE\n  dst D\nSET\n  (f1, f2, f3) = (T.f1, T.f2, T.f3)\nFROM\n  tmp T\nWHERE\n  (D.pk1, D.pk2) = (T.pk1, T.pk2) AND\n  (D.f1, D.f2, D.f3) IS DISTINCT FROM (T.f1, T.f2, T.f3); -- niet nodig om overeenkomende records bij te werken\n\n-- voeg ontbrekende toe\nINSERT INTO\n  dst\nSELECT\n  T.*\nFROM\n  tmp T\nLEFT JOIN\n  dst D\n    USING(pk1, pk2)\nWHERE\n  D IS NOT DISTINCT FROM NULL;\n\nCOMMIT;\n<\/code><\/pre>\n<p><\/p>\n<h3>In dezelfde KADRa moeten alle gewijzigde records bovendien door nazorg worden gecorrigeerd \u2014 genormaliseerd, sleutelwoorden ge\u00ebxtraheerd en naar de juiste structuren gebracht. Maar hoe weten we \u2014<\/h3>\n<p>\nwat precies is gewijzigd <b>, zonder de synchronisatiecode te compliceren, idealiter zelfs niets aan te raken?<\/b>Als er op het moment van synchronisatie alleen uw proces schrijf toegang heeft, kan een trigger worden gebruikt die al onze wijzigingen verzamelt:<\/p>\n<p>Als het schrijfrecht op het moment van synchroniseren alleen aan jouw proces behoort, kun je gebruikmaken van een trigger die alle wijzigingen voor ons verzamelt:<\/p>\n<pre><code class=\"sql\">-- doel tabellen\nCREATE TABLE kladr(...);\nCREATE TABLE kladr_house(...);\n\n-- tabellen met geschiedenis van wijzigingen\nCREATE TABLE kladr$log(\n  ro kladr, -- hier liggen de volledige beelden van oude\/nieuwe records\n  rn kladr\n);\n\nCREATE TABLE kladr_house$log(\n  ro kladr_house,\n  rn kladr_house\n);\n\n-- algemene functie voor het loggen van wijzigingen\nCREATE OR REPLACE FUNCTION diff$log() RETURNS trigger AS $$\nDECLARE\n  dst varchar = TG_TABLE_NAME || '$log';\n  stmt text = '';\nBEGIN\n  -- controleer de noodzaak van logging bij het bijwerken van een record\n  IF TG_OP = 'UPDATE' THEN\n    IF NEW IS NOT DISTINCT FROM OLD THEN\n      RETURN NEW;\n    END IF;\n  END IF;\n  -- maak een logrecord aan\n  stmt = 'INSERT INTO ' || dst::text || '(ro,rn)VALUES(';\n  CASE TG_OP\n    WHEN 'INSERT' THEN\n      EXECUTE stmt || 'NULL,$1)' USING NEW;\n    WHEN 'UPDATE' THEN\n      EXECUTE stmt || '$1,$2)' USING OLD, NEW;\n    WHEN 'DELETE' THEN\n      EXECUTE stmt || '$1,NULL)' USING OLD;\n  END CASE;\n  RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\n<\/code><\/pre>\n<p>\nNu kunnen we v\u00f3\u00f3r het starten van de synchronisatie triggers aanbrengen (of inschakelen via <code>ALTER TABLE ... ENABLE TRIGGER ...<\/code>):<\/p>\n<pre><code class=\"sql\">CREATE TRIGGER log\n  AFTER INSERT OR UPDATE OR DELETE\n  ON kladr\n    FOR EACH ROW\n      EXECUTE PROCEDURE diff$log();\n\nCREATE TRIGGER log\n  AFTER INSERT OR UPDATE OR DELETE\n  ON kladr_house\n    FOR EACH ROW\n      EXECUTE PROCEDURE diff$log();\n<\/code><\/pre>\n<p>\nEn dan kunnen we eenvoudig alle benodigde wijzigingen uit de log-tabellen ophalen en deze door aanvullende verwerkers laten lopen.<\/p>\n<h3>3.3. Importeren van gerelateerde sets<\/h3>\n<p>\nBoven hebben we gevallen besproken waarin de datastructuren van de bron en de ontvanger overeenkomen. Maar wat te doen als de export uit een extern systeem een formaat heeft dat verschilt van de opslagstructuur in onze database?<\/p>\n<p>Laten we als voorbeeld het opslaan van klanten en hun facturen nemen, een klassiek geval van 'veel-tot-een':<\/p>\n<pre><code class=\"sql\">CREATE TABLE client(\n  client_id\n    serial\n      PRIMARY KEY\n, inn\n    varchar\n      UNIQUE\n, name\n    varchar\n);\n\nCREATE TABLE invoice(\n  invoice_id\n    serial\n      PRIMARY KEY\n, client_id\n    integer\n      REFERENCES client(client_id)\n, number\n    varchar\n, dt\n    date\n, sum\n    numeric(32,2)\n);<\/code><\/pre>\n<p>\nMaar de export uit de externe bron komt bij ons binnen als 'alles-in-een':<\/p>\n<pre><code class=\"sql\">CREATE TEMPORARY TABLE invoice_import(\n  client_inn\n    varchar\n, client_name\n    varchar\n, invoice_number\n    varchar\n, invoice_dt\n    date\n, invoice_sum\n    numeric(32,2)\n);<\/code><\/pre>\n<p>\nHet is duidelijk dat de gegevens van klanten in deze opzet kunnen dupliceren, en de hoofdrecord is de 'factuur':<\/p>\n<pre><code class=\"plaintext\">0123456789;Vasily;A-01;2020-03-16;1000.00\n9876543210;Petya;A-02;2020-03-16;666.00\n0123456789;Vasily;B-03;2020-03-16;9999.00\n<\/code><\/pre>\n<p>\nVoor het model voegen we eenvoudig onze testgegevens in, maar onthoud \u2014 <code>COPY<\/code> effici\u00ebnter!<\/p>\n<pre><code class=\"sql\">INSERT INTO invoice_import\nVALUES\n  ('0123456789', 'Vasily', 'A-01', '2020-03-16', 1000.00)\n, ('9876543210', 'Petya', 'A-02', '2020-03-16', 666.00)\n, ('0123456789', 'Vasily', 'B-03', '2020-03-16', 9999.00);<\/code><\/pre>\n<p>\nLaten we eerst de 'sneden' \u0432\u044b\u0434\u0435\u043b\u0438\u043c waarop onze 'feiten' verwijzen. In ons geval verwijzen de facturen naar de klanten:<\/p>\n<pre><code class=\"sql\">CREATE TEMPORARY TABLE client_import AS\nSELECT DISTINCT ON(client_inn)\n-- gewoon SELECT DISTINCT, als de gegevens bij voorbaat niet tegenstrijdig zijn\n  client_inn inn\n, client_name \"name\"\nFROM\n  invoice_import;<\/code><\/pre>\n<p>\nOm rekeningen correct te koppelen aan klant-ID's, moeten we deze identificaties eerst weten of genereren. We voegen velden voor hen toe:<\/p>\n<pre><code class=\"sql\">ALTER TABLE invoice_import ADD COLUMN client_id integer;\nALTER TABLE client_import ADD COLUMN client_id integer;<\/code><\/pre>\n<p>\nWe maken gebruik van de hierboven beschreven methode voor het synchroniseren van tabellen met een kleine aanpassing \u2014 we zullen niets bijwerken of verwijderen in de doeltabel, want de import van klanten is 'append-only':<\/p>\n<pre><code class=\"sql\">-- we vullen de importtabel met ID's van reeds bestaande records\nUPDATE\n  client_import T\nSET\n  client_id = D.client_id\nVAN\n  client D\nWAAR\n  T.inn = D.inn; -- unieke sleutel\n\n-- we voegen ontbrekende records in en vullen hun ID's in\nWITH ins AS (\n  INSERT INTO client(\n    inn\n  , name\n  )\n  SELECT\n    inn\n  , name\n  VAN\n    client_import\n  WAAR\n    client_id IS NULL -- als ID niet is ingevuld\n  RETURNING *\n)\nUPDATE\n  client_import T\nSET\n  client_id = D.client_id\nVAN\n  ins D\nWAAR\n  T.inn = D.inn; -- unieke sleutel\n\n-- we vullen ID's van klanten in bij de rekeningrecords\nUPDATE\n  invoice_import T\nSET\n  client_id = D.client_id\nVAN\n  client_import D\nWAAR\n  T.client_inn = D.inn; -- toepasselijke sleutel\n<\/code><\/pre>\n<p>\nEigenlijk is dat alles \u2014 in <code>invoice_import<\/code> nu is het veld voor de koppeling ingevuld <code>client_id<\/code>, dat we zullen gebruiken om de rekening in te voegen.<br \/>\n<br \/>Bron: <a content=\"nofollow\" rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/tensor\/blog\/492464\/\">habr.com<\/a> <\/p>","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"excerpt":{"rendered":"<p>\u041f\u0440\u0438 \u0441\u043b\u043e\u0436\u043d\u043e\u0439 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u0431\u043e\u043b\u044c\u0448\u0438\u0445 \u043d\u0430\u0431\u043e\u0440\u043e\u0432 \u0434\u0430\u043d\u043d\u044b\u0445 (\u0440\u0430\u0437\u043d\u044b\u0435 ETL-\u043f\u0440\u043e\u0446\u0435\u0441\u0441\u044b: \u0438\u043c\u043f\u043e\u0440\u0442\u044b, \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438 \u0438 \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u0438 \u0441 \u0432\u043d\u0435\u0448\u043d\u0438\u043c \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u043c) \u0447\u0430\u0441\u0442\u043e \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u0435\u0442 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u044c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u00ab\u0437\u0430\u043f\u043e\u043c\u043d\u0438\u0442\u044c\u00bb, \u0438 \u0441\u0440\u0430\u0437\u0443 \u0431\u044b\u0441\u0442\u0440\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0447\u0442\u043e-\u0442\u043e \u043e\u0431\u044a\u0435\u043c\u043d\u043e\u0435. \u0422\u0438\u043f\u043e\u0432\u0430\u044f \u0437\u0430\u0434\u0430\u0447\u0430 \u043f\u043e\u0434\u043e\u0431\u043d\u043e\u0433\u043e \u0440\u043e\u0434\u0430 \u0437\u0432\u0443\u0447\u0438\u0442 \u043e\u0431\u044b\u0447\u043d\u043e \u043f\u0440\u0438\u043c\u0435\u0440\u043d\u043e \u0442\u0430\u043a: \u00ab\u0412\u043e\u0442 \u0442\u0443\u0442 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0438\u044f \u0432\u044b\u0433\u0440\u0443\u0437\u0438\u043b\u0430 \u0438\u0437 \u043a\u043b\u0438\u0435\u043d\u0442-\u0431\u0430\u043d\u043a\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0438\u0432\u0448\u0438\u0435 \u043e\u043f\u043b\u0430\u0442\u044b, \u043d\u0430\u0434\u043e \u0438\u0445 \u0431\u044b\u0441\u0442\u0440\u0435\u043d\u044c\u043a\u043e \u0432\u043a\u0430\u0447\u0430\u0442\u044c \u043d\u0430 \u0441\u0430\u0439\u0442 \u0438 \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u043a \u0441\u0447\u0435\u0442\u0430\u043c\u00bb \u041d\u043e \u043a\u043e\u0433\u0434\u0430 [&hellip;]<\/p>\n","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"author":1,"featured_media":74954,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[688],"tags":[],"class_list":["post-74953","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-administrirovanie"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.3 - aioseo.com -->\n\t<meta name=\"description\" content=\"\u041f\u0440\u0438 \u0441\u043b\u043e\u0436\u043d\u043e\u0439 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u0431\u043e\u043b\u044c\u0448\u0438\u0445 \u043d\u0430\u0431\u043e\u0440\u043e\u0432 \u0434\u0430\u043d\u043d\u044b\u0445 (\u0440\u0430\u0437\u043d\u044b\u0435 ETL-\u043f\u0440\u043e\u0446\u0435\u0441\u0441\u044b: \u0438\u043c\u043f\u043e\u0440\u0442\u044b, \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438 \u0438 \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u0438 \u0441 \u0432\u043d\u0435\u0448\u043d\u0438\u043c \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u043c) \u0447\u0430\u0441\u0442\u043e.\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"Yuri Gagarin\"\/>\n\t<link rel=\"canonical\" href=\"https:\/\/prohoster.info\/nl\/blog\/administrirovanie\/dba-gramotno-organizovyvaem-sinhronizaczii-i-importy\" \/>\n\t\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.3\" \/>\n\t\t<meta property=\"og:locale\" content=\"nl_NL\" \/>\n\t\t<meta property=\"og:site_name\" content=\"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"\ud83e\udd47DBA: \u0433\u0440\u0430\u043c\u043e\u0442\u043d\u043e \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u043e\u0432\u044b\u0432\u0430\u0435\u043c \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u044b | ProHoster\" \/>\n\t\t<meta property=\"og:description\" content=\"\u041f\u0440\u0438 \u0441\u043b\u043e\u0436\u043d\u043e\u0439 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u0431\u043e\u043b\u044c\u0448\u0438\u0445 \u043d\u0430\u0431\u043e\u0440\u043e\u0432 \u0434\u0430\u043d\u043d\u044b\u0445 (\u0440\u0430\u0437\u043d\u044b\u0435 ETL-\u043f\u0440\u043e\u0446\u0435\u0441\u0441\u044b: \u0438\u043c\u043f\u043e\u0440\u0442\u044b, \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438 \u0438 \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u0438 \u0441 \u0432\u043d\u0435\u0448\u043d\u0438\u043c \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u043c) \u0447\u0430\u0441\u0442\u043e.\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/prohoster.info\/nl\/blog\/administrirovanie\/dba-gramotno-organizovyvaem-sinhronizaczii-i-importy\" \/>\n\t\t<meta property=\"og:image\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:secure_url\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:width\" content=\"350\" \/>\n\t\t<meta property=\"og:image:height\" content=\"350\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2020-03-22T05:42:22+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2020-03-22T05:42:22+00:00\" \/>\n\t\t<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<meta property=\"article:author\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"\ud83e\udd47DBA: we organiseren synchronisaties en imports professioneel | ProHoster","description":"Bij complexe verwerking van grote datasets (verschillende ETL-processen: import, conversies en synchronisaties met een externe bron) vaak.","canonical_url":"https:\/\/prohoster.info\/nl\/blog\/administrirovanie\/dba-gramotno-organizovyvaem-sinhronizaczii-i-importy","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":null,"og:locale":"nl_NL","og:site_name":"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b","og:type":"article","og:title":"\ud83e\udd47DBA: \u0433\u0440\u0430\u043c\u043e\u0442\u043d\u043e \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u043e\u0432\u044b\u0432\u0430\u0435\u043c \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u044b | ProHoster","og:description":"\u041f\u0440\u0438 \u0441\u043b\u043e\u0436\u043d\u043e\u0439 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u0431\u043e\u043b\u044c\u0448\u0438\u0445 \u043d\u0430\u0431\u043e\u0440\u043e\u0432 \u0434\u0430\u043d\u043d\u044b\u0445 (\u0440\u0430\u0437\u043d\u044b\u0435 ETL-\u043f\u0440\u043e\u0446\u0435\u0441\u0441\u044b: \u0438\u043c\u043f\u043e\u0440\u0442\u044b, \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438 \u0438 \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u0438 \u0441 \u0432\u043d\u0435\u0448\u043d\u0438\u043c \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u043c) \u0447\u0430\u0441\u0442\u043e.","og:url":"https:\/\/prohoster.info\/nl\/blog\/administrirovanie\/dba-gramotno-organizovyvaem-sinhronizaczii-i-importy","og:image":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:secure_url":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:width":350,"og:image:height":350,"article:published_time":"2020-03-22T05:42:22+00:00","article:modified_time":"2020-03-22T05:42:22+00:00","article:publisher":"https:\/\/www.facebook.com\/prohoster","article:author":"https:\/\/www.facebook.com\/prohoster"},"aioseo_meta_data":{"post_id":"74953","title":null,"description":null,"keywords":null,"keyphrases":null,"primary_term":null,"canonical_url":null,"og_title":null,"og_description":null,"og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":null,"og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"","isEnabled":true},"graphs":[]},"schema_type":null,"schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":null,"robots_max_videopreview":null,"robots_max_imagepreview":"large","priority":null,"frequency":null,"local_seo":null,"seo_analyzer_scan_date":null,"breadcrumb_settings":null,"limit_modified_date":false,"reviewed_by":null,"ai":null,"created":"2021-02-28 18:04:26","updated":"2022-09-30 13:25:20","focus_keyword":null,"additional_keywords":null,"truseo_locale":null},"gt_translate_keys":[{"key":"link","format":"url"}],"_links":{"self":[{"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/posts\/74953","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/comments?post=74953"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/posts\/74953\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/media\/74954"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/media?parent=74953"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/categories?post=74953"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/tags?post=74953"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}