Combina ON DELETE SET NULL y ON DELETE CASCADE asignando cada acción a una clave foránea independiente. Este tutorial usa artículos sintéticos para mostrar cómo eliminar una colección puede conservar sus artículos, mientras que eliminar un artículo borra sus secciones.

La tarea consiste en definir ese comportamiento, verificar ambas rutas de eliminación desde la Terminal de un Mac y repetir las comprobaciones con node-postgres. Los ejemplos usan claves foráneas normales y tablas temporales, por lo que cada ejecución tiene sus propias filas de muestra.

¿Qué relación debería usar cada acción de eliminación?

Una acción de eliminación corresponde a la clave foránea de la tabla que hace referencia. SET NULL borra la referencia cuando desaparece la fila referenciada; CASCADE elimina la fila que hace referencia. PostgreSQL documenta las relaciones opcionales como un uso adecuado de SET NULL, y los componentes dependientes como un uso adecuado de CASCADE. Restricciones

En este ejemplo, una colección agrupa artículos, pero un artículo puede existir sin pertenecer a una colección. Una sección pertenece a un artículo y no tiene una función independiente. Estas son decisiones de diseño explícitas para el ejemplo:

Clave foránea Acción elegida Resultado previsto
demo_articles.collection_id hace referencia a demo_collections.collection_id ON DELETE SET NULL Al eliminar una colección, sus artículos se conservan sin una referencia a la colección.
demo_sections.article_id hace referencia a demo_articles.article_id ON DELETE CASCADE Al eliminar un artículo, se eliminan sus secciones.

El comportamiento resultante depende de dónde se inicie la eliminación. Eliminar una colección cambia el valor de collection_id de un artículo; no elimina el artículo. Por lo tanto, ON DELETE CASCADE de la clave foránea de las secciones no tiene una eliminación del artículo a la que responder. Eliminar el artículo en sí sí activa esa cascada. Esto se desprende de las definiciones documentadas de ambas acciones. CREATE TABLE

No agregues dos claves foráneas en conflicto a una misma relación para expresar «borrar la referencia y eliminar la fila». Decide si la fila que hace referencia debería conservarse y, luego, elige la acción que implemente esa decisión.

Crea un ejemplo completo en tu Mac

Necesitas un servidor PostgreSQL en ejecución, una base de datos existente a la que puedas conectarte y psql disponible en Terminal. Los comandos de abajo usan la base de datos local existente llamada postgres; sustitúyela por el nombre de tu base de datos existente si es necesario. La instalación y la configuración del servidor quedan fuera de este tutorial.

Guarda lo siguiente como un archivo de texto sin formato llamado setup.sql en una carpeta de trabajo. Las tres tablas son temporales: PostgreSQL elimina las tablas temporales cuando termina su sesión. En este ejemplo, todas las tablas referenciadas también son temporales, ya que PostgreSQL no permite claves foráneas entre tablas temporales y permanentes. CREATE TABLE

CREATE TEMP TABLE demo_collections (
    collection_id integer PRIMARY KEY,
    title text NOT NULL
);

CREATE TEMP TABLE demo_articles (
    article_id integer PRIMARY KEY,
    collection_id integer,
    title text NOT NULL,
    CONSTRAINT demo_articles_collection_fk
        FOREIGN KEY (collection_id)
        REFERENCES demo_collections (collection_id)
        ON DELETE SET NULL
);

CREATE TEMP TABLE demo_sections (
    section_id integer PRIMARY KEY,
    article_id integer NOT NULL,
    heading text NOT NULL,
    CONSTRAINT demo_sections_article_fk
        FOREIGN KEY (article_id)
        REFERENCES demo_articles (article_id)
        ON DELETE CASCADE
);

CREATE INDEX demo_articles_collection_idx
    ON demo_articles (collection_id);
CREATE INDEX demo_sections_article_idx
    ON demo_sections (article_id);

INSERT INTO demo_collections (collection_id, title) VALUES
    (1, 'Layout Notes'),
    (2, 'Color Notes');

INSERT INTO demo_articles (article_id, collection_id, title) VALUES
    (101, 1, 'Grid Spacing'),
    (102, 2, 'Color Pairings');

INSERT INTO demo_sections (section_id, article_id, heading) VALUES
    (1001, 101, 'Overview'),
    (1002, 101, 'Examples'),
    (1003, 102, 'Overview');

Observa que collection_id admite valores nulos, mientras que article_id en la tabla de secciones es obligatorio. Una acción SET NULL debe seguir cumpliendo las demás restricciones de la tabla. Los índices explícitos cubren las columnas que hacen referencia; PostgreSQL no crea automáticamente esos índices cuando se declara una clave foránea. Restricciones

Verifica ambas rutas de eliminación en una sesión

