Saltar al contenido

013 — Independencia de datos y los tres niveles de esquema

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

Programa · Parte 01 · ← Anterior · Siguiente →

Parte 01 — Fundamentos, sistemas y método · Fundamentos · 3 horas estimadas · motores postgresql, sqlite · laboratorio labs/01-sql-foundations · 3 fuentes.

Conceptos centrales: esquema conceptual · esquema físico · vista externa · independencia lógica

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

De qué trata esta clase

Los tres niveles de esquema —externo, conceptual y físico— y por qué separarlos es lo que permite añadir un índice, particionar una tabla o dividir una entidad sin reescribir las aplicaciones. La independencia lógica que se define aquí es el fundamento técnico de las migraciones sin caída de la parte 11.

flowchart LR
    C["🗄️ Clase 013"]
    C --> K1["esquema conceptual"]
    C --> K2["esquema físico"]
    C --> K3["vista externa"]
    C --> K4["independencia lógica"]
    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
011 Qué resuelve un sistema de bases de datos y qué no persistencia · concurrencia · integridad · recuperación · independencia de datos

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
esquema conceptual La descripción del qué: entidades, atributos y relaciones del dominio, sin decir cómo se guardan. Es el nivel en el que se discute con quien conoce el negocio. se introduce aquí
esquema físico Cómo se materializan realmente los datos: ficheros, páginas, índices, particiones, compresión. Debe poder cambiar —añadir un índice, particionar una tabla— sin que ninguna consulta se reescriba. se introduce aquí
vista externa La porción del esquema que ve cada aplicación o cada rol, normalmente mediante vistas. Permite dar acceso a lo necesario y solo a eso, y absorber cambios del esquema sin romper a quien consulta. se introduce aquí
independencia lógica Poder cambiar el esquema conceptual —dividir una tabla, renombrar una columna— sin romper las aplicaciones, apoyándose en vistas que preservan el contrato anterior. Es más difícil de lograr que la independencia física y es la base técnica de las migraciones sin caída. se introduce aquí

Propósito

Entender la idea que hizo posible la industria de las bases de datos: separar qué datos existen de cómo se almacenan. Sin independencia de datos, cada cambio de índice obligaría a reescribir la aplicación.

Resultados de aprendizaje

Al terminar podrás:

  1. Describir los tres niveles de esquema y qué se declara en cada uno.
  2. Distinguir independencia física de independencia lógica con ejemplos propios.
  3. Explicar qué aportó exactamente Codd (1970) frente a los sistemas jerárquicos y de red.
  4. Identificar en tu propio código las fugas de independencia más comunes.
  5. Usar vistas como capa externa y conocer sus límites.

Fundamentos

El argumento de Codd

Antes de 1970, los sistemas de gestión exponían al programador la estructura física: para recorrer datos había que seguir punteros entre registros en el orden en que estaban guardados. Cambiar el almacenamiento significaba reescribir programas.

Codd propuso exponer los datos como relaciones —conjuntos de tuplas— y acceder a ellos por valor, nunca por posición ni por puntero. La consecuencia práctica está en el propio título del artículo: large shared data banks. Compartidos significa que muchos programas distintos, escritos en momentos distintos, usan los mismos datos; si cada uno dependiera de la disposición física, ninguno podría evolucionar.

La independencia de datos no es una comodidad: es la condición para que un esquema sobreviva a las aplicaciones que lo usan.

Los tres niveles

La arquitectura de tres esquemas (formulada por el comité ANSI/SPARC y recogida en Silberschatz) separa:

Nivel Qué describe Quién lo define Ejemplo
Externo (vistas) Lo que ve cada grupo de usuarios Diseñador de la aplicación VIEW notas_publicas sin el RUT
Conceptual (lógico) Qué entidades, atributos y reglas existen Modelador de datos Tablas, claves, restricciones
Interno (físico) Cómo se guarda y se accede Motor y administrador Páginas, B-Tree, particiones, compresión

De ahí salen dos independencias distintas, y conviene no confundirlas:

flowchart TD
    U1["Aplicación A"] --> V1["Vista: notas_publicas"]
    U2["Aplicación B"] --> V2["Vista: ficha_docente"]
    V1 --> C["Esquema conceptual<br/>students, courses, enrollments"]
    V2 --> C
    C --> F["Esquema interno<br/>páginas, B-Tree, particiones"]
    F --> D[("Almacenamiento")]
    C -. "independencia lógica<br/>(parcial)" .-> V1
    F -. "independencia física<br/>(casi total)" .-> C

