{"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\/es\/blog\/administrirovanie\/dba-gramotno-organizovyvaem-sinhronizaczii-i-importy","title":{"rendered":"DBA: organizamos adecuadamente sincronizaciones e importaciones","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<p>En el procesamiento complejo de grandes conjuntos de datos (diferentes <noindex><a rel=\"nofollow\" href=\"https:\/\/ru.wikipedia.org\/wiki\/ETL\">procesos ETL<\/a><\/noindex>: importaciones, conversiones y sincronizaciones con fuentes externas) a menudo surge la necesidad <b>de \u00abrecordar\u00bb temporalmente y procesar r\u00e1pidamente<\/b> algo voluminoso.<\/p>\n<p>Una tarea t\u00edpica de este tipo suele plantearse de la siguiente manera: <i>\u00abAqu\u00ed la <noindex><a rel=\"nofollow\" href=\"https:\/\/sbis.ru\/accounting\">contabilidad ha exportado desde el banco de clientes<\/a><\/noindex> los \u00faltimos pagos recibidos, hay que cargarlos r\u00e1pidamente en el sitio y vincularlos a las cuentas\u00bb<\/i><\/p>\n<p>Pero cuando el volumen de esta \u00abcosa\u00bb comienza a medirse en cientos de megabytes, y el servicio debe seguir funcionando con la base de datos en modo 24\/7, aparecen muchos efectos secundarios que arruinar\u00e1n su vida.<br \/>\n<img decoding=\"async\" alt=\"DBA: organizamos adecuadamente sincronizaciones e importaciones\" src=\"\/wp-content\/uploads\/2020\/03\/f74afb2cd6f5f8de26a0932166933c95.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\nPara hacer frente a ellos en PostgreSQL (y no solo en \u00e9l), se pueden utilizar algunas funciones de optimizaci\u00f3n que permitir\u00e1n procesar todo m\u00e1s r\u00e1pido y con menos recursos.<br \/>\n<noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><\/p>\n<h2>1. \u00bfD\u00f3nde cargar?<\/h2>\n<p>\nPrimero, determinemos d\u00f3nde podemos volcar los datos que queremos \u00abprocesar\u00bb.<\/p>\n<h3>1.1. Tablas temporales (TEMPORARY TABLE)<\/h3>\n<p>\nEn principio, para PostgreSQL, las tablas temporales son iguales a cualquier otra tabla. Por lo tanto, son incorrectos los prejuicios como <i><b>\u00aball\u00ed todo se almacena solo en la memoria, y esta puede agotarse\u00bb<\/b><\/i>. Pero tambi\u00e9n hay algunas diferencias importantes.<\/p>\n<h4>Su propio \u00abespacio de nombres\u00bb para cada conexi\u00f3n a la BD<\/h4>\n<p>\nSi dos conexiones intentan ejecutar simult\u00e1neamente <code>CREATE TABLE x<\/code>, entonces alguien definitivamente obtendr\u00e1 <b>un error de no unicidad<\/b> de los objetos de la BD.<\/p>\n<p>Pero si ambos intentan ejecutar <code>CREAR <b>TEMPORARIO<\/b> TABLA x<\/code>, entonces ambos lo har\u00e1n correctamente, y cada uno obtendr\u00e1 <b>su propia instancia<\/b> de la tabla. Y no habr\u00e1 nada en com\u00fan entre ellas.<\/p>\n<h4>\u00abAutodestrucci\u00f3n\u00bb al desconectarse<\/h4>\n<p>\nAl cerrar la conexi\u00f3n, todas las tablas temporales se eliminan autom\u00e1ticamente, por lo que no tiene sentido ejecutar manualmente <code>DROP TABLE x<\/code> , salvo\u2026<\/p>\n<p>Si trabaja a trav\u00e9s de <b>pgbouncer en modo de transacci\u00f3n<\/b>, entonces la base todav\u00eda considera que esta conexi\u00f3n sigue activa, y en \u00e9l, esta tabla temporal sigue existiendo.<\/p>\n<p>Por lo tanto, intentar crearla de nuevo, ya desde otra conexi\u00f3n a pgbouncer, conducir\u00e1 a un error. Pero se puede eludir esto utilizando <code>CREAR TABLA TEMPORAL <b>SI NO EXISTE<\/b> x<\/code>.<\/p>\n<p>Sin embargo, es mejor no hacerlo, porque entonces puede \u00abdescubrir repentinamente\u00bb datos que quedan de \u00abel propietario anterior\u00bb. En su lugar, es mucho mejor leer el manual y ver que al crear la tabla hay una opci\u00f3n para a\u00f1adir <code>EN COMPROMISO <b>ELIMINAR<\/b><\/code> \u2014 es decir, al finalizar la transacci\u00f3n, la tabla ser\u00e1 eliminada autom\u00e1ticamente.<\/p>\n<h4>No-replicaci\u00f3n<\/h4>\n<p>\nDebido a que pertenece solo a una conexi\u00f3n espec\u00edfica, las tablas temporales no se replican. Sin embargo, <b>esto elimina la necesidad de escribir dos veces los datos<\/b> en heap + WAL, por lo que INSERT\/UPDATE\/DELETE en ella es significativamente m\u00e1s r\u00e1pido.<\/p>\n<p>Pero dado que la temporal es, al fin y al cabo, una tabla 'casi normal', no se puede crear en la r\u00e9plica tampoco. Al menos, por ahora, aunque ya hay un parche correspondiente circulando.<\/p>\n<h3>1.2. Tablas no registradas (UNLOGGED TABLE)<\/h3>\n<p>\nPero, \u00bfqu\u00e9 hacer si, por ejemplo, tiene un proceso ETL voluminoso que no se puede realizar dentro de una \u00fanica transacci\u00f3n, y usted sigue teniendo <b>pgbouncer en modo de transacci\u00f3n<\/b>?..<\/p>\n<p>O si el flujo de datos es tan grande que <b>la capacidad de un solo enlace a la base de datos no es suficiente (es decir, un proceso por CPU)?..<\/b> \u00bfO algunas de las operaciones se realizan<\/p>\n<p>de forma asincr\u00f3nica <b>en diferentes conexiones?..<\/b> Aqu\u00ed solo hay una opci\u00f3n \u2014<\/p>\n<p>crear temporalmente una tabla no temporal <b>. Un juego de palabras, s\u00ed. Es decir:<\/b>cre\u00f3 'sus' tablas con nombres lo m\u00e1s aleatorios posible para no cruzarse con nadie<\/p>\n<ul>\n<li>Extract<\/li>\n<li><b>: cargu\u00e9 en ellas los datos de una fuente externa<\/b>Transform<\/li>\n<li><b>: transform\u00e9, llen\u00e9 los campos de uni\u00f3n clave<\/b>: vert\u00ed los datos preparados en las tablas de destino<\/li>\n<li><b>Cargar<\/b>elimin\u00e9 las 'mis' tablas<\/li>\n<li>Y ahora \u2014 la cucharada de ceniza. De hecho,<\/li>\n<\/ul>\n<p>\ntoda la escritura en PostgreSQL ocurre dos veces <b>primero en WAL<\/b> \u2014 <noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/postgrespro\/blog\/461523\/\">, luego en los cuerpos de las tablas\/\u00edndices. Todo esto se hace para soportar ACID y garantizar la visibilidad correcta de los datos entre<\/a><\/noindex>'transacciones internas' y <code>COMMIT<\/code>\u2018incluidas y <code>ROLLBACK<\/code>\u2018incluidas transacciones.<\/p>\n<p>o se complet\u00f3 exitosamente o no. <b>. No importa cu\u00e1ntas transacciones intermedias haya; no nos interesa 'continuar el proceso desde la mitad', especialmente cuando no est\u00e1 claro d\u00f3nde estaba.<\/b>Para esto, los desarrolladores de PostgreSQL implementaron en la versi\u00f3n 9.1 algo como<\/p>\n<p>tablas no registradas (UNLOGGED) <noindex><a rel=\"nofollow\" href=\"https:\/\/postgrespro.ru\/docs\/postgresql\/12\/sql-createtable#SQL-CREATETABLE-UNLOGGED\">Con esta especificaci\u00f3n, la tabla se crea como no registrada. Los datos que se escriben en tablas no registradas no pasan por el registro de pre-escritura (ver Cap\u00edtulo 29), lo que hace que tales tablas<\/a><\/noindex>:<\/p>\n<blockquote><p>funcionen mucho m\u00e1s r\u00e1pido que las normales. <b>Sin embargo, no est\u00e1n protegidas contra fallos; en caso de fallo o apagado inesperado del servidor, la tabla no registrada<\/b>se corta autom\u00e1ticamente. <b>Adem\u00e1s, el contenido de la tabla no registrada<\/b>no se replica. <b>no se replica<\/b> en servidores esclavos. Cualquier \u00edndice creado para una tabla no registrada se convierte autom\u00e1ticamente en no registrado.<\/p><\/blockquote>\n<p>En resumen, <b>ser\u00e1 mucho m\u00e1s r\u00e1pido<\/b>, pero si el servidor DB \"cae\", ser\u00e1 un problema. Pero, \u00bftan a menudo sucede eso, y puede su proceso ETL ajustarse correctamente \"desde el medio\" despu\u00e9s de la \"resurrecci\u00f3n\" de la DB?..<\/p>\n<p>Si no es as\u00ed, y el caso anterior se parece al suyo, utilice <code>UNLOGGED<\/code>, pero nunca <b>active este atributo en tablas reales<\/b>, cuyos datos le importan.<\/p>\n<h3>1.3. ON COMMIT { DELETE ROWS | DROP }<\/h3>\n<p>\nEsta construcci\u00f3n permite al crear la tabla definir un comportamiento autom\u00e1tico al finalizar la transacci\u00f3n.<\/p>\n<p>Sobre <code>EN COMPROMISO <b>ELIMINAR<\/b><\/code> ya lo mencion\u00e9 arriba, genera <code>DROP TABLE<\/code>, pero con <code>EN COMPROMISO <b>ELIMINAR FILAS<\/b><\/code> la situaci\u00f3n es m\u00e1s interesante: aqu\u00ed se genera <code>TRUNCATE TABLE<\/code>.<\/p>\n<p>Dado que toda la infraestructura de almacenamiento de la meta descripci\u00f3n de la tabla temporal es exactamente la misma que la de la normal, <b>la creaci\u00f3n y eliminaci\u00f3n constante de tablas temporales lleva a un \"inflado\" significativo de las tablas del sistema<\/b> pg_class, pg_attribute, pg_attrdef, pg_depend,\u2026<\/p>\n<p>Ahora imagine que tiene un trabajador en una conexi\u00f3n directa con la DB, que cada segundo abre una nueva transacci\u00f3n, crea, llena, procesa y elimina una tabla temporal... Habr\u00e1 acumulaci\u00f3n de basura en las tablas del sistema, lo que generar\u00e1 retrasos en cada operaci\u00f3n.<\/p>\n<p>En resumen, \u00a1no haga eso! En este caso, es mucho m\u00e1s eficiente <code>CREATE TEMPORARY TABLE x ... ON COMMIT DELETE ROWS<\/code> sacar fuera del ciclo de transacciones: as\u00ed, al inicio de cada nueva transacci\u00f3n, la tabla ya <b>existir\u00e1<\/b> (ahorramos la llamada <code>CREAR<\/code>), pero <b>estar\u00e1 vac\u00eda<\/b>, gracias a <code>TRUNCATE<\/code> (tambi\u00e9n ahorramos su llamada) al finalizar la transacci\u00f3n anterior.<\/p>\n<h3>1.4. COMO\u2026 INCLUYENDO \u2026<\/h3>\n<p>\nMencion\u00e9 al principio que uno de los casos de uso t\u00edpicos para tablas temporales son diversos tipos de importaciones, y el desarrollador est\u00e1 cansado de copiar y pegar la lista de campos de la tabla de destino en la declaraci\u00f3n de su temporal...<\/p>\n<p>\u00a1Pero la pereza es el motor del progreso! Por eso <b>crear una nueva tabla \"a partir de un patr\u00f3n\"<\/b> se puede hacer mucho m\u00e1s f\u00e1cil:<\/p>\n<pre><code class=\"sql\">CREATE TEMPORARY TABLE import_table(\n  LIKE target_table\n);<\/code><\/pre>\n<p>\nDado que se pueden generar muchos datos en esta tabla, las b\u00fasquedas en ella no ser\u00e1n r\u00e1pidas en absoluto. Pero existe una soluci\u00f3n tradicional: \u00a1\u00edndices! Y, s\u00ed, <b>las tablas temporales tambi\u00e9n pueden tener \u00edndices.<\/b>.<\/p>\n<p>Dado que, a menudo, los \u00edndices necesarios coinciden con los \u00edndices de la tabla de destino, simplemente se puede escribir <code>COMO target_table <b>INCLUYENDO \u00cdNDICES<\/b><\/code>.<\/p>\n<p>Si tambi\u00e9n necesita <code>DEFAULT<\/code>-valores (por ejemplo, para completar los valores de la clave primaria), se puede utilizar <code>COMO target_table <b>INCLUYENDO LOS VALORES POR DEFECTO<\/b><\/code>. O simplemente \u2014 <code>COMO target_table <b>INCLUYENDO TODO<\/b><\/code> \u2014 copiar\u00e1 los valores predeterminados, \u00edndices, restricciones,\u2026<\/p>\n<p>Pero aqu\u00ed ya hay que entender que si has creado <b>la tabla de importaci\u00f3n directamente con \u00edndices, los datos tardar\u00e1n m\u00e1s en insertarse<\/b>, que si primero insertas todo y luego aplicas los \u00edndices; mira como lo hace <noindex><a rel=\"nofollow\" href=\"https:\/\/postgrespro.ru\/docs\/postgresql\/12\/app-pgdump\">pg_dump<\/a><\/noindex>.<\/p>\n<p>En general, <noindex><a rel=\"nofollow\" href=\"https:\/\/postgrespro.ru\/docs\/postgresql\/12\/sql-createtable\">RTFM<\/a><\/noindex>!<\/p>\n<h2>2. \u00bfC\u00f3mo escribir?<\/h2>\n<p>\nDir\u00e9 simplemente \u2014 utiliza <code><noindex><a rel=\"nofollow\" href=\"https:\/\/postgrespro.ru\/docs\/postgresql\/12\/sql-copy\">COPY<\/a><\/noindex><\/code>-flujo en lugar de \"lote\" <code>INSERTAR<\/code>, <noindex><a rel=\"nofollow\" href=\"https:\/\/www.citusdata.com\/blog\/2017\/11\/08\/faster-bulk-loading-in-postgresql-with-copy\/\">la aceleraci\u00f3n es m\u00faltiple<\/a><\/noindex>. Se puede incluso directamente desde un archivo previamente formado.<\/p>\n<h2>3. \u00bfC\u00f3mo procesar?<\/h2>\n<p>\nAs\u00ed que, supongamos que nuestra entrada se ve aproximadamente as\u00ed:<\/p>\n<ul>\n<li>tienes en la base una tabla con los datos de los clientes con <b>1M registros<\/b><\/li>\n<li>cada d\u00eda el cliente te env\u00eda un nuevo <b>\"conjunto completo\"<\/b><\/li>\n<li>por experiencia sabes que de vez en cuando <b>cambia no m\u00e1s de 10K registros<\/b><\/li>\n<\/ul>\n<p>\nUn ejemplo cl\u00e1sico de una situaci\u00f3n as\u00ed es <noindex><a rel=\"nofollow\" href=\"https:\/\/www.gnivc.ru\/technical_support\/classifiers_reference\/kladr\/\">la base de datos KADRK<\/a><\/noindex> \u2014 hay muchos direcciones, pero en cada volcado semanal de cambios (cambios de nombres de localidades, fusiones de calles, aparici\u00f3n de nuevos edificios) hay muy pocos incluso a escala nacional.<\/p>\n<h3>3.1. Algoritmo de sincronizaci\u00f3n completa<\/h3>\n<p>\nPara simplificar, supongamos que ni siquiera necesitas reestructurar los datos \u2014 solo llevar la tabla al formato adecuado, es decir:<\/p>\n<ul>\n<li><b>eliminar<\/b> todo lo que ya no existe<\/li>\n<li><b>Las opciones para procesadores \u00abElbrus\u00bb est\u00e1n disponibles mediante<\/b> todo lo que ya exist\u00eda y necesita ser actualizado<\/li>\n<li><b>insertar<\/b> todo lo que a\u00fan no exist\u00eda<\/li>\n<\/ul>\n<p>\n\u00bfPor qu\u00e9 exactamente en este orden hay que realizar las operaciones? Porque de esta forma el tama\u00f1o de la tabla crecer\u00e1 de manera m\u00ednima (<noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/tensor\/blog\/491366\/\">\u00a1recuerda el MVCC!<\/a><\/noindex>).<\/p>\n<h4>DELETE FROM dst<\/h4>\n<p>\nNo, por supuesto se puede hacer solo con dos operaciones:<\/p>\n<ul>\n<li><b>eliminar<\/b> (<code>ELIMINAR<\/code>) todo<\/li>\n<li><b>insertar<\/b> todo del nuevo conjunto<\/li>\n<\/ul>\n<p>\nPero al mismo tiempo, gracias al MVCC, <b>el tama\u00f1o de la tabla aumentar\u00e1 exactamente el doble<\/b>! Obtener +1M de registros en la tabla debido a la actualizaci\u00f3n de 10K \u2014 no es una sobrecarga deseable\u2026<\/p>\n<h4>TRUNCATE dst<\/h4>\n<p>\nUn desarrollador m\u00e1s experimentado sabe que se puede limpiar toda la tabla de manera bastante econ\u00f3mica:<\/p>\n<ul>\n<li><b>clear<\/b> (<code>TRUNCATE<\/code>) toda la tabla<\/li>\n<li><b>insertar<\/b> todo del nuevo conjunto<\/li>\n<\/ul>\n<p>\nEl m\u00e9todo es efectivo, <noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/tensor\/blog\/481866\/\">a veces es bastante aplicable<\/a><\/noindex>, pero hay un problema\u2026 Ingresar 1M de registros va a llevar muuuuucho tiempo, as\u00ed que no podemos permitirnos dejar la tabla vac\u00eda todo este tiempo (como suceder\u00e1 sin envolver en una transacci\u00f3n \u00fanica).<\/p>\n<p>As\u00ed que:<\/p>\n<ul>\n<li>comenzamos <b>una transacci\u00f3n larga<\/b><\/li>\n<li><code>TRUNCATE<\/code> impone <b>AccessExclusive<\/b>-bloqueo<\/li>\n<li>tardamos en hacer la inserci\u00f3n, y todos los dem\u00e1s en este tiempo <b>no pueden ni siquiera <code>SELECCIONAR<\/code><\/b><\/li>\n<\/ul>\n<p>\nAlgo est\u00e1 saliendo mal\u2026<\/p>\n<h4>ALTER TABLE\u2026 RENAME\u2026 \/ DROP TABLE \u2026<\/h4>\n<p>\nUna opci\u00f3n es cargar todo en una nueva tabla y luego simplemente renombrarla para reemplazar la antigua. Un par de detalles molestos:<\/p>\n<ul>\n<li>tambi\u00e9n <b>AccessExclusive<\/b>, aunque en un tiempo significativamente menor<\/li>\n<li>se restablecer\u00e1n todos los planes de consultas\/estad\u00edsticas de esta tabla, <noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/tensor\/blog\/479656\/\">hay que ejecutar ANALYZE<\/a><\/noindex><\/li>\n<li><b>se romper\u00e1n todas las claves externas<\/b> (FK) en la tabla<\/li>\n<\/ul>\n<p>\nHubo un parche WIP de Simon Riggs, que propon\u00eda hacer <code>ALTER<\/code>-una operaci\u00f3n para reemplazar el cuerpo de la tabla a nivel de archivo, sin tocar la estad\u00edstica y las FK, pero no logr\u00f3 suficiente apoyo.<\/p>\n<h4>DELETE, UPDATE, INSERT<\/h4>\n<p>\nAs\u00ed que nos quedamos con la opci\u00f3n no bloqueante de tres operaciones. Casi tres... \u00bfC\u00f3mo hacerlo de la manera m\u00e1s eficiente?<\/p>\n<pre><code class=\"sql\">-- hacemos todo en el marco de una transacci\u00f3n, para que nadie vea los \"estados intermedios\"\nBEGIN;\n\n-- creamos una tabla temporal con los datos importados\nCREATE TEMPORARY TABLE tmp(\n  LIKE dst INCLUDING INDEXES -- a imagen y semejanza, junto con los \u00edndices\n) ON COMMIT DROP; -- fuera de la transacci\u00f3n no la necesitamos\n\n-- r\u00e1pidamente inyectamos la nueva imagen a trav\u00e9s de COPY\nCOPY tmp FROM STDIN;\n-- ...\n-- .\n\n-- eliminamos los ausentes\nDELETE FROM\n  dst D\nUSING\n  dst X\nLEFT JOIN\n  tmp Y\n    USING(pk1, pk2) -- campos de la clave primaria\nWHERE\n  (D.pk1, D.pk2) = (X.pk1, X.pk2) AND\n  Y IS NOT DISTINCT FROM NULL; -- \"anti-join\"\n\n-- actualizamos los restantes\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); -- no hay necesidad de actualizar los coincidentes\n\n-- insertamos los ausentes\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>3.2. Postprocesamiento de la importaci\u00f3n<\/h3>\n<p>\nEn el mismo KLR, todos los registros modificados deben pasar por un postprocesamiento adicional: normalizar, extraer palabras clave, llevar a las estructuras necesarias. Pero, \u00bfc\u00f3mo saber \u2014 <b>qu\u00e9 exactamente se modific\u00f3<\/b>, sin complicar el c\u00f3digo de sincronizaci\u00f3n, idealmente, sin tocarlo en absoluto?<\/p>\n<p>Si el acceso de escritura en el momento de la sincronizaci\u00f3n solo est\u00e1 disponible para su proceso, se puede utilizar un trigger que recopile todos los cambios para nosotros:<\/p>\n<pre><code class=\"sql\">-- tablas objetivo\nCREATE TABLE kladr(...);\nCREATE TABLE kladr_house(...);\n\n-- tablas con historial de cambios\nCREATE TABLE kladr$log(\n  ro kladr, -- aqu\u00ed est\u00e1n las im\u00e1genes completas de los registros antiguos\/nuevos\n  rn kladr\n);\n\nCREATE TABLE kladr_house$log(\n  ro kladr_house,\n  rn kladr_house\n);\n\n-- funci\u00f3n general para registrar cambios\nCREATE OR REPLACE FUNCTION diff$log() RETURNS trigger AS $$\nDECLARE\n  dst varchar = TG_TABLE_NAME || '$log';\n  stmt text = '';\nBEGIN\n  -- verificar la necesidad de registrar al actualizar un registro\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  -- crear registro del log\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>\nAhora podemos aplicar (o activar mediante <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>\nLuego, extraemos tranquilamente todos los cambios que necesitamos de las tablas de log y los procesamos con manejadores adicionales.<\/p>\n<h3>3.3. Importaci\u00f3n de conjuntos relacionados<\/h3>\n<p>\nAnteriormente, discutimos los casos en los que las estructuras de datos de la fuente y el receptor coinciden. Pero, \u00bfqu\u00e9 hacer si la descarga desde un sistema externo tiene un formato diferente de la estructura de almacenamiento en nuestra base?<\/p>\n<p>Tomemos como ejemplo el almacenamiento de clientes y sus facturas, un caso cl\u00e1sico de \"muchos a uno\":<\/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>\nY esta es la extracci\u00f3n de una fuente externa presentada como \"todo en uno\":<\/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>\nEs obvio que los datos de los clientes pueden duplicarse en este formato, y el registro principal es \"la factura\":<\/p>\n<pre><code class=\"plaintext\">0123456789;Vasya;A-01;2020-03-16;1000.00\n9876543210;Petya;A-02;2020-03-16;666.00\n0123456789;Vasya;B-03;2020-03-16;9999.00\n<\/code><\/pre>\n<p>\nPara el modelo simplemente insertaremos nuestros datos de prueba, pero recordemos \u2014 <code>COPY<\/code> \u00a1m\u00e1s eficiente!<\/p>\n<pre><code class=\"sql\">INSERT INTO invoice_import\nVALUES\n  ('0123456789', 'Vasya', 'A-01', '2020-03-16', 1000.00)\n, ('9876543210', 'Petya', 'A-02', '2020-03-16', 666.00)\n, ('0123456789', 'Vasya', 'B-03', '2020-03-16', 9999.00);<\/code><\/pre>\n<p>\nPrimero identificaremos esos \"segmentos\" a los que nuestros \"hechos\" hacen referencia. En nuestro caso, las facturas hacen referencia a los clientes:<\/p>\n<pre><code class=\"sql\">CREATE TEMPORARY TABLE client_import AS\nSELECT DISTINCT ON(client_inn)\n-- se puede hacer simplemente un SELECT DISTINCT, si los datos son inherentemente consistentes\n  client_inn inn\n, client_name \"nombre\"\nFROM\n  invoice_import;<\/code><\/pre>\n<p>\nPara vincular correctamente las facturas con los ID de los clientes, primero necesitamos conocer o generar esos identificadores. Agregaremos campos para ellos:<\/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>\nUsaremos el m\u00e9todo de sincronizaci\u00f3n de tablas descrito anteriormente con una peque\u00f1a modificaci\u00f3n: no actualizaremos ni eliminaremos nada en la tabla de destino, ya que la importaci\u00f3n de clientes es \u00abappend-only\u00bb:<\/p>\n<pre><code class=\"sql\">-- asignamos en la tabla de importaci\u00f3n ID de registros ya existentes\nUPDATE\n  client_import T\nSET\n  client_id = D.client_id\nFROM\n  client D\nWHERE\n  T.inn = D.inn; -- clave \u00fanica\n\n-- insertamos registros faltantes y asignamos sus ID\nWITH ins AS (\n  INSERT INTO client(\n    inn\n  , name\n  )\n  SELECT\n    inn\n  , name\n  FROM\n    client_import\n  WHERE\n    client_id IS NULL -- si no se asign\u00f3 el ID\n  RETURNING *\n)\nUPDATE\n  client_import T\nSET\n  client_id = D.client_id\nFROM\n  ins D\nWHERE\n  T.inn = D.inn; -- clave \u00fanica\n\n-- asignamos ID de clientes a los registros de facturas\nUPDATE\n  invoice_import T\nSET\n  client_id = D.client_id\nFROM\n  client_import D\nWHERE\n  T.client_inn = D.inn; -- clave aplicativa\n<\/code><\/pre>\n<p>\nEso es todo; en <code>invoice_import<\/code> ahora tenemos el campo de relaci\u00f3n lleno <code>client_id<\/code>, con el que insertaremos la factura.<br \/>\n<br \/>Fuente: <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.2 - 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\/es\/blog\/administrirovanie\/dba-gramotno-organizovyvaem-sinhronizaczii-i-importy\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.2\" \/>\n\t\t<meta property=\"og:locale\" content=\"es_ES\" \/>\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\/es\/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: organizamos eficientemente sincronizaciones e importaciones | ProHoster","description":"Con el procesamiento complejo de grandes conjuntos de datos (diferentes procesos ETL: importaciones, conversiones y sincronizaciones con fuentes externas) a menudo.","canonical_url":"https:\/\/prohoster.info\/es\/blog\/administrirovanie\/dba-gramotno-organizovyvaem-sinhronizaczii-i-importy","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":null,"og:locale":"es_ES","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\/es\/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\/es\/wp-json\/wp\/v2\/posts\/74953","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/comments?post=74953"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/posts\/74953\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/media\/74954"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/media?parent=74953"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/categories?post=74953"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/tags?post=74953"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}