Guarda el siguiente bloque como verify.sql junto a setup.sql. Es un script completo de psql: \ir incluye el archivo de configuración relativo al script de verificación. Ejecuta ambos archivos mediante este script para que las tablas temporales permanezcan en la misma sesión. La opción -f ejecuta un archivo, y ON_ERROR_STOP detiene el procesamiento del script si ocurre un error. psql

BEGIN;
\ir setup.sql

DELETE FROM demo_collections
WHERE collection_id = 1
RETURNING collection_id;

SELECT a.article_id,
       a.title,
       a.collection_id,
       c.title AS collection_title,
       count(s.section_id) AS section_count
FROM demo_articles AS a
LEFT JOIN demo_collections AS c
    ON c.collection_id = a.collection_id
LEFT JOIN demo_sections AS s
    ON s.article_id = a.article_id
GROUP BY a.article_id, a.title, a.collection_id, c.title
ORDER BY a.article_id;

DELETE FROM demo_articles
WHERE article_id = 101
RETURNING article_id;

SELECT section_id, article_id, heading
FROM demo_sections
ORDER BY section_id;

ROLLBACK;

Desde Terminal, cambia a la carpeta que contiene los archivos y ejecuta:

psql -X -v ON_ERROR_STOP=1 -d postgres -f verify.sql

Los resultados previstos de la primera consulta se derivan de las filas de muestra:

Artículo ID de colección tras eliminar la colección Título de la colección Número de secciones
101 NULL NULL 2
102 2 Color Notes 1

El artículo 101 se conserva con sus dos secciones. Después de la segunda eliminación, la consulta de secciones debería devolver solo la sección 1003, que pertenece al artículo 102. Estas dos comprobaciones permiten distinguir entre conservar un artículo y eliminar un artículo junto con sus filas dependientes.

La consulta de verificación usa intencionalmente LEFT JOIN. PostgreSQL conserva una fila sin coincidencia de la tabla izquierda y proporciona valores nulos para los datos faltantes de la tabla derecha. Un INNER JOIN con las colecciones omitiría el artículo 101 después de que su referencia pase a ser nula, haciendo que el artículo conservado desapareciera de los resultados de esa consulta. Expresiones de tabla

Las cláusulas RETURNING informan las filas eliminadas de las tablas de destino. No proporcionan un inventario combinado de todas las secciones eliminadas por la cascada; la consulta de secciones por separado verifica ese resultado. DELETE

La sentencia ROLLBACK final cancela los cambios de la demostración. PostgreSQL también trata una sentencia independiente como una transacción, así que aquí resulta útil una transacción explícita para agrupar la configuración, la eliminación, la inspección y la limpieza. Transacciones

Ejecuta las mismas comprobaciones con node-postgres

Para la versión de JavaScript, deja setup.sql sin cambios. Usa Node.js 24 para este ejemplo e instala el paquete documentado pg en la carpeta de trabajo: node-postgres

npm install pg

Guarda el siguiente programa completo como verify.mjs junto a setup.sql. Lee los mismos datos de prueba sintéticos, verifica que eliminar una colección conserve ambos artículos y las tres secciones, y luego comprueba que eliminar un artículo borre solo las secciones correspondientes.

El programa usa un único cliente conectado durante toda la transacción, tal como lo requiere node-postgres. Sus valores de consulta se pasan mediante la interfaz documentada de matrices de parámetros, usando $1. Transacciones, Consultas

import pg from 'pg';
import { readFile } from 'node:fs/promises';
import assert from 'node:assert/strict';

const { Client } = pg;
const client = new Client();
const setup = await readFile(
  new URL('./setup.sql', import.meta.url),
  'utf8'
);

await client.connect();
try {
  await client.query('BEGIN');
  await client.query(setup);

  const removed = await client.query(
    'DELETE FROM demo_collections WHERE collection_id = $1 RETURNING collection_id',
    [1]
  );
  assert.deepEqual(removed.rows, [{ collection_id: 1 }]);

  const articles = await client.query(
    'SELECT article_id, collection_id FROM demo_articles ORDER BY article_id'
  );
  assert.deepEqual(articles.rows, [
    { article_id: 101, collection_id: null },
    { article_id: 102, collection_id: 2 }
  ]);

  const before = await client.query(
    'SELECT section_id FROM demo_sections ORDER BY section_id'
  );
  assert.deepEqual(
    before.rows.map(row => row.section_id),
    [1001, 1002, 1003]
  );

  await client.query(
    'DELETE FROM demo_articles WHERE article_id = $1',
    [101]
  );
  const after = await client.query(
    'SELECT section_id FROM demo_sections ORDER BY section_id'
  );
  assert.deepEqual(after.rows, [{ section_id: 1003 }]);

  console.log('Expected SET NULL and CASCADE outcomes verified.');
  await client.query('ROLLBACK');
} catch (error) {
  try {
    await client.query('ROLLBACK');
  } catch (rollbackError) {
    console.error('Rollback failed:', rollbackError.message);
  }
  throw error;
} finally {
  await client.end();
}