Dónde se rompe en la práctica

Date insiste en un punto incómodo: SQL debilita la independencia que el modelo relacional prometía. Las fugas más habituales:

Ejemplo trabajado

Partimos de una tabla que mezcla dos conceptos:

CREATE TABLE students (
  id       INTEGER PRIMARY KEY,
  nombre   TEXT NOT NULL,
  email    TEXT,
  telefono TEXT
);

Un requisito nuevo pide varios contactos por estudiante. El cambio conceptual correcto es dividir:

CREATE TABLE student_contacts (
  student_id INTEGER NOT NULL REFERENCES students(id),
  tipo       TEXT NOT NULL CHECK (tipo IN ('email','telefono')),
  valor      TEXT NOT NULL,
  PRIMARY KEY (student_id, tipo, valor)
);

Sin capa externa, cada cliente que hacía SELECT id, nombre, email FROM students se rompe. Con capa externa, no:

CREATE VIEW students_v1 AS
SELECT s.id,
       s.nombre,
       (SELECT c.valor FROM student_contacts c
         WHERE c.student_id = s.id AND c.tipo = 'email'  LIMIT 1) AS email,
       (SELECT c.valor FROM student_contacts c
         WHERE c.student_id = s.id AND c.tipo = 'telefono' LIMIT 1) AS telefono
FROM students s;

Los clientes antiguos siguen funcionando contra students_v1; los nuevos usan las tablas reales. Aquí está el límite honesto: la vista es legible pero no escribible sin ayuda. Un UPDATE students_v1 SET email = ... no tiene traducción única, porque la vista pierde información sobre cuál de los contactos actualizar. Para conseguir independencia lógica también en escritura hace falta un disparador INSTEAD OF que declare esa decisión de forma explícita.

Traza del efecto: si tres aplicaciones consumían la tabla original, dividir sin vista genera 3 despliegues coordinados; dividir con vista genera 1 despliegue de base de datos y 3 migraciones independientes, cada una a su ritmo. Ese es todo el valor de la capa externa (y el fundamento de las migraciones sin caída de la clase 049).

Comparación

Cambio ¿Rompe a los clientes sin capa externa? ¿Con vista de compatibilidad?
Crear un índice No No
Particionar una tabla No No
Renombrar una columna No
Dividir una tabla en dos No, en lectura
Cambiar el tipo de una columna Depende de la conversión
Añadir una columna Solo si se usa SELECT * No

Errores frecuentes

  1. «La independencia de datos es total.» La física casi lo es; la lógica solo en lectura y con trabajo explícito.
  2. «Las vistas son lentas por definición.» El motor las expande en la fase de reescritura; una vista simple no añade coste. Lo que puede ser lento es la consulta que hay dentro.
  3. «El orden de las filas se mantiene.» No existe orden sin ORDER BY. Cualquier código que dependa de él es un fallo latente.
  4. «El nivel externo es cosmético.» Es el mecanismo con el que se cambia el esquema sin coordinar despliegues, y la base del control de acceso por columna.
  5. «Codd inventó SQL.» Codd definió el modelo relacional; SQL llegó después (System R) y se apartó del modelo en varios puntos, empezando por permitir tablas con filas duplicadas.

De la clase a la operación

Todo esquema de larga vida termina necesitando cambiar mientras hay clientes conectados. La diferencia entre un cambio de diez minutos y una madrugada completa es si la capa externa existía desde el principio. Es una decisión de diseño barata al inicio y carísima de añadir después.

Reto de transferencia

Sobre el esquema del repositorio:

  1. Propón un cambio conceptual real (dividir, renombrar o extraer una entidad).
  2. Escribe la vista que preserva la interfaz anterior y demuestra con una consulta que el cliente antiguo sigue funcionando.
  3. Documenta qué operación de escritura deja de funcionar y qué haría falta para restaurarla.
  4. Estima cuántos despliegues coordinados evita la vista.

Preguntas de evaluación

  1. Da un cambio de tu propio código que rompió clientes y clasifícalo: ¿fue una fuga de independencia física o lógica?
  2. ¿Por qué SELECT * es una dependencia del nivel externo respecto del conceptual?
  3. Una vista con GROUP BY no es actualizable. Explica por qué en términos de información perdida.
  4. ¿Qué garantiza y qué no garantiza la independencia física cuando se cambia un índice B-Tree por uno hash?

