Saltar al contenido

011 — Qué resuelve un sistema de bases de datos y qué no

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

Programa · Parte 01 · ← Anterior · Siguiente →

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

Conceptos centrales: persistencia · concurrencia · integridad · recuperación · independencia de datos

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

De qué trata esta clase

La misma pregunta de la clase 002, ahora con el vocabulario de sistemas: qué garantiza un gestor —persistencia, concurrencia, integridad, recuperación— y, con la misma seriedad, qué no garantiza. La independencia de datos aparece aquí como la idea de Codd que ordena todo lo demás.

flowchart LR
    C["🗄️ Clase 011"]
    C --> K1["persistencia"]
    C --> K2["concurrencia"]
    C --> K3["integridad"]
    C --> K4["recuperación"]
    C --> K5["independencia de datos"]
    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
002 Del archivo y la hoja de cálculo a la base de datos integridad declarada · concurrencia · consulta declarativa · durabilidad
010 El mapa de los motores: seis familias y un criterio familias de motores · modelo de agregado · patrón de acceso · multimodelo

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
persistencia Que el dato siga existiendo cuando el proceso que lo escribió ya no está. Es el requisito mínimo de una base de datos y la única de sus funciones que un archivo también cumple. se introduce aquí
concurrencia Varias sesiones leyendo y escribiendo a la vez sobre los mismos datos. Un archivo compartido no la resuelve: el último en guardar pisa al anterior. Un gestor la resuelve con transacciones, bloqueo o versiones. se introdujo en la 002
integridad Que los datos cumplan siempre las reglas del dominio, incluidas las que ninguna aplicación recordó comprobar. El gestor la sostiene con restricciones declaradas y con transacciones. se introduce aquí
recuperación Volver a un estado correcto después de una caída, descartando lo no confirmado y rehaciendo lo confirmado. Es lo que distingue una base de datos de un archivo que se corrompió a medio escribir. se introduce aquí
independencia de datos Poder cambiar cómo se guardan los datos sin reescribir las aplicaciones que los consultan. Es la idea central del artículo de Codd de 1970 y la razón de que exista un nivel lógico separado del físico. se introduce aquí

Propósito

Establecer qué problemas concretos resuelve un sistema gestor de bases de datos (SGBD) y, sobre todo, cuáles no resuelve. Sin esa demarcación, cualquier archivo con datos se llama «base de datos» y cualquier fallo de diseño se atribuye al motor.

Resultados de aprendizaje

Al terminar podrás:

  1. Enumerar los seis problemas del almacenamiento en archivos planos que motivaron la aparición de los SGBD.
  2. Reproducir una anomalía de actualización perdida y explicar por qué el sistema de archivos no la impide.
  3. Distinguir lo que el motor garantiza de lo que solo permite declarar.
  4. Justificar cuándo no usar un SGBD, con un criterio distinto de «es lo que se usa siempre».
  5. Nombrar la fuente de cada afirmación anterior.

Fundamentos

El problema que existía antes

Silberschatz, Korth y Sudarshan abren Database System Concepts con la lista de defectos del enfoque «un archivo por aplicación». No es historia: cada uno reaparece cuando alguien decide guardar el estado en JSON y arreglarlo después.

Defecto Qué ocurre en la práctica
Redundancia e inconsistencia El mismo dato vive en dos archivos y divergen
Dificultad de acceso Cada consulta nueva exige escribir un programa nuevo
Aislamiento de datos Formatos distintos impiden combinar información
Problemas de integridad La regla «el saldo no puede ser negativo» vive en el código, no en el dato
Problemas de atomicidad Una caída a mitad de una transferencia deja el dinero en ninguna parte
Anomalías de concurrencia Dos procesos escriben a la vez y uno de los cambios desaparece
Problemas de seguridad No hay forma de dar acceso parcial: o se ve el archivo entero o nada

Lo que un SGBD aporta

Un gestor no es «un lugar donde guardar tablas». Es un programa que ofrece cuatro servicios que un sistema de archivos no ofrece:

Hellerstein, Stonebraker y Hamilton describen cómo se implementan esos servicios: gestor de procesos, procesador de consultas, gestor de transacciones y gestor de almacenamiento compartido. Ningún componente es opcional si se quieren las cuatro garantías.

