Saltar al contenido

020 — La relación como conjunto: tuplas, dominios y acceso por valor

🗂️ parte 🎚️ nivel ⏱️ duración 📗 clase

Programa · Parte 03 · ← Anterior · Siguiente →

Parte 03 — Modelo relacional y álgebra · Fundamentos · 3 horas estimadas · motores postgresql, sqlite · laboratorio labs/01-sql-foundations · 3 fuentes.

Conceptos centrales: relación · tupla · dominio · acceso por valor · cierre

En este caso se comparan 5 motores: 5 lo resuelven (5 con el resultado comprobado por máquina) y 0 no, con el motivo escrito.

De qué trata esta clase

La relación como conjunto de tuplas sobre dominios, con dos propiedades que SQL no respeta: no hay orden y no hay duplicados. Entender esa brecha explica de antemano la mitad de las sorpresas del lenguaje, del DISTINCT que hace falta al ORDER BY que no se puede dar por supuesto.

flowchart LR
    C["🗄️ Clase 020"]
    C --> K1["relación"]
    C --> K2["tupla"]
    C --> K3["dominio"]
    C --> K4["acceso por valor"]
    C --> K5["cierre"]
    classDef raiz fill:#0b3d2e,stroke:#3fb950,color:#fff
    class C raiz

Antes de empezar

Esta clase supone que ya trabajaste lo siguiente. Si algo de la última columna no te suena, vuelve a esa clase antes de seguir: aquí se usa sin volver a explicarlo.

# Clase previa Lo que se da por sabido
001 Qué es un dato, un registro y una tabla dato · información · registro · campo · tabla
004 Leer datos: SELECT, WHERE y ORDER BY filtrado · proyección · orden · LIMIT · IS NULL

Vocabulario de la clase

Los términos que siguen se usan más adelante con este significado exacto. La definición completa, con sus términos relacionados, está en el glosario del programa.

Término Qué significa Procedencia
relación En el modelo de Codd, un conjunto de tuplas sobre unos dominios dados. Al ser conjunto no tiene orden ni duplicados —dos propiedades que SQL no respeta, y de ahí nacen la mitad de las sorpresas del lenguaje. se introduce aquí
tupla Un elemento de la relación: una asignación de un valor a cada atributo. No es «una fila en una posición», porque en un conjunto no hay posiciones; se identifica por sus valores, no por dónde está. se introduce aquí
dominio El conjunto de valores admisibles de un atributo, con sus operaciones. Es el concepto del que los tipos de SQL son una aproximación pobre: SQL permite comparar un número de teléfono con un código postal si ambos son enteros. se introduce aquí
acceso por valor En el modelo relacional se llega a un dato por lo que vale, nunca por un puntero o una posición física. Es lo que hace posible la independencia de datos: el motor puede reorganizar el almacenamiento sin invalidar ninguna referencia. se introduce aquí
cierre Propiedad por la que toda operación del álgebra relacional sobre relaciones devuelve una relación. Es lo que permite anidar y componer consultas indefinidamente, y lo que sostiene las vistas y las CTE. se introduce aquí

Propósito

Precisar qué es una relación en el sentido de Codd y en qué se aparta SQL de esa definición. Muchos comportamientos «raros» de SQL —duplicados, orden, nulos— se explican exactamente por ahí.

Resultados de aprendizaje

Al terminar podrás:

  1. Definir relación, tupla, atributo y dominio sin recurrir a «tabla», «fila» y «columna».
  2. Enumerar las cuatro propiedades de una relación que SQL no respeta.
  3. Explicar el acceso por valor y por qué excluye punteros y posiciones.
  4. Justificar la propiedad de cierre y qué habilita.
  5. Detectar en código propio dependencias del orden físico.

Fundamentos

La definición

Dada una lista de dominios D1, …, Dn, una relación es un subconjunto del producto cartesiano D1 × … × Dn. De ahí, por ser un conjunto matemático, se siguen cuatro propiedades:

Propiedad Significado ¿SQL la respeta?
Sin tuplas duplicadas Un conjunto no repite elementos No. Una tabla sin clave admite filas idénticas
Sin orden entre tuplas Un conjunto no está ordenado No del todo: ORDER BY produce una lista, no una relación
Sin orden entre atributos Se accede por nombre No. SELECT * y INSERT sin lista de columnas dependen de la posición
Valores atómicos del dominio Cada celda es un valor del dominio Parcialmente. Admite nulos, que no pertenecen a ningún dominio