🌐 El mismo problema en cada motor

Caso: Cambiar la forma física de los datos sin tocar la consulta de la aplicación

La aplicación consulta siempre lo mismo: SELECT ... FROM panel_inscripciones. Debajo, la tabla cambia de forma —el estado deja de ser un texto repetido en cada fila y pasa a ser un código con su tabla de referencia—, y la vista absorbe el cambio. La consulta de la aplicación no se toca y devuelve exactamente las mismas filas antes y después.

Eso es la independencia lógica de datos: la vista es la frontera entre el esquema externo que la aplicación ve y el esquema conceptual que el administrador puede reorganizar.

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

estudiante estado
Ada activa
Grace retirada
Linus completada

El contrato vive en motores.yaml y lo comprueba python scripts/verificar_equivalencia.py --clase 013: 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
MySQL servicio código doc oficial
MongoDB servicio código doc oficial
Apache Cassandra no doc oficial
Redis no 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_createview.html
-- nota: la vista es el esquema externo; las dos tablas de debajo son el
--       conceptual. Cambia el segundo sin tocar el primero.

-- === preparacion ===
-- 1. El esquema conceptual de partida: el estado es un texto repetido en cada fila.
CREATE TABLE inscripciones_v1 (
    estudiante TEXT NOT NULL,
    estado     TEXT NOT NULL
);
INSERT INTO inscripciones_v1 (estudiante, estado) VALUES
    ('Ada', 'activa'), ('Linus', 'completada'), ('Grace', 'retirada');

-- 2. El esquema externo: lo unico que la aplicacion conoce.
CREATE VIEW panel_inscripciones AS
    SELECT estudiante, estado FROM inscripciones_v1;

-- 3. El administrador reorganiza: el estado pasa a codigo con tabla de referencia.
CREATE TABLE estados (
    codigo INTEGER PRIMARY KEY,
    nombre TEXT NOT NULL
);
INSERT INTO estados (codigo, nombre) VALUES (1, 'activa'), (2, 'completada'), (3, 'retirada');

CREATE TABLE inscripciones_v2 (
    estudiante    TEXT NOT NULL,
    estado_codigo INTEGER NOT NULL REFERENCES estados(codigo)
);
INSERT INTO inscripciones_v2 (estudiante, estado_codigo)
SELECT i.estudiante, e.codigo
FROM inscripciones_v1 i
JOIN estados e ON e.nombre = i.estado;

-- 4. La vista absorbe el cambio. La aplicacion no se entera.
DROP VIEW panel_inscripciones;
DROP TABLE inscripciones_v1;
CREATE VIEW panel_inscripciones AS
    SELECT i.estudiante, e.nombre AS estado
    FROM inscripciones_v2 i
    JOIN estados e ON e.codigo = i.estado_codigo;

-- === consulta ===
-- Exactamente la misma consulta que antes del cambio: ni una letra distinta.
SELECT estudiante, estado FROM panel_inscripciones ORDER BY estudiante;

DuckDB · implementaciones/duckdb/consulta.sql

verificado — se ejecuta en CI sin servicios

-- motor: duckdb
-- doc: https://duckdb.org/docs/stable/sql/statements/create_view.html
-- nota: la misma frontera sirve para cambiar de tabla a archivo Parquet sin
--       que la consulta de la aplicacion cambie.

-- === preparacion ===
-- 1. El esquema conceptual de partida: el estado es un texto repetido en cada fila.
CREATE TABLE inscripciones_v1 (
    estudiante VARCHAR NOT NULL,
    estado     VARCHAR NOT NULL
);
INSERT INTO inscripciones_v1 (estudiante, estado) VALUES
    ('Ada', 'activa'), ('Linus', 'completada'), ('Grace', 'retirada');

-- 2. El esquema externo: lo unico que la aplicacion conoce.
CREATE VIEW panel_inscripciones AS
    SELECT estudiante, estado FROM inscripciones_v1;

-- 3. El administrador reorganiza: el estado pasa a codigo con tabla de referencia.
CREATE TABLE estados (
    codigo INTEGER PRIMARY KEY,
    nombre VARCHAR NOT NULL
);
INSERT INTO estados (codigo, nombre) VALUES (1, 'activa'), (2, 'completada'), (3, 'retirada');

