Tenemos una relación CLIENTES y sabemos distinguir su esquema de sus datos. Ahora vamos a guardar pedidos. Podríamos escribir cualquier identificador de cliente y confiar en que quien introduzca los datos no se equivoque. Pero una base de datos debería ayudarnos a mantener esa coherencia, no limitarse a esperar buena suerte.
Seguimos con la tienda ficticia de la primera parte. Esta es una extensión posible de PEDIDOS:
| pedido_id | cliente_id | importe | estado | fecha_envio |
|---|---|---|---|---|
| 101 | 1 | 120.00 | pagado | 2026-09-03 |
| 102 | 1 | 180.00 | pagado | NULL |
| 103 | 2 | 40.00 | pagado | 2026-09-04 |
| 104 | 3 | 150.00 | cancelado | NULL |
| 105 | 3 | 110.00 | pagado | NULL |
La relación tiene grado 5 y cardinalidad 5. Su esquema es PEDIDOS(pedido_id, cliente_id, importe, estado, fecha_envio). En este ejemplo permitimos pedidos aún no enviados y pedimos que cada pedido pertenezca a un cliente registrado.
La clave foránea: señalar un cliente que existe
pedido_id identifica el pedido. cliente_id tiene otro papel: indicar quién lo ha realizado. Lo definimos como clave foránea, también llamada clave ajena, que referencia la clave primaria de CLIENTES.
Que el valor 1 se repita en varios pedidos no es un error. Ana puede comprar más de una vez. Una clave foránea no tiene por qué ser única en la relación que la contiene. La unicidad corresponde aquí a pedido_id.
La conexión se expresa mediante valores, no por la posición de las filas ni mediante una flecha que guardamos dentro de la celda. El pedido 101 contiene cliente_id = 1; buscamos ese identificador en CLIENTES y encontramos a Ana.
Una clave foránea puede tener varios atributos y puede referenciar la propia relación. Por ejemplo, responsable_id en EMPLEADOS podría apuntar a otro empleado. Cuando la clave referenciada es compuesta, la referencia también necesita el conjunto correspondiente de atributos, con dominios compatibles. En SQL, además de una clave primaria, puede referenciar una clave con una restricción de unicidad adecuada.
Cuatro comprobaciones que no hacen lo mismo
La integridad exige que los datos respeten las condiciones del modelo y las reglas que hemos definido para lo que representan. No garantiza que el dato sea verdadero: un importe equivocado de 120 euros puede pasar todas las restricciones. Sí permite rechazar contradicciones que sabemos identificar.
Unicidad de la clave primaria
No puede haber dos pedidos con pedido_id = 101, aunque sus importes sean distintos. La regla protege la identificación: cuando buscamos el pedido 101, no deberíamos tener que adivinar a cuál nos referimos.
En una clave compuesta se comprueba la combinación, no cada atributo por separado. En LINEAS_PEDIDO puede repetirse pedido_id y también numero_linea, siempre que no se repita la pareja completa.
Integridad de entidad
Ningún atributo de la clave primaria puede ser nulo. Un pedido con identificador desconocido no tiene una identidad completa dentro de nuestro modelo.
Unicidad e integridad de entidad están relacionadas, pero responden a problemas diferentes: una impide dos identidades iguales; la otra impide una identidad incompleta. Una declaración SQL PRIMARY KEY exige ambas.
Integridad referencial
Un pedido con cliente_id = 99 sería inválido si no existe ese cliente. La clave foránea debe apuntar a una tupla existente, o admitir ausencia de referencia cuando el diseño permita valores nulos. En nuestra tienda la pertenencia a un cliente es obligatoria, así que esa columna tampoco admite NULL.
No basta con comprobarlo al crear el pedido. También debemos mantener la regla si cambiamos su cliente, borramos un cliente referenciado o modificamos su identificador. De lo contrario, una referencia que ayer era válida podría quedar rota mañana.
Integridad de dominio
Los valores no nulos deben pertenecer al dominio de su atributo. estado = 'azul' no entra en los tres estados permitidos; importe = -20 queda fuera si definimos el dominio como importes no negativos.
Hay además una cuestión de significado: las operaciones deben ser apropiadas para los dominios. Un identificador de cliente y un importe pueden almacenarse como números, pero no tiene sentido compararlos para decidir quién ha comprado más. Los gestores detectan muchas incompatibilidades de tipos, pero no necesariamente todas las incompatibilidades semánticas entre dominios definidos por el usuario. Que una expresión se pueda ejecutar no significa que esté bien planteada.
Las reglas del modelo y las reglas de esta tienda
No todas las restricciones tienen el mismo origen. La identidad y las referencias forman parte de las reglas generales del modelo. Decidir que un pedido no puede tener un importe negativo o que un pedido cancelado no debe tener fecha de envío es una regla de nuestro caso concreto: una restricción de integridad de usuario, también llamada regla de negocio.
En otro sistema, un importe negativo podría representar una devolución. Y una cancelación posterior al envío podría estar permitida. No hay que convertir las decisiones de nuestro ejemplo en leyes universales.
Una misma regla puede expresarse de distintas formas. «Importe no negativo» puede formar parte de un dominio definido por el usuario o imponerse mediante una restricción sobre la columna. Lo importante es que la base de datos la compruebe, no solo que quede escrita en un comentario del código.
NULL no es una fecha inventada
En la tabla, fecha_envio = NULL indica que no tenemos una fecha de envío registrada. Puede ser porque todavía no se ha enviado o porque, al estar cancelado, el envío no sea aplicable. Un único marcador no distingue por sí solo todas esas situaciones; por eso también guardamos el estado.
NULL no significa cero ni texto vacío, y no es un valor ordinario del dominio de fechas. Es un marcador de información ausente. Si elegimos una fecha ficticia, como el primer día del año, estaremos afirmando algo que no sabemos y que después puede contaminar un cálculo.
En SQL se comprueba con IS NULL, no con = NULL. Las comparaciones ordinarias con información desconocida pueden dar un resultado desconocido, y WHERE solo conserva las filas cuya condición es verdadera.
Algo menos evidente: CHECK (importe >= 0) no basta para hacer obligatorio el importe, porque un CHECK de PostgreSQL también admite un resultado desconocido. Necesitamos NOT NULL por separado. La documentación de restricciones de PostgreSQL permite comprobar estos matices y las opciones de claves foráneas.
Qué hacemos cuando se quiere borrar a Ana
Ana tiene los pedidos 101 y 102. Si la eliminamos de CLIENTES sin hacer nada más, ambos quedarían apuntando a un cliente inexistente. El diseño debe decidir qué comportamiento corresponde al significado de los datos.
| Política | Qué ocurre al borrar el cliente | Cuándo podría encajar |
|---|---|---|
| Restricción | Se rechaza el borrado mientras tenga pedidos que lo referencien. | Queremos conservar pedidos y su referencia. |
| Cascada | Se borran también los pedidos que lo referencian. | Los registros dependientes deben desaparecer con el principal. |
| Anulación | Los pedidos permanecen, pero su referencia pasa a NULL. |
Permitimos pedidos sin cliente asociado. |
La anulación no encaja con nuestra columna obligatoria. Y usar cascada para pedidos históricos podría eliminar información que necesitamos conservar. «Cascada» no es una opción cómoda que podamos elegir sin pensar en sus consecuencias.
Las políticas también se pueden definir para una modificación de la clave referenciada. Por ejemplo, restringir el borrado y propagar un cambio de identificador son decisiones compatibles. Cambiar una clave no obliga a tratarlo igual que borrar la entidad.
Llevar estas decisiones a SQL
Este pequeño esquema usa sintaxis de PostgreSQL. Incluye la regla de nuestra tienda que prohíbe una fecha de envío en pedidos cancelados; no intenta describir todos los casos de un comercio real.
CREATE TABLE clientes (
cliente_id INTEGER PRIMARY KEY CHECK (cliente_id > 0),
nombre TEXT NOT NULL CHECK (trim(nombre) <> ''),
email TEXT NOT NULL UNIQUE,
ciudad TEXT NOT NULL
);
CREATE TABLE pedidos (
pedido_id INTEGER PRIMARY KEY CHECK (pedido_id > 0),
cliente_id INTEGER NOT NULL,
importe NUMERIC(10, 2) NOT NULL CHECK (importe >= 0),
estado TEXT NOT NULL
CHECK (estado IN ('pendiente', 'pagado', 'cancelado')),
fecha_envio DATE,
FOREIGN KEY (cliente_id) REFERENCES clientes(cliente_id)
ON DELETE RESTRICT ON UPDATE CASCADE,
CHECK (estado <> 'cancelado' OR fecha_envio IS NULL)
);
La clave primaria aporta unicidad y ausencia de nulos. UNIQUE y NOT NULL conservan la candidatura del correo bajo las reglas del ejemplo, aunque no validan su formato. Los CHECK estrechan los valores admitidos. La clave foránea impide pedidos huérfanos. Cada declaración tiene un trabajo concreto; no son adornos para que el esquema parezca más completo.
Y ahora sí: preguntar a los datos
El modelo relacional no se queda en la estructura y la integridad. También contempla la manipulación: insertar, borrar y modificar tuplas, y consultar información a partir de las relaciones.
El álgebra relacional describe consultas mediante operaciones que reciben relaciones y producen otra relación. Esa propiedad se llama cierre relacional: podemos utilizar un resultado como entrada del siguiente paso.
La selección elige las tuplas que cumplen una condición. La proyección conserva determinados atributos. Una combinación o join conecta tuplas que satisfacen una condición entre relaciones. Por ejemplo, seleccionamos pedidos pagados de más de 100 euros, los combinamos con CLIENTES por su identificador y proyectamos identificador y nombre. Con nuestros datos obtenemos a Ana y Carla.
También podemos unir conjuntos compatibles, obtener su intersección o calcular una diferencia. Para saber qué clientes no tienen pedidos, restamos los identificadores presentes en PEDIDOS de los identificadores de CLIENTES: queda Diego. No restamos tablas completas con esquemas diferentes, sino relaciones comparables.
Todo esto resulta menos abstracto cuando cada palabra tiene un referente: sabemos qué es una tupla, qué significa su identificador y qué reglas protegen su conexión. Antes de escribir una consulta complicada, probaría a explicar la tabla con esas ideas. Si todavía no sé qué representa una fila o qué identifica realmente su clave, el problema no se arreglará añadiendo otro JOIN.

¿Te ha resultado útil? Si te apetece apoyar este espacio, puedes invitarme a un café.
Invítame a un café Apoyo voluntario a través de PayPal. Tú eliges el importe.