Date insiste en que SQL implementa «tablas», no relaciones: una tabla es un multiconjunto (bag) con orden de columnas. Todas las sorpresas de la parte 03 —UNION frente a UNION ALL, COUNT(*) frente a COUNT(col), el resultado de NOT IN con nulos— derivan de esa distancia.

Acceso por valor

Codd exige que todo dato sea localizable por la terna (nombre de relación, valor de clave, nombre de atributo). Nunca por posición física ni por puntero.

Consecuencias que se usan a diario:

Cierre

Todo operador relacional recibe relaciones y devuelve una relación. Eso permite componer sin límite: el resultado de una consulta puede ser la entrada de otra. En SQL se manifiesta en las subconsultas, las CTE y las vistas. Es lo que hace que el lenguaje sea composicional en lugar de un catálogo de comandos.

flowchart LR
    subgraph M["Modelo relacional (Codd)"]
        R1["Relación: conjunto"] --> P1["sin duplicados"]
        R1 --> P2["sin orden"]
        R1 --> P3["acceso por valor"]
        R1 --> P4["cierre"]
    end
    subgraph S["SQL (implementación)"]
        T1["Tabla: multiconjunto"] --> Q1["admite duplicados"]
        T1 --> Q2["orden observable"]
        T1 --> Q3["posición de columnas"]
        T1 --> Q4["cierre conservado"]
    end
    M -- "se aparta en 3 de 4" --> S

Ejemplo trabajado

Creemos una tabla sin clave y observemos las tres desviaciones.

CREATE TABLE t (a INTEGER, b TEXT);
INSERT INTO t VALUES (1,'x'), (1,'x'), (2,'y');
SELECT COUNT(*) FROM t;              -- 3
SELECT COUNT(*) FROM (SELECT DISTINCT a, b FROM t);  -- 2

Si t fuese una relación, ambas consultas darían 2. Dan 3 y 2: t es un multiconjunto. La consecuencia inmediata:

SELECT a, b FROM t
EXCEPT
SELECT 1, 'x';

En SQL estándar EXCEPT elimina duplicados, así que el resultado es (2,'y'): se han borrado las dos filas (1,'x') con una sola tupla. Con EXCEPT ALL el resultado incluiría una (1,'x') superviviente. Dos operadores distintos porque el modelo subyacente no es un conjunto.

Desviación de orden. Sobre el dominio del repositorio:

SELECT nombre FROM students;

El orden que devuelve depende del plan. Si el motor decide un barrido secuencial, sale el orden de inserción; si decide recorrer un índice, sale el orden del índice. Añadir un índice puede cambiar el resultado observado sin cambiar ningún dato. Todo código que dependa de ese orden es un fallo latente que se activa el día que alguien optimiza.

Desviación de posición.

INSERT INTO students VALUES (5, 'Ana');    -- depende del orden de columnas
INSERT INTO students (id, nombre) VALUES (5, 'Ana');  -- acceso por nombre

La primera forma se rompe en silencio si alguien añade una columna en medio. La segunda es la que respeta el acceso por valor.

Traza del riesgo. Un INSERT posicional sobre una tabla de 6 columnas, tras insertar una columna nueva en la posición 3, no falla: desplaza los valores y guarda datos incorrectos con tipos compatibles. El error se descubre en un informe semanas después.

Comparación

Operación Semántica de conjunto Semántica de multiconjunto (SQL)
UNION Sin duplicados UNION sin, UNION ALL con
INTERSECT Sin duplicados INTERSECT / INTERSECT ALL
EXCEPT Sin duplicados EXCEPT / EXCEPT ALL
Proyección Elimina duplicados Los conserva salvo DISTINCT
Conteo Cardinalidad del conjunto COUNT(*) cuenta repeticiones

Errores frecuentes

  1. Suponer que el motor devuelve las filas «en orden». No hay orden sin ORDER BY, y con ORDER BY sobre columna no única el desempate tampoco está definido.
  2. Usar SELECT * en código de producción. Ata el cliente a la posición y al número de columnas.
  3. Proyectar sin pensar en duplicados. SELECT ciudad FROM clientes devuelve la ciudad repetida por cada cliente; casi nunca es lo que se quería.
  4. Confundir NULL con un valor. No pertenece a ningún dominio: es una marca de información ausente (clase 019).
  5. Crear tablas sin clave primaria. Sin ella no hay forma de referirse a una fila concreta ni de borrar un duplicado sin borrar el otro.

De la clase a la operación

