Guías de ingeniería
Importaciones de CSV en PostgreSQL: valores predeterminados, identidad y columnas generadas
Elige una lista explícita de columnas para COPY. Comprende los valores omitidos, NULL, la sustitución de identidades y las expresiones generadas antes de importar datos de clientes.
Cuando PostgreSQL deba aportar un valor, omite esa columna de la lista de entrada de COPY. No añadas una celda CSV en blanco para sustituir la omisión. Un valor en blanco puede convertirse en NULL, y NULL normalmente no activa el valor predeterminado de una columna.
Hay una segunda decisión: si se permite un valor de origen. Un valor predeterminado ordinario suele permitir que un valor explícito lo sustituya. Una expresión generada no lo permite. Las columnas de identidad tienen sus propias reglas, incluida una excepción de COPY que puede sorprender a un ingeniero acostumbrado a INSERT.
Estas distinciones importan cuando una exportación de un cliente contiene campos como id, created_at o un total calculado. La capacidad de la base de datos para aceptar un valor y el permiso de la aplicación para aceptarlo son cuestiones distintas.
Alcance y supuestos. Los ejemplos están pensados para PostgreSQL 17. PostgreSQL 18 también admite columnas generadas virtuales; los ejemplos de esta guía usan STORED.
Clasifica los valores antes de mapear los encabezados
| Comportamiento de la columna | ¿Qué aporta el valor? | Decisión de importación |
|---|---|---|
| Columna ordinaria sin valor predeterminado | El origen o un valor de confianza de la aplicación; en su defecto, NULL | Aporta expresamente los datos obligatorios |
| Expresión DEFAULT ordinaria | El valor predeterminado si se omite; un valor explícito puede sustituirlo | Decide si se permiten sustituciones |
| GENERATED BY DEFAULT AS IDENTITY | La secuencia si se omite; es posible aportar valores | Elige una política de conservación de IDs |
| GENERATED ALWAYS AS IDENTITY | La secuencia en un INSERT ordinario que omite el valor; un INSERT explícito necesita una cláusula de sustitución | No supongas que COPY rechaza los IDs aportados |
| GENERATED ALWAYS AS (expression) STORED | PostgreSQL lo calcula a partir de la fila | Omítela de la lista de columnas de COPY |
| Tenant o actor controlado por la aplicación | Contexto autorizado de la aplicación | Exclúyelo de los mapeos controlados por el cliente |
«Controlado por la base de datos» es una política de importación, no un tipo único de columna de PostgreSQL. Una marca de tiempo con DEFAULT now() puede representar un evento histórico que el origen tiene permiso para aportar, o una hora de auditoría que el origen nunca debe cambiar. El DDL no puede determinar ese significado de negocio.
Documenta la decisión para cada campo. Para las marcas de tiempo históricas, anota también la zona horaria del origen y el tratamiento de los valores ausentes. Para los identificadores, indica si son referencias externas o claves primarias de producción. Que los nombres coincidan no establece una identidad coincidente.
Una pequeña importación que deja la generación a PostgreSQL
Ejecuta este ejemplo en una sola sesión de psql conectada a una base de datos desechable de PostgreSQL 17. La tabla temporal desaparece cuando termina la sesión.
CREATE TEMP TABLE import_lines_demo (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
external_ref text NOT NULL UNIQUE,
quantity integer NOT NULL CHECK (quantity > 0),
unit_price numeric(12,2) NOT NULL CHECK (unit_price >= 0),
status text NOT NULL DEFAULT 'new',
total numeric GENERATED ALWAYS AS (quantity * unit_price) STORED
);
COPY import_lines_demo (external_ref, quantity, unit_price)
FROM STDIN WITH (FORMAT csv, HEADER MATCH);
external_ref,quantity,unit_price
line-a,2,12.50
line-b,3,4.00
\.
SELECT id, external_ref, status, total
FROM import_lines_demo ORDER BY id;El resultado contiene los IDs 1 y 2, el estado new en ambas filas y los totales 25.00 y 12.00. Este es un ejemplo con datos en línea para psql: la línea que contiene una barra inversa y un punto termina el flujo de datos. No es un bloque para pegar en un editor SQL web.
La lista de importación tiene tres columnas porque hay tres campos de origen. El ID, el estado y el total no están en la entrada. PostgreSQL los aporta según sus distintas definiciones. Esto resulta más fácil de revisar que enviar seis campos y esperar que los valores en blanco signifiquen lo correcto.
Para un archivo local independiente, usa la misma lista de columnas con \copy de psql. La referencia de COPY especifica cómo se interpretan el orden de los campos, las columnas omitidas y las opciones de entrada.
Omitido, NULL y vacío son entradas diferentes
Con la configuración CSV predeterminada de PostgreSQL, un campo vacío sin comillas representa NULL. Un campo vacío entre comillas, "", representa una cadena vacía. Ninguno equivale a dejar una columna fuera de la lista de entrada.
external_ref,quantity,unit_price,status
line-c,1,8.00,
line-d,1,8.00,""Si se importa con el estado incluido y las opciones CSV predeterminadas, el primer registro incumple la restricción NOT NULL de la tabla. El segundo aportaría un estado vacío en lugar de new. Esta tabla de demostración no tiene un CHECK de estados permitidos, por lo que una cadena vacía por sí sola se aceptaría. Esto ilustra por qué algo «aceptado por PostgreSQL» puede seguir siendo incorrecto para la aplicación.
En una sentencia INSERT, el SQL DEFAULT puede solicitar expresamente un valor predeterminado. El texto DEFAULT en un campo CSV ordinario es solo texto. PostgreSQL 17 tiene una opción independiente de COPY para un marcador DEFAULT; úsala únicamente con una convención de archivo acordada y sin colisiones. No es necesaria cuando puede omitirse toda la columna.
Cuando algunos registros aportan un valor de forma intencional y otros necesitan el predeterminado, una sola lista de columnas omitidas no puede expresar ambos casos. Las opciones incluyen un marcador de valor predeterminado documentado, agrupar las filas según los campos que se escriban o usar un área de preparación seguida de sentencias INSERT definidas expresamente. No sustituyas esa distinción por «vacío significa predeterminado» salvo que el responsable de los datos apruebe realmente esa regla.
COPY puede aportar una identidad ALWAYS
GENERATED ALWAYS AS IDENTITY impide que un INSERT ordinario aporte el valor de identidad sin más. COPY FROM sí puede usar los IDs presentes en su entrada. Trátalo como una capacidad explícita de importación, no como un límite de autorización.
Después de la demostración anterior, el siguiente COPY puede insertar un ID elegido:
COPY import_lines_demo (id, external_ref, quantity, unit_price)
FROM STDIN WITH (FORMAT csv);
900,line-explicit,1,1.00
\.Si tu política consiste en crear identidades de destino nuevas, excluye id tanto del mapeo como de la lista de COPY. Si se pretende conservar los IDs, comprueba los conflictos y el estado de la secuencia asociada. Aportar una identidad no hace avanzar por sí solo la secuencia hasta el mayor ID importado; un valor generado posterior podría acabar colisionando.
La reparación de secuencias forma parte del plan de migración del propietario de la base de datos. No ejecutes un comando genérico para «ajustar la secuencia al ID máximo» mientras la aplicación escribe de forma concurrente: puede haber valores ya asignados. Ensaya el procedimiento elegido con las condiciones de concurrencia y mantenimiento de la aplicación. La documentación de identidades y la referencia de funciones de secuencia explican los mecanismos.
Las expresiones generadas se recalculan, no se importan
La hoja de cálculo de un cliente puede tener su propia columna «Total». En la tabla del ejemplo, el total de destino se calcula a partir de la cantidad y el precio unitario. Deja el total de origen fuera del mapeo de escritura y compáralo por separado si sirve como prueba.
Si los totales difieren, decide qué regla es incorrecta. Un sistema de origen podría calcular el impuesto antes de redondear, mientras que el destino redondea primero. Sobrescribir un valor generado del destino no es la vía de corrección. Necesitas una transformación de entrada aprobada o un cambio revisado del modelo de destino.
PostgreSQL 17 admite columnas generadas almacenadas. La generación se realiza después de los cambios pertinentes de los disparadores BEFORE sobre la fila base. Por tanto, la presencia de una expresión generada no garantiza que el valor sea igual al de un cálculo realizado antes en una vista previa. Lee la expresión y los disparadores aplicables, y prueba después la vía real de escritura. Consulta las reglas de columnas generadas de PostgreSQL 17.
Qué incluir en la revisión de la importación
Para cada columna, registra quién aporta su valor, la regla de omisión, las sustituciones permitidas desde el origen y las pruebas que respaldan esa decisión. Incluye la tabla de destino exacta y la lista ordenada de columnas de COPY. Si un valor obligatorio procede de la aplicación, identifica la autoridad que lo aporta.
- Prueba por separado los valores omitidos, NULL y vacíos cuando tengan significado.
- Prueba un intento de sustitución desde el origen de cada columna protegida.
- Confirma si conservar los IDs requiere un procedimiento de secuencias o de mapeo de referencias.
- Compara los valores derivados almacenados con las reglas de negocio aprobadas después de la inserción.
PG Import Check puede ayudar a clasificar el DDL facilitado: los valores de origen obligatorios, los valores predeterminados ordinarios, las identidades y las expresiones generadas se mantienen diferenciados. También muestra hallazgos estructurales y lagunas del análisis. No ejecuta expresiones, inspecciona el esquema real, valida registros de origen ni deduce que un valor predeterminado autorice un campo.
Para campos como el ID del tenant, continúa con la guía de autoridad y RLS. Para comprobar los valores resultantes, usa la verificación posterior a la importación.