El Blueprint de Esquemas y Restricciones
Un esquema de PostgreSQL es más que una lista de tablas y columnas.
Es un conjunto de promesas sobre qué datos están permitidos existir, y las restricciones son el mecanismo que mantiene esas promesas verdaderas incluso cuando el código de la aplicación tiene errores.
Esta página construye el modelo mental detrás del diseño de esquemas y restricciones antes de que toques las páginas de "cómo hacerlo" en esta sección sobre claves, columnas generadas, eliminaciones lógicas o transacciones DDL.
Comprender este modelo es importante porque cada decisión posterior, desde la elección de una estrategia de clave primaria hasta la elección de una restricción CHECK sobre una verificación de aplicación, fluye de la misma pregunta subyacente: ¿dónde debe vivir la fuente de verdad para la corrección?
Resumen
- Un esquema define la forma de tus datos, y las restricciones definen qué instancias de esa forma son válidas.
- Por Qué Importa: Errores, condiciones de carrera y migraciones a medio terminar intentarán eventualmente escribir datos inválidos, y solo la base de datos puede garantizar que nunca se queden.
- Conceptos Clave: normalización, clave primaria, clave foránea, restricción
CHECK, restricciónUNIQUE, DDL. - Cuándo Usar: Cada tabla que creas necesita este pensamiento, pero es más importante para datos financieros, sistemas multiinquilino y cualquier cosa con relaciones entre tablas.
- Limitaciones / Compensaciones: Las restricciones estrictas añaden sobrecarga en el momento de la escritura y pueden hacer que algunas migraciones sean más difíciles de implementar sin tiempo de inactividad.
- Temas Relacionados: claves primarias y foráneas, columnas generadas e de identidad, transacciones DDL, eliminaciones lógicas.
Fundamentos
Un esquema en el sentido de diseño es el conjunto de tablas, columnas, tipos y relaciones que modelan tu dominio de negocio.
Esto es distinto de un "esquema" de PostgreSQL, un objeto de espacio de nombres, aunque los dos comparten un nombre por razones históricas.
Diseñar un esquema comienza con la normalización, la práctica de dividir los datos en tablas de modo que cada hecho se almacene en un solo lugar.
Una división normalizada de orders y order_lines, por ejemplo, evita repetir la dirección de envío de un cliente en cada línea de pedido.
La normalización reduce las anomalías de actualización, donde cambiar un hecho requiere tocar muchas filas, pero también significa más uniones en el momento de la consulta.
La mayoría de los esquemas de producción se encuentran en algún punto entre completamente normalizados y deliberadamente desnormalizados, y ese punto de aterrizaje es una decisión de diseño, no un accidente.
Las restricciones son las reglas que hacen que un esquema sea más que una sugerencia.
Una restricción NOT NULL dice que un valor siempre es requerido, una restricción CHECK dice que un valor debe satisfacer una expresión booleana, y una restricción UNIQUE dice que ninguna fila puede compartir un valor.
Una PRIMARY KEY combina NOT NULL y UNIQUE en una sola columna o conjunto de columnas que identifica una fila.
Una FOREIGN KEY vincula una columna en una tabla a una clave en otra, de modo que una referencia nunca pueda apuntar a una fila que no existe.
Piensa en un esquema sin restricciones como un archivador sin etiquetas, cualquiera puede poner cualquier cosa en cualquier lugar, y piensa en las restricciones como las etiquetas que hacen posible la recuperación y la confianza.
La ilustración más simple es una sola tabla con una clave primaria y una restricción CHECK que trabajan juntas.
CREATE TABLE app.order_lines (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_id bigint NOT NULL,
qty int NOT NULL CHECK (qty > 0)
);Esta única declaración codifica tres promesas separadas: cada fila tiene una identidad estable, cada línea pertenece a un pedido y ninguna línea puede reclamar una cantidad no positiva.
Mecánicas e Interacciones
Las restricciones no son documentación pasiva, se aplican en cada escritura por el ejecutor antes de que se permita confirmar una transacción.
Cuando insertas o actualizas una fila, PostgreSQL verifica inmediatamente las restricciones NOT NULL y CHECK como parte de esa declaración.
Las restricciones UNIQUE y PRIMARY KEY están respaldadas por un índice, por lo que la unicidad se aplica buscando el nuevo valor en ese índice antes de que se permita que la fila se asiente.
Las restricciones FOREIGN KEY son verificadas por un disparador del sistema que consulta la tabla referenciada, por lo que una clave padre sin índice puede hacer que las verificaciones de clave foránea sean lentas.
Por defecto, la mayoría de las restricciones se verifican al final de cada declaración, pero las restricciones DEFERRABLE se pueden posponer hasta el final de la transacción.
Esta postergación es importante cuando necesitas insertar filas que se referencian entre sí, ya que una verificación de clave foránea estrictamente inmediata rechazaría la primera fila antes de que exista la segunda.
DDL, las declaraciones que crean o alteran estos objetos, tiene sus propias mecánicas que vale la pena entender temprano.
En PostgreSQL, la mayoría de las declaraciones DDL son transaccionales, lo que significa que un CREATE TABLE o ALTER TABLE dentro de un bloque BEGIN/COMMIT se puede revertir como cualquier otro cambio.
Esto es inusual en comparación con muchas otras bases de datos relacionales, y es una ventaja genuina cuando una migración de varios pasos necesita ser todo o nada.
Sin embargo, agregar una restricción a una tabla existente no es gratuito en el momento en que la agregas.
ALTER TABLE ... ADD CONSTRAINT típicamente toma un bloqueo ACCESS EXCLUSIVE mientras valida cada fila existente contra la nueva regla.
En una tabla pequeña, esta validación es instantánea, pero en una tabla grande y ocupada, puede bloquear lecturas y escrituras durante la duración del escaneo.
PostgreSQL ofrece un patrón de dos pasos para este problema exacto: primero agrega la restricción como NOT VALID, lo que solo bloquea nuevas escrituras, luego ejecuta VALIDATE CONSTRAINT por separado para verificar las filas existentes con un bloqueo mucho más ligero.
Las columnas generadas y de identidad extienden este mismo modelo de aplicación a valores calculados.
Una columna de identidad garantiza una clave sustituta generada por el servidor y tolerante a huecos sin que la aplicación elija nunca un valor, mientras que una columna generada deriva su valor de otras columnas y no se puede escribir directamente.
Ambas eliminan una categoría completa de errores donde dos rutas de código discrepan sobre cómo calcular lo mismo.
Consideraciones Avanzadas y Aplicaciones
A escala, las preguntas interesantes sobre esquemas pasan de "¿qué restricciones necesito?" a "¿cómo las cambio sin interrupción?".
Agregar una restricción NOT NULL a una tabla grande solía requerir una reescritura completa de la tabla en versiones anteriores de PostgreSQL, pero el PostgreSQL moderno puede validarla como una verificación rápida contra una restricción CHECK existente, por lo que el relleno y la restricción a menudo se hacen mejor como pasos separados y secuenciados.
Los esquemas multiinquilino plantean una pregunta relacionada sobre dónde vive el aislamiento del inquilino, ya sea como una columna tenant_id con seguridad a nivel de fila, una clave foránea impuesta por restricciones, o esquemas completamente separados por inquilino.
Las restricciones de exclusión, construidas sobre la misma infraestructura que los índices únicos pero generalizadas a cualquier operador, resuelven problemas que las restricciones únicas no pueden, como prevenir rangos de fechas superpuestos para el mismo recurso.
Los dominios ofrecen otra capa de reutilización, permitiéndote definir un tipo con nombre con una restricción CHECK incorporada una vez y aplicarlo a muchas columnas de manera consistente, aunque son menos flexibles que las restricciones a nivel de tabla cuando una regla necesita referenciar múltiples columnas.
La pregunta arquitectónica más profunda es dónde debe vivir la lógica de validación, y esa pregunta no tiene una respuesta universalmente correcta, solo compensaciones.
| Enfoque | Fortaleza | Debilidad | Mejor Ajuste |
|---|---|---|---|
| Restricciones de base de datos (CHECK, UNIQUE, FK) | Aplicadas para cada escritor, incluyendo scripts y otros servicios | Costo de migración y validación en tablas grandes | Invariantes que nunca deben violarse, independientemente del llamador |
| Validación a nivel de aplicación | Rápido de iterar, mensajes de error ricos, fácil de probar de forma aislada | Eludido por SQL directo, trabajos por lotes o un segundo servicio | Moldeado de entrada del usuario y reglas de negocio que cambian con frecuencia |
| Disparadores de base de datos | Pueden imponer reglas entre filas y entre tablas que las restricciones no pueden expresar | Más difíciles de razonar, pueden ocultar la lógica a los lectores del esquema | Invariantes raros demasiado complejos para una restricción declarativa |
| Validación a nivel de ORM | Conveniente, ubicado junto al código del modelo | Solo tan fuerte como la única ruta de código que usa el ORM | Una segunda línea de defensa superpuesta a las restricciones reales |
El patrón que mejor se mantiene en producción es la superposición: validación de aplicaciones para la experiencia del usuario, restricciones de base de datos para el invariante que nunca debe romperse, y disparadores reservados para el caso raro que ninguno de los dos puede expresar limpiamente.
La evolución del esquema también tiene una dimensión de observabilidad, ya que una violación de restricción en producción es una señal, no solo un error, y rastrear qué restricciones rechazan la mayor cantidad de filas con el tiempo a menudo apunta a una discrepancia entre lo que la aplicación asume y lo que realmente sucede aguas arriba.
Conceptos Erróneos Comunes
- "La validación de la aplicación es suficiente si el código es cuidadoso." Cada escritor adicional, script, trabajo ETL o servicio futuro que toque la tabla elude por completo el código de la aplicación, por lo que solo una restricción de base de datos está garantizada para aplicarse en todas partes.
- "La normalización es siempre el valor predeterminado correcto." La normalización intensiva puede convertir lecturas simples en uniones costosas de múltiples vías, y una columna deliberadamente desnormalizada es a veces la compensación adecuada para una ruta de lectura en caliente.
- "DDL es instantáneo, así que puedo ejecutar migraciones en cualquier momento." Muchas operaciones DDL toman bloqueos fuertes que bloquean lecturas y escrituras concurrentes, y el patrón seguro a menudo es dividir un cambio en un paso de bloqueo ligero seguido de un paso de validación.
- "Las claves foráneas siempre ralentizan demasiado las escrituras como para molestarse." El costo real suele ser un índice faltante en la columna de referencia, no la clave foránea en sí, y una clave foránea indexada solo agrega una sobrecarga pequeña y predecible.
- "NULL significa lo mismo que una cadena vacía o cero." NULL representa desconocido o no aplicable, y tratarlo como equivalente a un valor predeterminado causa errores silenciosos en comparaciones y agregados.
Preguntas Frecuentes
¿Cuál es la diferencia entre un "esquema" de PostgreSQL y el diseño de un esquema de base de datos?
- Un esquema de PostgreSQL (como en
CREATE SCHEMA app) es un espacio de nombres que agrupa tablas, vistas y funciones. - El diseño de esquemas como disciplina se refiere a cómo modelas tablas, claves y restricciones para tu dominio.
- Los dos conceptos comparten un nombre pero operan en niveles completamente diferentes.
¿Debería cada tabla tener una clave primaria sustituta?
- Una clave sustituta como una columna de identidad suele ser el valor predeterminado más seguro para la estabilidad de las uniones.
- Una clave natural aún merece una restricción
UNIQUEincluso cuando existe una clave sustituta. - Las claves naturales compuestas funcionan bien para tablas de referencia o búsqueda puras que rara vez son referenciadas por muchas tablas hijas.
¿Por qué usaría una restricción CHECK en lugar de validar en el código de la aplicación?
Una restricción CHECK se aplica a cada escritor de esa tabla, incluyendo scripts, migraciones y otros servicios, mientras que la validación de la aplicación solo protege la ruta de código que la ejecuta.
¿Agregar una clave foránea siempre bloquea la tabla durante mucho tiempo?
- El bloqueo en sí es breve, pero la validación de las filas existentes contra la nueva clave puede llevar tiempo proporcional al tamaño de la tabla.
- Indexar la columna de referencia de antemano mantiene esa validación rápida.
- El patrón
NOT VALIDmásVALIDATE CONSTRAINTminimiza el bloqueo en tablas grandes y ocupadas.
¿Cuál es la diferencia práctica entre UNIQUE y PRIMARY KEY?
Una PRIMARY KEY es una restricción UNIQUE combinada con NOT NULL, y una tabla solo puede tener una clave primaria pero muchas restricciones únicas separadas.
¿Cuándo debería usar una restricción EXCLUSION en lugar de UNIQUE?
Una restricción de exclusión generaliza la unicidad a cualquier operador, lo que la convierte en la herramienta adecuada cuando necesitas prevenir rangos superpuestos, como dos reservas para el mismo recurso que comparten una ventana de tiempo superpuesta.
¿Son las columnas generadas más lentas de escribir que las columnas regulares?
- Una columna generada añade un pequeño costo de cómputo en cada inserción o actualización que toca sus entradas.
- Ese costo es casi siempre menor que los errores causados por dos rutas de código que calculan el mismo valor derivado de manera diferente.
- Las columnas generadas almacenadas también añaden una pequeña cantidad de almacenamiento adicional en comparación con el cálculo del valor en el momento de la lectura.
¿Es el DDL de PostgreSQL realmente transaccional?
La mayoría de las declaraciones DDL, incluyendo CREATE TABLE y ALTER TABLE, participan completamente en transacciones y se revierten limpiamente si la transacción circundante se aborta, lo cual es una ventaja genuina sobre las bases de datos donde el DDL se confirma automáticamente.
¿Cómo agrego una restricción NOT NULL a una tabla enorme sin tiempo de inactividad?
- Primero, agrega una restricción
CHECKcomoNOT VALIDque refleje la regla NOT NULL. - Valídala por separado con
VALIDATE CONSTRAINT, que toma un bloqueo mucho más ligero. - El PostgreSQL moderno puede entonces derivar la garantía NOT NULL de esa
CHECKvalidada sin un segundo escaneo completo de la tabla.
¿Cuál es la diferencia entre un dominio y una restricción CHECK a nivel de tabla?
Un dominio empaqueta un tipo base con una restricción incorporada para que pueda ser reutilizado consistentemente en muchas columnas y tablas, mientras que una restricción CHECK a nivel de tabla se aplica solo a esa tabla y puede referenciar múltiples columnas a la vez.
¿Por qué importa la normalización si el almacenamiento es barato?
La normalización no se trata principalmente del costo de almacenamiento, sino de evitar anomalías de actualización donde el mismo hecho almacenado en múltiples lugares puede desincronizarse.
¿Deberían las eliminaciones lógicas ser impuestas con una restricción?
Las eliminaciones lógicas típicamente dependen de una columna deleted_at anulable emparejada con un índice parcial o una restricción única parcial, de modo que las reglas de unicidad solo se apliquen a las filas aún activas.
¿Cuál es el mayor error de diseño de esquemas que cometen los equipos al principio?
Tratar las restricciones como documentación opcional en lugar del mecanismo de aplicación real es el error más común, ya que deja la corrección dependiente de que cada escritor futuro se comporte perfectamente.
Relacionados
- Fundamentos del Diseño de Esquemas - ejemplos prácticos de claves y convenciones de nomenclatura
- Claves Primarias y Foráneas - integridad referencial y comportamiento de ON DELETE en la práctica
- Restricciones CHECK y UNIQUE - reglas de validación a nivel de fila y a nivel de conjunto
- Columnas Generadas y de Identidad - valores calculados y generados por el servidor
- Transacciones DDL - cómo los cambios de esquema participan en transacciones y bloqueos
- Eliminaciones Lógicas vs. Eliminaciones Duras - estrategia de eliminación y sus implicaciones de restricciones
Versiones de Stack: Esta página fue escrita para PostgreSQL 18.4 (línea principal estable 18, línea de mantenimiento 17 también soportada).