Los tres apartamientos de SQL respecto del modelo son la causa de una familia entera de fallos de producción: informes con totales inflados por duplicados, procesos que dependen del orden, migraciones que desplazan columnas. Reconocer la causa común los convierte en un solo problema con una sola disciplina.

Reto de transferencia

  1. Encuentra en un esquema real una tabla sin clave primaria y demuestra con una consulta que contiene duplicados lógicos.
  2. Muestra una consulta de tu código que dependa del orden sin ORDER BY.
  3. Reproduce el efecto de EXCEPT frente a EXCEPT ALL con tus propios datos.
  4. Convierte un INSERT posicional en uno por nombre y explica qué fallo evitaste.

Preguntas de evaluación

  1. Da una consulta cuyo resultado cambie al crear un índice, sin que cambien los datos, y explica por qué.
  2. ¿Por qué COUNT(*) y COUNT(DISTINCT ...) difieren, y qué dice eso del modelo subyacente?
  3. Explica el acceso por valor y por qué prohíbe exponer identificadores de fila físicos.
  4. La propiedad de cierre habilita las CTE. Da una consulta tuya que sería imposible sin ella.

🌐 El mismo problema en cada motor

Caso: Convertir una bolsa de registros en una relación

En el modelo relacional una relación es un conjunto: no tiene filas repetidas y no tiene orden. Lo que las tablas guardan de verdad son bolsas (multisets): admiten repetidos y llegan en el orden que sea.

El caso parte de un registro de accesos con repeticiones —Ada entró dos veces a DB-101, Linus dos veces también— y devuelve el conjunto de pares distintos, ordenado. El DISTINCT es lo que convierte la bolsa en conjunto; el ORDER BY es una decisión de presentación, y por eso hay que escribirlo siempre: sin él, ningún motor está obligado a devolver nada en un orden concreto.

Salida esperada, idéntica en todos los motores que lo resuelven:

estudiante curso
Ada DB-101
Ada SE-201
Linus DB-101

El contrato vive en motores.yaml y lo comprueba python scripts/verificar_equivalencia.py --clase 020: 5 de las 5 implementaciones se ejecutan de verdad y su resultado se compara con esa tabla; el resto se declara como material revisado, no ejecutado.

Motor ¿Resuelve el caso? Nivel de prueba Código Fuente
SQLite núcleo código doc oficial
DuckDB núcleo código doc oficial
PostgreSQL servicio código doc oficial
Redis servicio código doc oficial
MongoDB servicio código doc oficial

Los que resuelven el caso

SQLite · implementaciones/sqlite/consulta.sql

verificado — se ejecuta en CI sin servicios

-- motor: sqlite
-- doc: https://sqlite.org/lang_select.html
-- nota: quitar el ORDER BY no rompe la consulta, y ese es el peligro: devuelve
--       un orden que parece estable hasta que un indice nuevo cambia el plan.

-- === preparacion ===
-- El registro de accesos es una BOLSA: admite repetidos y tiene orden de
-- llegada. Una relacion no es eso.
CREATE TABLE accesos (
    id         INTEGER PRIMARY KEY,
    estudiante TEXT NOT NULL,
    curso      TEXT NOT NULL
);
INSERT INTO accesos (id, estudiante, curso) VALUES
    (1, 'Linus', 'DB-101'),
    (2, 'Ada',   'DB-101'),
    (3, 'Ada',   'DB-101'),
    (4, 'Ada',   'SE-201'),
    (5, 'Linus', 'DB-101');

-- === consulta ===
-- DISTINCT convierte la bolsa en conjunto; ORDER BY impone un orden que la
-- relacion NO tiene: es una decision de presentacion, no del modelo.
SELECT DISTINCT estudiante, curso
FROM accesos
ORDER BY estudiante, curso;

DuckDB · implementaciones/duckdb/consulta.sql

verificado — se ejecuta en CI sin servicios

-- motor: duckdb
-- doc: https://duckdb.org/docs/stable/sql/query_syntax/orderby.html
-- nota: al ejecutar en paralelo por trozos, sin ORDER BY el orden cambia entre
--       ejecuciones de verdad. Es el motor que mejor demuestra que una relacion
--       no tiene orden.

-- === preparacion ===
-- El registro de accesos es una BOLSA: admite repetidos y tiene orden de
-- llegada. Una relacion no es eso.
CREATE TABLE accesos (
    id         INTEGER PRIMARY KEY,
    estudiante VARCHAR NOT NULL,
    curso      VARCHAR NOT NULL
);
INSERT INTO accesos (id, estudiante, curso) VALUES
    (1, 'Linus', 'DB-101'),
    (2, 'Ada',   'DB-101'),
    (3, 'Ada',   'DB-101'),
    (4, 'Ada',   'SE-201'),
    (5, 'Linus', 'DB-101');