CREATE TABLE inscripciones_v2 (
    estudiante    VARCHAR NOT NULL,
    estado_codigo INTEGER NOT NULL
);
INSERT INTO inscripciones_v2 (estudiante, estado_codigo)
SELECT i.estudiante, e.codigo
FROM inscripciones_v1 i
JOIN estados e ON e.nombre = i.estado;

-- 4. La vista absorbe el cambio. La aplicacion no se entera.
DROP VIEW panel_inscripciones;
DROP TABLE inscripciones_v1;
CREATE VIEW panel_inscripciones AS
    SELECT i.estudiante, e.nombre AS estado
    FROM inscripciones_v2 i
    JOIN estados e ON e.codigo = i.estado_codigo;

-- === consulta ===
-- Exactamente la misma consulta que antes del cambio: ni una letra distinta.
SELECT estudiante, estado FROM panel_inscripciones ORDER BY estudiante;

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-createview.html
-- nota: en produccion el paso 4 se hace con CREATE OR REPLACE VIEW, que
--       sustituye la definicion de forma atomica y sin dejar un instante en
--       el que la vista no exista.

DROP VIEW IF EXISTS panel_inscripciones;
DROP TABLE IF EXISTS inscripciones_v1, inscripciones_v2, estados;

-- === preparacion ===
-- 1. El esquema conceptual de partida: el estado es un texto repetido en cada fila.
CREATE TABLE inscripciones_v1 (
    estudiante text NOT NULL,
    estado     text NOT NULL
);
INSERT INTO inscripciones_v1 (estudiante, estado) VALUES
    ('Ada', 'activa'), ('Linus', 'completada'), ('Grace', 'retirada');

-- 2. El esquema externo: lo unico que la aplicacion conoce.
CREATE VIEW panel_inscripciones AS
    SELECT estudiante, estado FROM inscripciones_v1;

-- 3. El administrador reorganiza: el estado pasa a codigo con tabla de referencia.
CREATE TABLE estados (
    codigo integer PRIMARY KEY,
    nombre text NOT NULL
);
INSERT INTO estados (codigo, nombre) VALUES (1, 'activa'), (2, 'completada'), (3, 'retirada');

CREATE TABLE inscripciones_v2 (
    estudiante    text NOT NULL,
    estado_codigo integer NOT NULL REFERENCES estados(codigo)
);
INSERT INTO inscripciones_v2 (estudiante, estado_codigo)
SELECT i.estudiante, e.codigo
FROM inscripciones_v1 i
JOIN estados e ON e.nombre = i.estado;

-- 4. La vista absorbe el cambio. La aplicacion no se entera.
DROP VIEW panel_inscripciones;
DROP TABLE inscripciones_v1;
CREATE VIEW panel_inscripciones AS
    SELECT i.estudiante, e.nombre AS estado
    FROM inscripciones_v2 i
    JOIN estados e ON e.codigo = i.estado_codigo;

-- === consulta ===
-- Exactamente la misma consulta que antes del cambio: ni una letra distinta.
SELECT estudiante, estado FROM panel_inscripciones ORDER BY estudiante;

MySQL · implementaciones/mysql/consulta.sql

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

-- motor: mysql
-- doc: https://dev.mysql.com/doc/refman/8.4/en/views.html
-- nota: esta vista es una simple proyeccion con reunion, asi que MySQL usa el
--       algoritmo MERGE y no materializa nada. Con agregacion o UNION caeria
--       en TEMPTABLE y perderia los indices de debajo.

DROP VIEW IF EXISTS panel_inscripciones;
DROP TABLE IF EXISTS inscripciones_v1;
DROP TABLE IF EXISTS inscripciones_v2;
DROP TABLE IF EXISTS estados;

-- === preparacion ===
-- 1. El esquema conceptual de partida: el estado es un texto repetido en cada fila.
CREATE TABLE inscripciones_v1 (
    estudiante VARCHAR(50) NOT NULL,
    estado     VARCHAR(50) NOT NULL
);
INSERT INTO inscripciones_v1 (estudiante, estado) VALUES
    ('Ada', 'activa'), ('Linus', 'completada'), ('Grace', 'retirada');

-- 2. El esquema externo: lo unico que la aplicacion conoce.
CREATE VIEW panel_inscripciones AS
    SELECT estudiante, estado FROM inscripciones_v1;