Lo que un SGBD no hace

Aquí se pierde más tiempo del que se cree:

Cuándo no usar un SGBD

Criterios defendibles para quedarse con archivos: los datos son de un solo escritor, caben en memoria, no hay reglas de integridad entre elementos, y perderlos es aceptable (caché, artefactos de compilación, registros efímeros). En cuanto aparezcan dos escritores o una invariante entre dos registros, el argumento se cae.

flowchart TD
    A["Necesito guardar datos"] --> B{"¿Más de un escritor<br/>concurrente?"}
    B -- "No" --> C{"¿Hay invariantes<br/>entre registros?"}
    B -- "Sí" --> G["SGBD: hace falta control<br/>de concurrencia"]
    C -- "No" --> D{"¿Perderlos es<br/>aceptable?"}
    C -- "Sí" --> G
    D -- "Sí" --> E["Archivos: suficiente"]
    D -- "No" --> F{"¿Consultas variadas<br/>o no previstas?"}
    F -- "No" --> H["Archivo + copia verificada"]
    F -- "Sí" --> G

Ejemplo trabajado

Dos cajeros aplican un cargo sobre la misma cuenta, que tiene 1 000 de saldo. Con archivos, cada proceso lee, calcula y escribe:

t0  Cajero A lee saldo         -> 1000
t1  Cajero B lee saldo         -> 1000
t2  Cajero A calcula 1000 - 300 -> 700
t3  Cajero B calcula 1000 - 500 -> 500
t4  Cajero A escribe            -> 700
t5  Cajero B escribe            -> 500

Saldo final: 500. Saldo correcto: 1000 − 300 − 500 = 200. Se perdieron 300 sin que ningún programa fallara ni ningún archivo se corrompiera: los dos cajeros hicieron exactamente lo que su código decía. Es la actualización perdida, la anomalía que Gray formalizaría como violación de aislamiento.

El mismo escenario dentro de un SGBD, con una transacción por cajero y un nivel de aislamiento que detecte el conflicto, tiene tres desenlaces posibles: uno espera al otro y ambos terminan (200), o uno se aborta y se reintenta (200), o el motor informa de un fallo serializable y el cliente decide. Lo que no puede ocurrir es que un cambio confirmado se pierda en silencio.

Comprobación numérica del argumento: con archivos, la ventana de riesgo es el intervalo t0t5. Si cada operación dura 5 ms y llegan 40 cargos por segundo sobre la misma cuenta, la probabilidad de solapamiento no es marginal: es el caso habitual.

Comparación

Dimensión Archivos planos SGBD
Unidad de escritura El archivo o un bloque La transacción
Recuperación tras caída Lo que alcanzó a escribirse Último estado confirmado
Reglas de integridad En cada programa cliente Una vez, en el esquema
Consultas no previstas Programa nuevo Consulta nueva
Concurrencia Responsabilidad del programador Del gestor, según nivel declarado
Control de acceso Permisos del sistema de archivos Por objeto, rol y fila
Costo de operación Casi nulo Proceso que mantener

Errores frecuentes

  1. «La base de datos garantiza que los datos sean correctos.» Garantiza las restricciones declaradas. Un esquema sin CHECK, sin NOT NULL y sin claves foráneas no garantiza nada; solo almacena.
  2. «SQL es la base de datos.» SQL es un lenguaje. El gestor es el programa que lo ejecuta, y hay gestores sin SQL con las mismas garantías transaccionales.
  3. «Si va lento, el motor es malo.» Antes de esa conclusión hay que mirar el modelo, los índices y el plan de ejecución (parte 08). El motor casi nunca es el primer sospechoso.
  4. «Con NoSQL me ahorro el modelado.» Se cambia dónde ocurre el modelado, no si ocurre: pasa del esquema a los patrones de acceso (parte 05).
  5. «Un archivo JSON es una base de datos.» Lo es en sentido coloquial y no lo es en sentido técnico: no ofrece atomicidad, aislamiento ni control de acceso.

De la clase a la operación

En un sistema real, la decisión «SGBD sí o no» arrastra consecuencias que no aparecen en el prototipo: quién aplica los parches de seguridad, dónde se guardan las copias, cómo se prueba la restauración, qué pasa cuando el disco se llena y quién recibe la alerta. Elegir un gestor es adoptar un servicio que operar, no solo una biblioteca que importar.