-- === consulta ===
-- DISTINCT convierte la bolsa en conjunto; ORDER BY impone un orden que la
-- relacion NO tiene: es una decision de presentacion, no del modelo.
SELECT DISTINCT estudiante, curso
FROM accesos
ORDER BY estudiante, curso;

PostgreSQL · implementaciones/postgresql/consulta.sql

verificado — se ejecuta contra el motor real levantado con docker compose

-- motor: postgresql
-- doc: https://www.postgresql.org/docs/current/sql-select.html
-- nota: la documentacion lo dice sin rodeos: sin ORDER BY el orden de las filas
--       es indeterminado. No es un descuido del motor; es el modelo.

DROP TABLE IF EXISTS accesos;

-- === preparacion ===
-- El registro de accesos es una BOLSA: admite repetidos y tiene orden de
-- llegada. Una relacion no es eso.
CREATE TABLE accesos (
    id         integer PRIMARY KEY,
    estudiante text NOT NULL,
    curso      text NOT NULL
);
INSERT INTO accesos (id, estudiante, curso) VALUES
    (1, 'Linus', 'DB-101'),
    (2, 'Ada',   'DB-101'),
    (3, 'Ada',   'DB-101'),
    (4, 'Ada',   'SE-201'),
    (5, 'Linus', 'DB-101');

-- === consulta ===
-- DISTINCT convierte la bolsa en conjunto; ORDER BY impone un orden que la
-- relacion NO tiene: es una decision de presentacion, no del modelo.
SELECT DISTINCT estudiante, curso
FROM accesos
ORDER BY estudiante, curso;

Redis · implementaciones/redis/consulta.txt

verificado — se ejecuta contra el motor real levantado con docker compose

# motor: redis
# doc: https://redis.io/docs/latest/develop/data-types/sets/
# nota: el conjunto no es una operacion sobre los datos, es el tipo de dato.
#       El precio: el par estudiante-curso hay que serializarlo en una cadena.

# === preparacion ===
FLUSHDB
SADD accesos Linus|DB-101
SADD accesos Ada|DB-101
SADD accesos Ada|DB-101
SADD accesos Ada|SE-201
SADD accesos Linus|DB-101

# === consulta ===
SORT accesos ALPHA

MongoDB · implementaciones/mongodb/consulta.js

verificado — se ejecuta contra el motor real levantado con docker compose

// motor: mongodb
// doc: https://www.mongodb.com/docs/manual/reference/operator/aggregation/group/
// nota: $group por la pareja de campos hace de DISTINCT. El $sort explicito
//       deja claro que el orden se pide; no se hereda del orden de insercion.

// === preparacion ===
db.accesos.drop();
db.accesos.insertMany([
  { estudiante: "Linus", curso: "DB-101" },
  { estudiante: "Ada", curso: "DB-101" },
  { estudiante: "Ada", curso: "DB-101" },
  { estudiante: "Ada", curso: "SE-201" },
  { estudiante: "Linus", curso: "DB-101" },
]);

// === consulta ===
db.accesos
  .aggregate([
    { $group: { _id: { estudiante: "$estudiante", curso: "$curso" } } },
    { $sort: { "_id.estudiante": 1, "_id.curso": 1 } },
  ])
  .forEach((d) => print(d._id.estudiante + "|" + d._id.curso));

Laboratorio

python scripts/validate_repository.py
python labs/01-sql-foundations/run_lab.py

Guarda como evidencia la salida completa, la versión del motor y la semilla o los parámetros usados. Una captura sin comando no es evidencia: no se puede repetir.

Evaluación

Criterio Peso Qué se comprueba
Comprensión conceptual 25 % Explica el mecanismo, no solo el resultado
Ejecución reproducible 25 % Otra persona obtiene lo mismo con las instrucciones dadas
Interpretación basada en evidencia 25 % Cada conclusión se apoya en una salida o una medición
Límites y riesgos declarados 25 % Dice qué no demuestra el ejercicio y qué faltaría en producción

La clase se da por superada cuando la respuesta explica el mecanismo, muestra la salida que la respalda y declara al menos un límite del ejercicio.

Fuentes de esta clase

Todo lo afirmado arriba procede de estas obras. Los identificadores viven en catalog/sources.json y el estado de los enlaces se comprueba con python scripts/check_external_links.py.


Programa · Parte 03 · ← Anterior · Siguiente →