-- 3. El administrador reorganiza: el estado pasa a codigo con tabla de referencia.
CREATE TABLE estados (
    codigo INT PRIMARY KEY,
    nombre VARCHAR(50) NOT NULL
);
INSERT INTO estados (codigo, nombre) VALUES (1, 'activa'), (2, 'completada'), (3, 'retirada');

CREATE TABLE inscripciones_v2 (
    estudiante    VARCHAR(50) NOT NULL,
    estado_codigo INT NOT NULL REFERENCES estados(codigo)
);
INSERT INTO inscripciones_v2 (estudiante, estado_codigo)
SELECT i.estudiante, e.codigo
FROM inscripciones_v1 i
JOIN estados e ON e.nombre = i.estado;

-- 4. La vista absorbe el cambio. La aplicacion no se entera.
DROP VIEW panel_inscripciones;
DROP TABLE inscripciones_v1;
CREATE VIEW panel_inscripciones AS
    SELECT i.estudiante, e.nombre AS estado
    FROM inscripciones_v2 i
    JOIN estados e ON e.codigo = i.estado_codigo;

-- === consulta ===
-- Exactamente la misma consulta que antes del cambio: ni una letra distinta.
SELECT estudiante, estado FROM panel_inscripciones ORDER BY estudiante;

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/core/views/
// nota: una vista es una tuberia de agregacion con nombre. Aqui el cambio
//       fisico es el mismo: el estado deja de ser una cadena repetida y pasa a
//       ser un codigo con su coleccion de referencia.

// === preparacion ===
db.panel_inscripciones.drop();
db.inscripciones_v1.drop();
db.inscripciones_v2.drop();
db.estados.drop();

// 1. Esquema de partida.
db.inscripciones_v1.insertMany([
  { estudiante: "Ada", estado: "activa" },
  { estudiante: "Linus", estado: "completada" },
  { estudiante: "Grace", estado: "retirada" },
]);

// 2. Esquema externo: lo unico que la aplicacion conoce.
db.createView("panel_inscripciones", "inscripciones_v1", [
  { $project: { _id: 0, estudiante: 1, estado: 1 } },
]);

// 3. Reorganizacion fisica.
db.estados.insertMany([
  { _id: 1, nombre: "activa" },
  { _id: 2, nombre: "completada" },
  { _id: 3, nombre: "retirada" },
]);
db.inscripciones_v2.insertMany([
  { estudiante: "Ada", estado_codigo: 1 },
  { estudiante: "Linus", estado_codigo: 2 },
  { estudiante: "Grace", estado_codigo: 3 },
]);

// 4. La vista absorbe el cambio.
db.panel_inscripciones.drop();
db.inscripciones_v1.drop();
db.createView("panel_inscripciones", "inscripciones_v2", [
  { $lookup: { from: "estados", localField: "estado_codigo",
               foreignField: "_id", as: "e" } },
  { $unwind: "$e" },
  { $project: { _id: 0, estudiante: 1, estado: "$e.nombre" } },
]);

// === consulta ===
// La misma consulta de siempre, contra el mismo nombre de siempre.
db.panel_inscripciones
  .find()
  .sort({ estudiante: 1 })
  .forEach((d) => print(d.estudiante + "|" + d.estado));

Los que no resuelven este caso — y qué se hace en su lugar

Descartar un motor con un argumento es tan formativo como usarlo. Ninguna de estas filas dice que el motor sea peor: dice que este problema no es el suyo.

Motor Por qué no Qué se hace en su lugar Fuente
Apache Cassandra No hay vistas que reescriban una consulta. Existen vistas materializadas, pero mantienen una copia real de los datos y su propia documentación advierte de que se consideran experimentales por los problemas de coherencia entre la tabla base y la vista. Escribir a mano la tabla que la consulta necesita y mantenerla desde la aplicación, aceptando que el cambio de forma física obliga a migrar datos, no solo a redefinir un objeto. doc
Redis No existe una capa lógica sobre los datos: la clave es la forma física. Cambiar cómo se guarda algo cambia la clave, y todo cliente que la conocía deja de encontrarla. Poner una capa de servicio delante que traduzca nombres estables a claves concretas: la independencia se implementa en el código, no en el almacén. doc

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 01 · ← Anterior · Siguiente →