Reto de transferencia

Toma un sistema que hayas escrito y que guarde estado en archivos. Documenta, con evidencia:

  1. Una invariante entre dos registros que hoy nadie hace cumplir.
  2. Una secuencia concreta de dos procesos que la rompa (con marcas de tiempo, como en el ejemplo).
  3. Qué restricción del esquema la impediría en un SGBD.
  4. El costo de operación que asumirías al migrar.

Preguntas de evaluación

  1. Explica, con una traza temporal propia, una anomalía de concurrencia distinta de la actualización perdida y por qué el sistema de archivos no la evita.
  2. Un compañero afirma: «migramos a PostgreSQL, así que los datos ya son consistentes». ¿Qué le falta declarar para que esa frase sea cierta?
  3. Da un caso real de tu trabajo donde usar un SGBD sería una mala decisión, y justifica con los criterios del diagrama.
  4. ¿Qué componente descrito por Hellerstein et al. desaparece si renuncias a la durabilidad, y qué garantía pierdes con él?

🌐 El mismo problema en cada motor

Caso: Que el sistema impida por sí solo dos estudiantes con el mismo correo

Se intentan registrar tres estudiantes, y el tercero repite el correo del primero. El programa no comprueba nada: la comprobación tiene que hacerla el sistema de datos. Al terminar, la consulta devuelve los correos registrados, ordenados alfabéticamente, uno por línea.

Es la pregunta más elemental que se le puede hacer a un almacén de datos: ¿puede garantizar una regla, o solo guardar lo que le den? Un archivo de texto y una hoja de cálculo aceptan el duplicado sin protestar; por eso no son bases de datos.

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

correo
ada@example.org
linus@example.org

El contrato vive en motores.yaml y lo comprueba python scripts/verificar_equivalencia.py --clase 011: 6 de las 7 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
MongoDB servicio código doc oficial
Redis servicio código doc oficial
MySQL servicio código doc oficial
Neo4j declarado código doc oficial
Apache Cassandra no doc oficial
Amazon DynamoDB 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_createtable.html
-- nota: INSERT OR IGNORE deja que el motor rechace el duplicado sin abortar el
--       guion. Sin OR IGNORE, la tercera insercion lanza un error: esa es
--       exactamente la garantia que se esta demostrando.

-- === preparacion ===
CREATE TABLE estudiantes (
    id     INTEGER PRIMARY KEY,
    correo TEXT NOT NULL UNIQUE
);

INSERT INTO estudiantes (id, correo) VALUES (1, 'ada@example.org');
INSERT INTO estudiantes (id, correo) VALUES (2, 'linus@example.org');
-- El programa no comprueba nada: lo intenta igual. El motor lo rechaza.
INSERT OR IGNORE INTO estudiantes (id, correo) VALUES (3, 'ada@example.org');

-- === consulta ===
SELECT correo FROM estudiantes ORDER BY correo;

DuckDB · implementaciones/duckdb/consulta.sql

verificado — se ejecuta en CI sin servicios

-- motor: duckdb
-- doc: https://duckdb.org/docs/stable/sql/constraints.html
-- nota: DuckDB no tiene INSERT OR IGNORE; la forma equivalente es
--       ON CONFLICT DO NOTHING, que es tambien la del estandar reciente.

-- === preparacion ===
CREATE TABLE estudiantes (
    id     INTEGER PRIMARY KEY,
    correo VARCHAR NOT NULL UNIQUE
);

INSERT INTO estudiantes VALUES (1, 'ada@example.org');
INSERT INTO estudiantes VALUES (2, 'linus@example.org');
INSERT INTO estudiantes VALUES (3, 'ada@example.org') ON CONFLICT DO NOTHING;

-- === consulta ===
SELECT correo FROM estudiantes ORDER BY correo;

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/ddl-constraints.html
-- nota: la restriccion se implementa con un indice unico. Consultar
--       pg_indexes despues de crear la tabla lo hace visible.

-- === preparacion ===
DROP TABLE IF EXISTS estudiantes;

CREATE TABLE estudiantes (
    id     integer PRIMARY KEY,
    correo text NOT NULL UNIQUE
);