Ejecútalo en la misma base de datos local existente:

PGDATABASE=postgres node verify.mjs

new Client() lee las variables de entorno de conexión de PostgreSQL. Conserva los ajustes de conexión que ya necesites para tu servidor local; cambiar solo el nombre de la base de datos no configura otro servidor. Conexión

Si falla una aserción, el programa entra en la ruta de reversión y muestra un error. El mensaje de éxito aparece solo después de que pasan todas las comprobaciones de resultados. El programa también revierte los cambios si todo sale bien, para que la demostración pueda repetirse.

La sintaxis de JavaScript se comprobó durante la preparación. El entorno de investigación no tenía disponible un ejecutable de PostgreSQL, por lo que los ejemplos de base de datos se contrastaron con la documentación en lugar de ejecutarse; las tablas anteriores muestran los resultados previstos.

¿Qué cambia con las claves foráneas compuestas?

El ejemplo principal usa claves foráneas de una sola columna. Una clave foránea compuesta plantea otra decisión: ¿la eliminación debería borrar el valor de todas las columnas que hacen referencia o solo el de la columna opcional? PostgreSQL 15 introdujo listas de columnas para las acciones ON DELETE SET de las claves foráneas; en versiones anteriores, la acción afectaba a todas las columnas de la clave foránea. Notas de la versión de PostgreSQL 15

Por ejemplo, una referencia compuesta a un artículo podría contener un identificador obligatorio de edición y un identificador opcional de artículo relacionado. En PostgreSQL 15 o posterior, ON DELETE SET NULL (related_article_id) puede conservar el identificador de edición y borrar la referencia opcional al artículo. Sin esa lista de columnas, SET NULL afecta a ambas columnas. Esta sintaxis es una extensión de PostgreSQL y está disponible para ON DELETE, no para ON UPDATE. CREATE TABLE

La anulación parcial también requiere una regla de coincidencia compatible. El valor predeterminado MATCH SIMPLE permite una clave foránea compuesta que contiene un valor nulo; MATCH FULL rechaza una combinación de valores nulos y no nulos en las columnas que hacen referencia. Por lo tanto, conservar un identificador obligatorio de edición y anular solo el identificador del artículo relacionado es incompatible con MATCH FULL. Restricciones

En PostgreSQL 14, usa un esquema con una referencia opcional independiente de una sola columna, como en el ejemplo principal, cuando represente correctamente la relación. Si la relación compuesta debe mantenerse, borra explícitamente su referencia opcional antes de eliminar la fila referenciada, usando una transacción. Esa secuencia se gestiona desde la aplicación y no usa la sintaxis de lista de columnas, que no está disponible.

En PostgreSQL 18 existe otro límite documentado: las claves foráneas temporales que usan PERIOD no admiten CASCADE ni SET NULL. Las claves foráneas normales que se muestran aquí son la opción compatible para estas acciones de eliminación cuando la relación no incluye cobertura de duración. CREATE TABLE

¿Cómo diagnosticas un resultado inesperado?

Sigue la ruta de eliminación para diagnosticar el problema:

  1. Confirma la tabla que hace referencia. Inspecciona \d demo_articles y \d demo_sections en la sesión que contiene las tablas. Comprueba las acciones reales de las claves foráneas en vez de inferirlas por los nombres de las columnas. PostgreSQL documenta \d como forma de inspeccionar los detalles de una tabla y los nombres de sus restricciones. Modificación de tablas
  2. Comprueba la nulabilidad y las demás restricciones. Si falla la eliminación de una colección, confirma que la columna que se va a borrar admite valores nulos y que las demás restricciones aceptan la fila resultante. SET NULL no las omite. Restricciones
  3. Inspecciona todas las claves foráneas pertinentes. Puede haber otra relación con una acción diferente. Si se omite una acción, el valor predeterminado es NO ACTION, que puede impedir la eliminación mientras queden referencias. CREATE TABLE
  4. Consulta la tabla base antes de interpretar una unión. Si el artículo todavía existe allí, inspecciona las uniones y los filtros que podrían omitir su nueva referencia nula. Expresiones de tabla
  5. Mantén las sentencias de la transacción en un solo cliente. En una aplicación con un grupo de conexiones, adquiere un cliente para toda la transacción. node-postgres aconseja explícitamente no usar pool.query() para las sentencias de una transacción. Transacciones

Para este modelo de artículos, la prueba de aceptación es concreta: eliminar la colección 1 deja intactos el artículo 101 y sus secciones, mientras que eliminar el artículo 101 deja solo la sección del artículo 102. Mantén juntas esas dos comprobaciones al adaptar el esquema para que el comportamiento previsto de conservación y eliminación siga siendo visible.

Fuentes