{"id":6543,"date":"2026-10-07T21:28:13","date_gmt":"2026-10-07T19:28:13","guid":{"rendered":"https:\/\/blog-david.moreno-morales.com\/?p=6543"},"modified":"2026-10-07T21:28:15","modified_gmt":"2026-10-07T19:28:15","slug":"bases-de-datos-temporales","status":"publish","type":"post","link":"https:\/\/blog-david.moreno-morales.com\/index.php\/2026\/10\/07\/bases-de-datos-temporales\/","title":{"rendered":"Bases de datos temporales: c\u00f3mo modelar el pasado, el presente y la vigencia sin perder informaci\u00f3n"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-post\" data-elementor-id=\"6543\" class=\"elementor elementor-6543\">\n\t\t\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-1f790ae elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"1f790ae\" data-element_type=\"section\" data-e-type=\"section\" data-settings=\"{&quot;tc_dark_mode_responsive_hide_in_dark&quot;:&quot;no&quot;,&quot;tc_dark_mode_responsive_hide_in_light&quot;:&quot;no&quot;}\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-858b1f5\" data-id=\"858b1f5\" data-element_type=\"column\" data-e-type=\"column\" data-settings=\"{&quot;tc_dark_mode_responsive_hide_in_dark&quot;:&quot;no&quot;,&quot;tc_dark_mode_responsive_hide_in_light&quot;:&quot;no&quot;}\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<section class=\"elementor-section elementor-inner-section elementor-element elementor-element-61e0e1f elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"61e0e1f\" data-element_type=\"section\" data-e-type=\"section\" data-settings=\"{&quot;tc_dark_mode_responsive_hide_in_dark&quot;:&quot;no&quot;,&quot;tc_dark_mode_responsive_hide_in_light&quot;:&quot;no&quot;}\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-inner-column elementor-element elementor-element-1cc40bf\" data-id=\"1cc40bf\" data-element_type=\"column\" data-e-type=\"column\" data-settings=\"{&quot;tc_dark_mode_responsive_hide_in_dark&quot;:&quot;no&quot;,&quot;tc_dark_mode_responsive_hide_in_light&quot;:&quot;no&quot;}\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-085053d elementor-widget elementor-widget-text-editor\" data-id=\"085053d\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;tc_dark_mode_responsive_hide_in_dark&quot;:&quot;no&quot;,&quot;tc_dark_mode_responsive_hide_in_light&quot;:&quot;no&quot;}\" data-widget_type=\"text-editor.default\">\n\t\t\t\t\t\t\t\t\t<p data-pm-slice=\"1 1 []\">La mayor\u00eda de las bases de datos responden muy bien a una pregunta: \u00bfcu\u00e1l es el valor actual?<\/p><p>El problema empieza cuando necesitamos preguntar algo diferente:<\/p><ul data-spread=\"false\"><li><p>\u00bfQu\u00e9 precio ten\u00eda este producto el 15 de marzo?<\/p><\/li><li><p>\u00bfQu\u00e9 direcci\u00f3n constaba para este cliente cuando se emiti\u00f3 una factura?<\/p><\/li><li><p>\u00bfDesde cu\u00e1ndo estaba vigente una determinada tarifa?<\/p><\/li><li><p>\u00bfQu\u00e9 datos conoc\u00eda realmente el sistema antes de que alguien corrigiera un error?<\/p><\/li><li><p>\u00bfExistieron dos contratos incompatibles vigentes al mismo tiempo?<\/p><\/li><\/ul><p>En una tabla convencional, un <code dir=\"ltr\">UPDATE<\/code> sustituye un valor por otro. Si ayer un precio era 100 y hoy pasa a 120, normalmente la fila termina conteniendo \u00fanicamente 120. El presente queda perfectamente representado, el pasado desaparece.<\/p><p>Ah\u00ed entra en juego el modelado temporal. Una base de datos temporal no se limita a almacenar hechos, tambi\u00e9n representa cu\u00e1ndo son o fueron v\u00e1lidos esos hechos y, en algunos modelos, cu\u00e1ndo llegaron a conocimiento del sistema.<\/p><p>Esa distinci\u00f3n parece peque\u00f1a, pero cambia profundamente la forma de dise\u00f1ar tablas, claves, relaciones, auditor\u00edas y consultas hist\u00f3ricas.<\/p><h2>Dos preguntas distintas sobre el tiempo<\/h2><p>Antes de crear columnas <code dir=\"ltr\">fecha_desde<\/code> y <code dir=\"ltr\">fecha_hasta<\/code>, conviene aclarar qu\u00e9 significa exactamente el tiempo dentro del modelo.<\/p><p>Hay dos dimensiones especialmente importantes.<\/p><h3>1. Tiempo de validez o tiempo de negocio<\/h3><p>Representa cu\u00e1ndo es cierto un dato en el mundo que estamos modelando.<\/p><p>Imaginemos una tarifa:<\/p><table><tbody><tr><th>tarifa<\/th><th>precio<\/th><th>v\u00e1lida desde<\/th><th>v\u00e1lida hasta<\/th><\/tr><tr><td>Premium<\/td><td>49 \u20ac<\/td><td>2026-01-01<\/td><td>2026-04-01<\/td><\/tr><tr><td>Premium<\/td><td>55 \u20ac<\/td><td>2026-04-01<\/td><td>2026-09-01<\/td><\/tr><tr><td>Premium<\/td><td>59 \u20ac<\/td><td>2026-09-01<\/td><td>\u221e<\/td><\/tr><\/tbody><\/table><p>La pregunta que resuelve esta dimensi\u00f3n es: \u00bfQu\u00e9 valor era v\u00e1lido en una fecha determinada?<\/p><p>No importa cu\u00e1ndo se introdujo la informaci\u00f3n en la base de datos, importa cu\u00e1ndo deb\u00eda aplicarse desde el punto de vista del negocio.<\/p><h3>2. Tiempo del sistema o tiempo de transacci\u00f3n<\/h3><p>Representa durante qu\u00e9 periodo una determinada versi\u00f3n estuvo registrada en la base de datos.<\/p><p>Supongamos que el 10 de octubre descubrimos que una tarifa que hab\u00edamos registrado con fecha de inicio 1 de septiembre deb\u00eda haber comenzado realmente el 15 de agosto. Desde la perspectiva del negocio, corregimos la fecha de vigencia, pero desde la perspectiva de auditor\u00eda tambi\u00e9n puede interesarnos saber que:<\/p><ul data-spread=\"false\"><li><p>hasta el 10 de octubre el sistema almacenaba una versi\u00f3n;<\/p><\/li><li><p>a partir del 10 de octubre almacen\u00f3 la versi\u00f3n corregida.<\/p><\/li><\/ul><p>La pregunta pasa a ser: \u00bfQu\u00e9 sab\u00eda el sistema en un momento concreto?<\/p><p>SQL Server denomina a este enfoque system-versioned temporal tables y mantiene autom\u00e1ticamente las versiones anteriores de las filas en una tabla hist\u00f3rica.<\/p><h2>Cuando necesitamos ambas dimensiones: modelos bitemporales<\/h2><p>Los problemas m\u00e1s interesantes aparecen cuando necesitamos conservar simult\u00e1neamente:<\/p><ul data-spread=\"false\"><li><p>cu\u00e1ndo era v\u00e1lido un dato en negocio;<\/p><\/li><li><p>cu\u00e1ndo fue conocido o registrado por el sistema.<\/p><\/li><\/ul><p>Eso produce un modelo bitemporal.<\/p><p>Pensemos en una p\u00f3liza de seguro, la compa\u00f1\u00eda registra el 5 de mayo que una cobertura comienza el 1 de mayo. El 20 de mayo recibe documentaci\u00f3n que demuestra que la cobertura realmente comenz\u00f3 el 28 de abril. La nueva informaci\u00f3n cambia el periodo v\u00e1lido de negocio, pero no deber\u00eda borrar el hecho de que, entre el 5 y el 20 de mayo, el sistema cre\u00eda otra cosa.<\/p><p>Con dos dimensiones temporales podemos responder tanto: \u00bfQu\u00e9 cobertura correspond\u00eda al cliente el 30 de abril?<\/p><p>como: \u00bfQu\u00e9 pens\u00e1bamos el 10 de mayo que correspond\u00eda al cliente el 30 de abril?<\/p><p>Son consultas completamente distintas. Y esa diferencia es fundamental en \u00e1mbitos como finanzas, seguros, facturaci\u00f3n, recursos humanos, contratos, inventario, regulaci\u00f3n o sistemas donde una correcci\u00f3n retrospectiva no deber\u00eda reescribir silenciosamente la historia.<\/p><h2>El patr\u00f3n cl\u00e1sico: intervalos de vigencia<\/h2><p>Cuando el motor no gestiona autom\u00e1ticamente el tiempo de negocio, un patr\u00f3n habitual consiste en representar cada versi\u00f3n mediante un intervalo.<\/p><p>Por ejemplo:<\/p><pre><code class=\"language-sql\">CREATE TABLE precio_producto (\n    producto_id   BIGINT        NOT NULL,\n    precio        DECIMAL(10,2) NOT NULL,\n    valido_desde  TIMESTAMP     NOT NULL,\n    valido_hasta  TIMESTAMP,\n    PRIMARY KEY (producto_id, valido_desde)\n);<\/code><\/pre><p>Cada fila deja de representar simplemente \u00abel precio del producto\u00bb.<\/p><p>Representa: \u00abEste precio fue v\u00e1lido para este producto durante este intervalo.\u00bb<\/p><p>La diferencia conceptual es importante.<\/p><h2>\u00bfIntervalos cerrados o semiabiertos?<\/h2><p>Una de las decisiones m\u00e1s sencillas y m\u00e1s importantes del dise\u00f1o temporal consiste en definir qu\u00e9 significan los extremos.<\/p><p>Una convenci\u00f3n particularmente \u00fatil es utilizar intervalos semiabiertos:<\/p><pre><code class=\"language-text\">[desde, hasta)<\/code><\/pre><p>Es decir:<\/p><ul data-spread=\"false\"><li><p><code dir=\"ltr\">desde<\/code> est\u00e1 incluido;<\/p><\/li><li><p><code dir=\"ltr\">hasta<\/code> est\u00e1 excluido.<\/p><\/li><\/ul><p>As\u00ed podemos representar dos periodos consecutivos:<\/p><pre><code class=\"language-text\">[2026-01-01, 2026-04-01)\n[2026-04-01, 2026-09-01)<\/code><\/pre><p>sin crear ambig\u00fcedad sobre cu\u00e1l de las dos versiones est\u00e1 vigente exactamente el 1 de abril.<\/p><p>Una consulta conceptual ser\u00eda:<\/p><pre><code class=\"language-sql\">SELECT *\nFROM precio_producto\nWHERE producto_id = 42\n  AND valido_desde &lt;= TIMESTAMP '2026-04-15 12:00:00'\n  AND (\n        valido_hasta &gt; TIMESTAMP '2026-04-15 12:00:00'\n        OR valido_hasta IS NULL\n      );<\/code><\/pre><p>La regla importante no es \u00fanicamente escribir correctamente esta consulta, es utilizar la misma sem\u00e1ntica temporal en todo el sistema.<\/p><h2>El problema que dos columnas no solucionan: los solapamientos<\/h2><p>A\u00f1adir <code dir=\"ltr\">valido_desde<\/code> y <code dir=\"ltr\">valido_hasta<\/code> no convierte m\u00e1gicamente una tabla en un modelo temporal correcto.<\/p><p>Imaginemos estas filas:<\/p><pre><code class=\"language-text\">Producto 42 \u2014 100 \u20ac \u2014 [01 enero, 01 junio)\nProducto 42 \u2014 120 \u20ac \u2014 [01 mayo, 01 septiembre)<\/code><\/pre><p>Durante mayo existen dos precios simult\u00e1neamente. Si el dominio permite solamente un precio por producto, el modelo ha violado una regla de negocio.<\/p><p>La aplicaci\u00f3n puede intentar evitarlo, pero cuanto m\u00e1s cr\u00edtica sea la integridad, m\u00e1s interesante resulta que sea la propia base de datos la que impida estados imposibles.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<section class=\"elementor-section elementor-inner-section elementor-element elementor-element-26a56fa elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"26a56fa\" data-element_type=\"section\" data-e-type=\"section\" data-settings=\"{&quot;tc_dark_mode_responsive_hide_in_dark&quot;:&quot;no&quot;,&quot;tc_dark_mode_responsive_hide_in_light&quot;:&quot;no&quot;}\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-50 elementor-inner-column elementor-element elementor-element-bf5ce92\" data-id=\"bf5ce92\" data-element_type=\"column\" data-e-type=\"column\" data-settings=\"{&quot;tc_dark_mode_responsive_hide_in_dark&quot;:&quot;no&quot;,&quot;tc_dark_mode_responsive_hide_in_light&quot;:&quot;no&quot;}\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-2c79896 elementor-widget elementor-widget-image\" data-id=\"2c79896\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;tc_dark_mode_responsive_hide_in_dark&quot;:&quot;no&quot;,&quot;tc_dark_mode_responsive_hide_in_light&quot;:&quot;no&quot;}\" data-widget_type=\"image.default\">\n\t\t\t\t\t\t\t\t\t\t\t\t<figure class=\"wp-caption\">\n\t\t\t\t\t\t\t\t\t\t<img fetchpriority=\"high\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/blog-david.moreno-morales.com\/wp-content\/uploads\/2026\/10\/bbddtemp_002-1024x576.png\" class=\"attachment-large size-large wp-image-6545\" alt=\"Valid time vs. system time\" \/>\t\t\t\t\t\t\t\t\t\t\t<figcaption class=\"widget-image-caption wp-caption-text\">Valid time vs. system time<\/figcaption>\n\t\t\t\t\t\t\t\t\t\t<\/figure>\n\t\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t<div class=\"elementor-column elementor-col-50 elementor-inner-column elementor-element elementor-element-5d576d9\" data-id=\"5d576d9\" data-element_type=\"column\" data-e-type=\"column\" data-settings=\"{&quot;tc_dark_mode_responsive_hide_in_dark&quot;:&quot;no&quot;,&quot;tc_dark_mode_responsive_hide_in_light&quot;:&quot;no&quot;}\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-d7256b4 elementor-widget elementor-widget-image\" data-id=\"d7256b4\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;tc_dark_mode_responsive_hide_in_dark&quot;:&quot;no&quot;,&quot;tc_dark_mode_responsive_hide_in_light&quot;:&quot;no&quot;}\" data-widget_type=\"image.default\">\n\t\t\t\t\t\t\t\t\t\t\t\t<figure class=\"wp-caption\">\n\t\t\t\t\t\t\t\t\t\t<img decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/blog-david.moreno-morales.com\/wp-content\/uploads\/2026\/10\/bbddtemp_003-1024x576.png\" class=\"attachment-large size-large wp-image-6544\" alt=\"C\u00f3mo evitar periodos solapados\" \/>\t\t\t\t\t\t\t\t\t\t\t<figcaption class=\"widget-image-caption wp-caption-text\">C\u00f3mo evitar periodos solapados<\/figcaption>\n\t\t\t\t\t\t\t\t\t\t<\/figure>\n\t\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<section class=\"elementor-section elementor-inner-section elementor-element elementor-element-e8ae2ac elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"e8ae2ac\" data-element_type=\"section\" data-e-type=\"section\" data-settings=\"{&quot;tc_dark_mode_responsive_hide_in_dark&quot;:&quot;no&quot;,&quot;tc_dark_mode_responsive_hide_in_light&quot;:&quot;no&quot;}\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-inner-column elementor-element elementor-element-7c91588\" data-id=\"7c91588\" data-element_type=\"column\" data-e-type=\"column\" data-settings=\"{&quot;tc_dark_mode_responsive_hide_in_dark&quot;:&quot;no&quot;,&quot;tc_dark_mode_responsive_hide_in_light&quot;:&quot;no&quot;}\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-af8d6c7 elementor-widget elementor-widget-text-editor\" data-id=\"af8d6c7\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;tc_dark_mode_responsive_hide_in_dark&quot;:&quot;no&quot;,&quot;tc_dark_mode_responsive_hide_in_light&quot;:&quot;no&quot;}\" data-widget_type=\"text-editor.default\">\n\t\t\t\t\t\t\t\t\t<h2 data-pm-slice=\"1 1 []\">PostgreSQL 18 y las restricciones temporales<\/h2><p>Aqu\u00ed aparece una evoluci\u00f3n especialmente relevante. PostgreSQL 18 incorpor\u00f3 soporte para restricciones temporales mediante <code dir=\"ltr\">WITHOUT OVERLAPS<\/code> en claves primarias y restricciones <code dir=\"ltr\">UNIQUE<\/code>, adem\u00e1s de claves for\u00e1neas temporales mediante <code dir=\"ltr\">PERIOD<\/code>.<\/p><p>Podemos modelar un periodo utilizando un rango:<\/p><pre><code class=\"language-sql\">CREATE TABLE precio_producto (\n    producto_id BIGINT NOT NULL,\n    precio      NUMERIC(10,2) NOT NULL,\n    vigencia    tstzrange NOT NULL,\n    PRIMARY KEY (producto_id, vigencia WITHOUT OVERLAPS)\n);<\/code><\/pre><p>El objetivo de <code dir=\"ltr\">WITHOUT OVERLAPS<\/code> es precisamente impedir que dos filas con la misma parte no temporal de la clave tengan periodos que se solapen.<\/p><p>Conceptualmente:<\/p><pre><code class=\"language-text\">Producto 42 + [enero, abril)   \u2713\nProducto 42 + [abril, junio)   \u2713\nProducto 42 + [marzo, mayo)    \u2717<\/code><\/pre><p>PostgreSQL implementa esta sem\u00e1ntica aprovechando sus tipos <code dir=\"ltr\">range<\/code> y mecanismos de exclusi\u00f3n. La documentaci\u00f3n actual explica que la \u00faltima columna de una clave con <code dir=\"ltr\">WITHOUT OVERLAPS<\/code> debe ser un rango o multirango.<\/p><p>Esto tiene una consecuencia arquitect\u00f3nica interesante: la integridad temporal puede dejar de ser exclusivamente responsabilidad de la aplicaci\u00f3n.<\/p><h2>Relaciones que tambi\u00e9n dependen del tiempo<\/h2><p>Evitar solapamientos dentro de una tabla es solamente la mitad del problema.<\/p><p>Supongamos que tenemos contratos asociados a una tarifa, no basta con comprobar que la tarifa existe, necesitamos garantizar que existe durante todo el periodo del contrato.<\/p><p>PostgreSQL 18 permite utilizar <code dir=\"ltr\">PERIOD<\/code> en determinadas claves for\u00e1neas temporales. La referencia se considera v\u00e1lida cuando las filas correspondientes de la tabla referenciada cubren conjuntamente todo el periodo que queremos referenciar.<\/p><p>Esta idea resuelve un problema que tradicionalmente exig\u00eda triggers, l\u00f3gica de aplicaci\u00f3n o consultas adicionales: no solo necesito saber si la entidad referenciada existe, sino si existe durante el intervalo necesario.<\/p><p>En sistemas complejos, esta diferencia resulta enorme.<\/p><h2>SQL Server: versionado autom\u00e1tico del historial<\/h2><p>SQL Server aborda otra dimensi\u00f3n del problema temporal de forma especialmente integrada. Una tabla temporal versionada por el sistema utiliza dos columnas de periodo y una tabla hist\u00f3rica asociada.<\/p><p>Un ejemplo simplificado ser\u00eda:<\/p><pre><code class=\"language-sql\">CREATE TABLE dbo.Cliente\n(\n    ClienteId INT NOT NULL PRIMARY KEY,\n    Nombre NVARCHAR(200) NOT NULL,\n    Ciudad NVARCHAR(100),\n    ValidFrom DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL,\n    ValidTo DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL,\n    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)\n)\nWITH (\n    SYSTEM_VERSIONING = ON\n);<\/code><\/pre><p>A partir de ah\u00ed, las operaciones normales de modificaci\u00f3n contin\u00faan realiz\u00e1ndose sobre la tabla actual. Cuando una fila cambia o se elimina, SQL Server conserva autom\u00e1ticamente la versi\u00f3n anterior en una tabla hist\u00f3rica asociada.<\/p><p>Eso evita tener que implementar manualmente gran parte de la infraestructura de auditor\u00eda hist\u00f3rica.<\/p><h2>Consultar el pasado con <code dir=\"ltr\">FOR SYSTEM_TIME<\/code><\/h2><p>Una de las ventajas m\u00e1s visibles aparece al reconstruir estados hist\u00f3ricos.<\/p><p>Por ejemplo:<\/p><pre><code class=\"language-sql\">SELECT *\nFROM dbo.Cliente\nFOR SYSTEM_TIME AS OF '2026-03-01T10:00:00'\nWHERE ClienteId = 42;<\/code><\/pre><p>La consulta pide: mu\u00e9strame la versi\u00f3n del cliente 42 que el sistema consideraba vigente en ese instante.<\/p><p>SQL Server dispone adem\u00e1s de variantes para consultar intervalos o todo el historial mediante opciones como <code dir=\"ltr\">FROM ... TO<\/code>, <code dir=\"ltr\">BETWEEN<\/code>, <code dir=\"ltr\">CONTAINED IN<\/code> y <code dir=\"ltr\">ALL<\/code>.<\/p><p>Para an\u00e1lisis forense, auditor\u00eda y reconstrucci\u00f3n de estados, esa capacidad simplifica much\u00edsimo consultas que de otro modo tendr\u00edan que combinar manualmente tablas actuales e hist\u00f3ricas.<\/p><h2>MariaDB: tiempo del sistema y tiempo de aplicaci\u00f3n<\/h2><p>MariaDB tambi\u00e9n ofrece capacidades temporales que permiten diferenciar entre versionado del sistema y periodos definidos desde la l\u00f3gica de negocio.<\/p><p>Los periodos de aplicaci\u00f3n permiten definir columnas de inicio y fin mediante <code dir=\"ltr\">PERIOD<\/code>, mientras cl\u00e1usulas como <code dir=\"ltr\">FOR PORTION OF<\/code> permiten operar sobre partes concretas del periodo.<\/p><p>Un esquema conceptual ser\u00eda:<\/p><pre><code class=\"language-sql\">CREATE TABLE contrato (\n    contrato_id INT,\n    descripcion VARCHAR(200),\n    valido_desde DATE,\n    valido_hasta DATE,\n    PERIOD FOR vigencia (valido_desde, valido_hasta)\n);<\/code><\/pre><p>La diferencia frente a guardar dos fechas arbitrarias est\u00e1 en que el motor empieza a comprender que esas columnas forman conjuntamente un periodo temporal con sem\u00e1ntica propia.<\/p><h2>Oracle y Flashback<\/h2><p>Oracle aborda parte del problema desde sus tecnolog\u00edas Flashback.<\/p><p>Flashback Data Archive permite conservar autom\u00e1ticamente datos hist\u00f3ricos y asociar pol\u00edticas de retenci\u00f3n, de forma que versiones anteriores puedan consultarse mediante mecanismos de Flashback Query.<\/p><p>De nuevo aparece una idea recurrente: el historial no deber\u00eda considerarse simplemente una copia secundaria de datos antiguos. Puede convertirse en una dimensi\u00f3n consultable del modelo.<\/p><h2>Temporalidad no es lo mismo que auditor\u00eda<\/h2><p>Es f\u00e1cil confundir varios conceptos. Una tabla de auditor\u00eda podr\u00eda registrar algo parecido a:<\/p><pre><code class=\"language-text\">usuario: david\nacci\u00f3n: UPDATE\ntabla: cliente\nfecha: 2026-10-05 17:20<\/code><\/pre><p>Eso puede ser suficiente para saber qui\u00e9n hizo qu\u00e9. Pero no necesariamente permite reconstruir correctamente:<\/p><ul data-spread=\"false\"><li><p>el estado completo de una entidad;<\/p><\/li><li><p>su periodo de validez;<\/p><\/li><li><p>todas sus relaciones hist\u00f3ricas;<\/p><\/li><li><p>qu\u00e9 datos estaban vigentes en un instante.<\/p><\/li><\/ul><p>La auditor\u00eda responde principalmente a preguntas sobre acciones, el modelado temporal responde a preguntas sobre estados y periodos. Pueden complementarse, pero no son equivalentes.<\/p><h2>Tampoco es lo mismo que un Slowly Changing Dimension<\/h2><p>En Business Intelligence es habitual utilizar dimensiones lentamente cambiantes, especialmente el patr\u00f3n SCD Tipo 2.<\/p><p>Por ejemplo:<\/p><pre><code class=\"language-text\">cliente_id | ciudad     | desde      | hasta\n-----------|------------|------------|------------\n10         | Madrid     | 2024-01-01 | 2025-06-10\n10         | Pamplona   | 2025-06-10 | NULL<\/code><\/pre><p>El patr\u00f3n tiene mucho en com\u00fan con el modelado temporal porque conserva diferentes versiones de una entidad. Pero una arquitectura anal\u00edtica y un modelo operacional tienen objetivos distintos.<\/p><p>En un data warehouse solemos priorizar:<\/p><ul data-spread=\"false\"><li><p>an\u00e1lisis hist\u00f3rico;<\/p><\/li><li><p>cargas ETL\/ELT;<\/p><\/li><li><p>dimensiones y hechos;<\/p><\/li><li><p>reproducibilidad de informes.<\/p><\/li><\/ul><p>En sistemas transaccionales necesitamos adem\u00e1s pensar en:<\/p><ul data-spread=\"false\"><li><p>concurrencia;<\/p><\/li><li><p>integridad;<\/p><\/li><li><p>modificaciones retroactivas;<\/p><\/li><li><p>relaciones temporales;<\/p><\/li><li><p>reglas de negocio;<\/p><\/li><li><p>coste de almacenamiento;<\/p><\/li><li><p>pol\u00edticas de retenci\u00f3n.<\/p><\/li><\/ul><p>Por eso copiar mec\u00e1nicamente un patr\u00f3n SCD a una aplicaci\u00f3n operacional no siempre es suficiente.<\/p><h2>La pregunta inc\u00f3moda: \u00bfcu\u00e1nto historial vamos a conservar?<\/h2><p>Guardar la historia tiene un coste. En SQL Server, cada modificaci\u00f3n o eliminaci\u00f3n de una fila versionada puede generar datos hist\u00f3ricos. Microsoft recomienda definir expl\u00edcitamente una estrategia de retenci\u00f3n porque tablas hist\u00f3ricas con mucha actividad y largos periodos de conservaci\u00f3n pueden crecer de forma importante y terminar afectando tanto al almacenamiento como al rendimiento de consultas temporales.<\/p><p>Antes de habilitar temporalidad conviene responder:<\/p><ol start=\"1\" data-spread=\"false\"><li><p>\u00bfDurante cu\u00e1nto tiempo necesitamos consultar el hist\u00f3rico online?<\/p><\/li><li><p>\u00bfExiste una obligaci\u00f3n legal de conservarlo?<\/p><\/li><li><p>\u00bfPuede archivarse parte del historial?<\/p><\/li><li><p>\u00bfQu\u00e9 frecuencia de actualizaci\u00f3n tendr\u00e1 la tabla?<\/p><\/li><li><p>\u00bfQu\u00e9 \u00edndices necesitan las consultas hist\u00f3ricas?<\/p><\/li><li><p>\u00bfCu\u00e1nto crecer\u00e1 la tabla en uno, tres y cinco a\u00f1os?<\/p><\/li><\/ol><p>Una pol\u00edtica temporal sin pol\u00edtica de retenci\u00f3n suele convertirse tarde o temprano en un problema de operaciones.<\/p><h2>No todos los datos necesitan temporalidad<\/h2><p>Guardar historia porque \u00abpuede resultar \u00fatil\u00bb no siempre justifica la complejidad. La temporalidad tiene especial sentido cuando el pasado tiene valor operativo, contractual, anal\u00edtico o regulatorio.<\/p><p>Ejemplos claros:<\/p><ul data-spread=\"false\"><li><p>precios;<\/p><\/li><li><p>contratos;<\/p><\/li><li><p>p\u00f3lizas;<\/p><\/li><li><p>salarios;<\/p><\/li><li><p>jerarqu\u00edas organizativas;<\/p><\/li><li><p>permisos;<\/p><\/li><li><p>configuraciones;<\/p><\/li><li><p>datos maestros;<\/p><\/li><li><p>asignaciones;<\/p><\/li><li><p>estados regulatorios;<\/p><\/li><li><p>atributos que afectan a facturaci\u00f3n;<\/p><\/li><li><p>relaciones cuya vigencia necesita demostrarse.<\/p><\/li><\/ul><p>En cambio, para determinados datos ef\u00edmeros o f\u00e1cilmente regenerables, conservar todas sus versiones puede ofrecer poco valor.<\/p><h2>Cinco reglas pr\u00e1cticas para dise\u00f1ar datos temporales<\/h2><h3>1. Define qu\u00e9 reloj est\u00e1s modelando<\/h3><p>\u00bfRepresentas tiempo de negocio, tiempo del sistema o ambos?<\/p><p>Nunca utilices una columna llamada simplemente <code dir=\"ltr\">fecha<\/code> si su significado temporal es importante.<\/p><h3>2. Documenta la sem\u00e1ntica de los extremos<\/h3><p>Decide expl\u00edcitamente si tus intervalos son:<\/p><pre><code class=\"language-text\">[desde, hasta)<\/code><\/pre><p>o utilizan otra convenci\u00f3n. No dejes esa interpretaci\u00f3n en manos de cada desarrollador.<\/p><h3>3. Impide solapamientos cuando el dominio no los permita<\/h3><p>Una regla cr\u00edtica de integridad deber\u00eda estar, siempre que sea posible, lo m\u00e1s cerca posible de los datos.<\/p><p>Las restricciones temporales disponibles en motores modernos pueden reducir mucha l\u00f3gica defensiva en la aplicaci\u00f3n.<\/p><h3>4. Dise\u00f1a tambi\u00e9n las relaciones temporales<\/h3><p>No basta con versionar entidades individualmente.<\/p><p>Pregunta: \u00bfLa relaci\u00f3n entre estas entidades tambi\u00e9n tiene una vigencia?<\/p><p>Un empleado puede pertenecer a distintos departamentos a lo largo del tiempo. Un producto puede pertenecer a diferentes categor\u00edas. Una persona puede tener diferentes permisos.<\/p><p>Muchas veces la relaci\u00f3n es tan temporal como la entidad.<\/p><h3>5. Dise\u00f1a la eliminaci\u00f3n del historial antes de acumularlo<\/h3><p>Retenci\u00f3n, particionado, archivado e \u00edndices deber\u00edan formar parte del dise\u00f1o inicial.<\/p><p>No esperes a que una tabla hist\u00f3rica tenga cientos de millones de filas para preguntarte cu\u00e1nto tiempo era necesario conservarlas.<\/p><h2>Qu\u00e9 motor elegir<\/h2><p>No existe una \u00fanica estrategia universal.<\/p><p><strong>SQL Server<\/strong> resulta especialmente c\u00f3modo cuando necesitas versionado autom\u00e1tico del estado almacenado y consultas hist\u00f3ricas integradas mediante <code dir=\"ltr\">FOR SYSTEM_TIME<\/code>.<\/p><p><strong>PostgreSQL 18<\/strong> da un salto importante en integridad temporal al incorporar claves con <code dir=\"ltr\">WITHOUT OVERLAPS<\/code> y relaciones temporales mediante <code dir=\"ltr\">PERIOD<\/code>.<\/p><p><strong>MariaDB<\/strong> permite trabajar tanto con versionado del sistema como con periodos de aplicaci\u00f3n.<\/p><p><strong>Oracle<\/strong> dispone de mecanismos Flashback orientados a reconstrucci\u00f3n y conservaci\u00f3n hist\u00f3rica.<\/p><p>La elecci\u00f3n no deber\u00eda basarse \u00fanicamente en cu\u00e1l posee m\u00e1s sintaxis temporal.<\/p><p>Lo importante es saber qu\u00e9 problema necesitas resolver:<\/p><ul data-spread=\"false\"><li><p>auditor\u00eda;<\/p><\/li><li><p>vigencia de negocio;<\/p><\/li><li><p>reconstrucci\u00f3n hist\u00f3rica;<\/p><\/li><li><p>correcciones retroactivas;<\/p><\/li><li><p>integridad de intervalos;<\/p><\/li><li><p>relaciones temporales;<\/p><\/li><li><p>an\u00e1lisis bitemporal.<\/p><\/li><\/ul><h2>Conclusi\u00f3n<\/h2><p>Los datos cambian, nuestros modelos deber\u00edan reconocerlo. Cuando actualizamos una fila convencional estamos tomando una decisi\u00f3n impl\u00edcita: el valor anterior deja de importarnos.<\/p><p>En muchos dominios esa suposici\u00f3n es falsa. El pasado puede determinar una factura, explicar una decisi\u00f3n, reconstruir un incidente, validar un contrato o responder una auditor\u00eda.<\/p><p>Modelar correctamente el tiempo no consiste en a\u00f1adir dos columnas de fecha, significa decidir:<\/p><ul data-spread=\"false\"><li><p>qu\u00e9 representa cada reloj;<\/p><\/li><li><p>qu\u00e9 periodos son v\u00e1lidos;<\/p><\/li><li><p>si pueden solaparse;<\/p><\/li><li><p>c\u00f3mo se relacionan temporalmente las entidades;<\/p><\/li><li><p>qu\u00e9 historia queremos conservar;<\/p><\/li><li><p>y durante cu\u00e1nto tiempo.<\/p><\/li><\/ul><p>La buena noticia es que los motores relacionales est\u00e1n incorporando cada vez m\u00e1s de estas reglas como capacidades nativas.<\/p><p>Y cuanto mejor entienda la base de datos la sem\u00e1ntica temporal, menos dependeremos de c\u00f3digo accidental para recordar correctamente nuestro propio pasado.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<section class=\"elementor-section elementor-inner-section elementor-element elementor-element-4fa9b73 elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"4fa9b73\" data-element_type=\"section\" data-e-type=\"section\" data-settings=\"{&quot;tc_dark_mode_responsive_hide_in_dark&quot;:&quot;no&quot;,&quot;tc_dark_mode_responsive_hide_in_light&quot;:&quot;no&quot;}\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-inner-column elementor-element elementor-element-eeaff01\" data-id=\"eeaff01\" data-element_type=\"column\" data-e-type=\"column\" data-settings=\"{&quot;tc_dark_mode_responsive_hide_in_dark&quot;:&quot;no&quot;,&quot;tc_dark_mode_responsive_hide_in_light&quot;:&quot;no&quot;}\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-6abbbf9 elementor-widget elementor-widget-heading\" data-id=\"6abbbf9\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;tc_dark_mode_responsive_hide_in_dark&quot;:&quot;no&quot;,&quot;tc_dark_mode_responsive_hide_in_light&quot;:&quot;no&quot;}\" data-widget_type=\"heading.default\">\n\t\t\t\t\t<h4 class=\"elementor-heading-title elementor-size-default\">REFERENCIAS\n<\/h4>\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-4439ef8 elementor-widget elementor-widget-text-editor\" data-id=\"4439ef8\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;tc_dark_mode_responsive_hide_in_dark&quot;:&quot;no&quot;,&quot;tc_dark_mode_responsive_hide_in_light&quot;:&quot;no&quot;}\" data-widget_type=\"text-editor.default\">\n\t\t\t\t\t\t\t\t\t<ul class=\"content-list\"><li>Microsoft Learn \u2014 Temporal tables (SQL Server), documentaci\u00f3n actualizada en 2026: https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/tables\/temporal-tables?view=sql-server-ver17<\/li><li>Microsoft Learn \u2014 Create a System-Versioned Temporal Table, actualizado el 18\/08\/2026: https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/tables\/creating-a-system-versioned-temporal-table?view=sql-server-ver17<\/li><li>Microsoft Learn \u2014 Query Data in a System-Versioned Temporal Table: https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/tables\/temporal\/query-data?view=sql-server-ver15<\/li><li>Microsoft Learn \u2014 Manage Historical Data in System-Versioned Temporal Tables: https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/tables\/manage-retention-of-historical-data-in-system-versioned-temporal-tables?view=sql-server-ver17<\/li><li>PostgreSQL 18 \u2014 Release notes: temporal constraints with WITHOUT OVERLAPS and PERIOD: https:\/\/www.postgresql.org\/docs\/18\/release-18.html<\/li><li>PostgreSQL 18 \u2014 CREATE TABLE, temporal keys and PERIOD foreign keys: https:\/\/www.postgresql.org\/docs\/18\/sql-createtable.html<\/li><li>MariaDB \u2014 Rewinding Time in MariaDB Databases: System-Versioning and Application-Time: https:\/\/mariadb.com\/resources\/blog\/rewinding-time-in-mariadb-databases-system-versioning-and-application-time\/<\/li><li>Oracle Database \u2014 CREATE FLASHBACK ARCHIVE: https:\/\/docs.oracle.com\/en\/database\/oracle\/oracle-database\/18\/sqlrf\/CREATE-FLASHBACK-ARCHIVE.html<\/li><\/ul>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Explicar vigencia, hist\u00f3ricos y relaciones temporales con ejemplos SQL.<\/p>\n","protected":false},"author":1,"featured_media":6546,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[39],"tags":[72,73,74,75,48],"class_list":["post-6543","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-bbdd","tag-bases-de-datos","tag-bbdd","tag-modelado","tag-series-temporales","tag-software"],"_links":{"self":[{"href":"https:\/\/blog-david.moreno-morales.com\/index.php\/wp-json\/wp\/v2\/posts\/6543","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog-david.moreno-morales.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog-david.moreno-morales.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog-david.moreno-morales.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog-david.moreno-morales.com\/index.php\/wp-json\/wp\/v2\/comments?post=6543"}],"version-history":[{"count":6,"href":"https:\/\/blog-david.moreno-morales.com\/index.php\/wp-json\/wp\/v2\/posts\/6543\/revisions"}],"predecessor-version":[{"id":6552,"href":"https:\/\/blog-david.moreno-morales.com\/index.php\/wp-json\/wp\/v2\/posts\/6543\/revisions\/6552"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog-david.moreno-morales.com\/index.php\/wp-json\/wp\/v2\/media\/6546"}],"wp:attachment":[{"href":"https:\/\/blog-david.moreno-morales.com\/index.php\/wp-json\/wp\/v2\/media?parent=6543"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog-david.moreno-morales.com\/index.php\/wp-json\/wp\/v2\/categories?post=6543"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog-david.moreno-morales.com\/index.php\/wp-json\/wp\/v2\/tags?post=6543"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}