INSERT INTO estudiantes (id, correo) VALUES (1, 'ada@example.org');
INSERT INTO estudiantes (id, correo) VALUES (2, 'linus@example.org');
INSERT INTO estudiantes (id, correo) VALUES (3, 'ada@example.org')
    ON CONFLICT (correo) DO NOTHING;

-- === consulta ===
SELECT correo FROM estudiantes ORDER BY correo;

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/index-unique/
// nota: con ordered:false, insertMany intenta TODOS los documentos y solo
//       falla el que viola el indice; el try/catch recoge ese error concreto.

// === preparacion ===
db.estudiantes.drop();
db.estudiantes.createIndex({ correo: 1 }, { unique: true });

try {
  db.estudiantes.insertMany(
    [
      { _id: 1, correo: "ada@example.org" },
      { _id: 2, correo: "linus@example.org" },
      { _id: 3, correo: "ada@example.org" },
    ],
    { ordered: false },
  );
} catch (e) {
  // Error 11000 = clave duplicada. Es el resultado esperado, no un fallo.
  if (!String(e).includes("11000")) throw e;
}

// === consulta ===
db.estudiantes
  .find({}, { _id: 0, correo: 1 })
  .sort({ correo: 1 })
  .forEach((d) => print(d.correo));

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: un conjunto no admite repetidos por definicion. SADD devuelve 1 si
#       anadio el elemento y 0 si ya estaba: la tercera orden devuelve 0.

# === preparacion ===
FLUSHDB
SADD correos ada@example.org
SADD correos linus@example.org
SADD correos ada@example.org

# === consulta ===
SORT correos ALPHA

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/create-index.html
-- nota: la columna se declara con intercalacion binaria para que la unicidad
--       distinga mayusculas de minusculas; con la intercalacion por omision,
--       'Ada@example.org' contaria como duplicado.

-- === preparacion ===
DROP TABLE IF EXISTS estudiantes;

CREATE TABLE estudiantes (
    id     INT PRIMARY KEY,
    correo VARCHAR(200) COLLATE utf8mb4_bin NOT NULL UNIQUE
) ENGINE=InnoDB;

INSERT INTO estudiantes (id, correo) VALUES (1, 'ada@example.org');
INSERT INTO estudiantes (id, correo) VALUES (2, 'linus@example.org');
INSERT IGNORE INTO estudiantes (id, correo) VALUES (3, 'ada@example.org');

-- === consulta ===
SELECT correo FROM estudiantes ORDER BY correo;

Neo4j · implementaciones/neo4j/consulta.cypher

declarado — se revisa a mano contra la documentación citada; la máquina no lo ejecuta

// motor: neo4j
// doc: https://neo4j.com/docs/cypher-manual/current/constraints/
// nota: implementacion declarada. La restriccion de unicidad se declara sobre
//       una propiedad de nodo; MERGE busca antes de crear, asi que el tercer
//       estudiante no llega a duplicarse.

// === preparacion ===
MATCH (n:Estudiante) DETACH DELETE n;
CREATE CONSTRAINT correo_unico IF NOT EXISTS
  FOR (e:Estudiante) REQUIRE e.correo IS UNIQUE;
MERGE (:Estudiante {correo: 'ada@example.org'});
MERGE (:Estudiante {correo: 'linus@example.org'});
MERGE (:Estudiante {correo: 'ada@example.org'});

// === consulta ===
MATCH (e:Estudiante) RETURN e.correo AS correo ORDER BY correo;

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 Un INSERT en Cassandra es en realidad un «escribe esto», no un «añade si no existe»: si la clave ya está, sobrescribe en silencio. Comprobar primero exige una transacción ligera (IF NOT EXISTS), que necesita acuerdo entre réplicas y cuesta varias veces más que una escritura normal. Hacer del correo la clave de partición y usar INSERT ... IF NOT EXISTS solo en el registro, aceptando su costo, o dejar que la unicidad la garantice el servicio de identidad que está delante. doc
Amazon DynamoDB No existen restricciones de unicidad sobre atributos que no sean la clave primaria; PutItem sobrescribe el elemento salvo que se le añada una condición explícita. Usar el correo como clave de partición y escribir con ConditionExpression: attribute_not_exists(pk), que falla si el elemento ya existe. La regla pasa a estar en cada llamada, no en el esquema. 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 →