Saltar a contenido

7.1 Relacional frente a no relacional

Qué se ha ejecutado y qué no

En esta unidad no hay un servidor de MongoDB, Redis, Cassandra ni Neo4j. Lo que ves con resultado se ha ejecutado de verdad en SQLite, que sabe guardar y consultar documentos JSON: sirve para ver el modelo de datos y sus consecuencias, aunque no es una base de datos documental completa. Los modelos en Python están marcados como didácticos (explican la idea, no son un motor real). Las órdenes de los motores de verdad se muestran como «sin ejecutar»: necesitan instalar su servidor.

Dos formas de organizar los mismos datos

Un pedido de una tienda tiene un cliente, una fecha y varias líneas (producto, precio, cantidad). Así lo guardaría una base relacional, repartido en tres tablas unidas por claves:

CREATE TABLE clientes (id INTEGER PRIMARY KEY, nombre TEXT NOT NULL, ciudad TEXT NOT NULL);
CREATE TABLE pedidos  (id INTEGER PRIMARY KEY, id_cliente INTEGER NOT NULL REFERENCES clientes(id), fecha TEXT NOT NULL);
CREATE TABLE lineas   (id_pedido INTEGER NOT NULL REFERENCES pedidos(id), producto TEXT NOT NULL, precio REAL NOT NULL, cantidad INTEGER NOT NULL);
INSERT INTO clientes VALUES (1, 'Ana', 'Lugo'), (2, 'Luis', 'Vigo');
INSERT INTO pedidos VALUES (101, 1, '2025-03-01'), (102, 1, '2025-03-05'), (103, 2, '2025-03-07');
INSERT INTO lineas VALUES (101, 'Cuaderno', 3.5, 2), (101, 'Lápiz', 0.9, 10), (102, 'Mochila', 25, 1), (103, 'Cuaderno', 3.5, 1), (103, 'Goma', 0.6, 3);

Para ver un pedido completo hay que juntar las tablas con JOIN:

SELECT p.id, c.nombre AS cliente, p.fecha, ROUND(SUM(l.precio * l.cantidad), 2) AS total
FROM pedidos p
JOIN clientes c ON c.id = p.id_cliente
JOIN lineas l   ON l.id_pedido = p.id
GROUP BY p.id
ORDER BY p.id;

Resultado:

id cliente fecha total
101 Ana 2025-03-01 16.0
102 Ana 2025-03-05 25.0
103 Luis 2025-03-07 5.3

3 filas

Una base documental (un tipo de no relacional) guarda el mismo pedido como un único documento, con el cliente y las líneas dentro. Lo que se lee de una vez es lo que se guardó junto:

{
  "cliente": { "nombre": "Ana", "ciudad": "Lugo" },
  "fecha": "2025-03-01",
  "lineas": [
    { "producto": "Cuaderno", "precio": 3.5, "cantidad": 2 },
    { "producto": "Lápiz",    "precio": 0.9, "cantidad": 10 }
  ],
  "etiquetas": ["papelería", "urgente"]
}

SQLite puede guardar documentos JSON en una columna y consultarlos, así que sirve para ver la diferencia. Cada fila de pedidos_doc es un pedido completo:

CREATE TABLE pedidos_doc (id INTEGER PRIMARY KEY, doc TEXT NOT NULL CHECK (json_valid(doc)));
INSERT INTO pedidos_doc VALUES
 (101, '{"cliente": {"nombre": "Ana", "ciudad": "Lugo"}, "fecha": "2025-03-01", "lineas": [{"producto": "Cuaderno", "precio": 3.5, "cantidad": 2}, {"producto": "Lápiz", "precio": 0.9, "cantidad": 10}], "etiquetas": ["papelería", "urgente"]}'),
 (102, '{"cliente": {"nombre": "Ana", "ciudad": "Lugo"}, "fecha": "2025-03-05", "lineas": [{"producto": "Mochila", "precio": 25, "cantidad": 1}], "etiquetas": ["mochilas"]}'),
 (103, '{"cliente": {"nombre": "Luis", "ciudad": "Vigo"}, "fecha": "2025-03-07", "lineas": [{"producto": "Cuaderno", "precio": 3.5, "cantidad": 1}, {"producto": "Goma", "precio": 0.6, "cantidad": 3}], "etiquetas": ["papelería", "urgente"]}');

Y ahora, el mismo resultado de antes, sin ningún JOIN (los operadores ->> leen un campo del JSON, y json_each recorre una lista):

SELECT d.id,
       d.doc ->> '$.cliente.nombre' AS cliente,
       d.doc ->> '$.fecha'          AS fecha,
       ROUND((SELECT SUM(l.value ->> '$.precio' * (l.value ->> '$.cantidad'))
              FROM json_each(d.doc, '$.lineas') AS l), 2) AS total
FROM pedidos_doc d
ORDER BY d.id;

Resultado:

id cliente fecha total
101 Ana 2025-03-01 16.0
102 Ana 2025-03-05 25.0
103 Luis 2025-03-07 5.3

3 filas

Los dos dan lo mismo, pero por caminos opuestos:

Relacional Documental
¿Dónde está el cliente? Una sola vez, en clientes Copiado dentro de cada pedido
Leer un pedido completo Varias tablas y JOIN Un único documento, de golpe
Cambiar la ciudad de Ana Un UPDATE Tocar todos sus pedidos

Esa tabla resume casi todo lo demás.

Las diferencias, una a una

Característica Relacional (SQL) No relacional (NoSQL)
Modelo Tablas con filas y columnas Documentos, clave-valor, columnas o grafos
Esquema Fijo y obligatorio: las columnas y tipos se declaran antes Flexible: cada elemento puede tener campos distintos
Relaciones Con claves foráneas y JOIN Datos anidados o referencias que gestiona el programa
Duplicación Se evita (normalización) Se acepta (desnormalización) para leer rápido
Consultas SQL, estándar y muy potente Cada motor tiene su propio lenguaje o API
Consistencia ACID: transacciones completas Suele ser BASE: se prima disponibilidad y velocidad
Crecer Sobre todo vertical (un servidor más potente) Sobre todo horizontal (repartir entre muchos servidores)
Ideal para Datos estructurados con muchas relaciones y reglas Datos variables, enormes cantidades o relaciones complejas

¿NoSQL significa «sin SQL»?

No exactamente. La sigla se entiende hoy como Not only SQL («no solo SQL»): son bases que no se limitan al modelo de tablas. Algunas incluso ofrecen lenguajes parecidos a SQL, y muchas bases relacionales (SQLite, PostgreSQL, MySQL) también guardan JSON, como acabas de ver.

Esquema fijo frente a esquema flexible

En una base relacional todas las filas de una tabla tienen las mismas columnas, y el motor rechaza lo que no cumple las reglas:

INSERT INTO clientes (id, nombre) VALUES (3, 'Marta');

Resultado: error

IntegrityError: NOT NULL constraint failed: clientes.ciudad

En una base documental cada documento puede ser distinto. Aquí un pedido con un campo que los demás no tienen (regalo) y otro sin líneas:

INSERT INTO pedidos_doc VALUES
 (104, '{"cliente": {"nombre": "Marta", "ciudad": "León"}, "fecha": "2025-03-09", "regalo": {"para": "Pablo", "mensaje": "¡Felicidades!"}, "lineas": [{"producto": "Libreta", "precio": 6, "cantidad": 1}]}');

SELECT id,
       doc ->> '$.cliente.nombre' AS cliente,
       doc ->> '$.regalo.para'    AS regalo_para,
       json_array_length(doc, '$.etiquetas') AS n_etiquetas
FROM pedidos_doc
ORDER BY id;

Resultado:

id cliente regalo_para n_etiquetas
101 Ana NULL 2
102 Ana NULL 1
103 Luis NULL 2
104 Marta Pablo NULL

4 filas

Lo que no existe devuelve NULL (el pedido sin regalo no da error). Esa libertad es muy cómoda cuando los datos cambian a menudo: productos con características distintas, formularios que evolucionan, datos que llegan de varias fuentes. Pero tiene un precio. El motor no impide que entre un documento absurdo:

INSERT INTO pedidos_doc VALUES (105, '{"cliente": 42, "fecha": "ayer", "lineas": "ninguna"}');

SELECT id, json_type(doc, '$.cliente') AS tipo_cliente, doc ->> '$.fecha' AS fecha FROM pedidos_doc WHERE id >= 104 ORDER BY id;

Resultado:

id tipo_cliente fecha
105 integer ayer

1 fila

json_valid solo comprueba que sea JSON bien formado; el documento 105 tiene un «cliente» que es un número y una fecha que es «ayer». El motor lo acepta. La validación pasa a ser responsabilidad de tu programa (aunque algunos motores, como MongoDB, permiten añadir reglas de validación opcionales).

La duplicación: rápido de leer, difícil de mantener

Fíjate en que el cliente «Ana» está copiado en dos pedidos. Si Ana se muda a Vigo y solo se actualiza uno, los datos se contradicen:

UPDATE pedidos_doc SET doc = json_set(doc, '$.cliente.ciudad', 'Vigo') WHERE id = 101;   -- ¡nos olvidamos del 102!

SELECT id, doc ->> '$.cliente.nombre' AS cliente, doc ->> '$.cliente.ciudad' AS ciudad FROM pedidos_doc WHERE doc ->> '$.cliente.nombre' = 'Ana' ORDER BY id;

Resultado:

id cliente ciudad
101 Ana Vigo
102 Ana Lugo

2 filas

Ana vive en Vigo y en Lugo a la vez. En el modelo relacional ese error no puede ocurrir, porque la ciudad está en un único sitio:

UPDATE clientes SET ciudad = 'Vigo' WHERE id = 1;

SELECT p.id, c.nombre, c.ciudad FROM pedidos p JOIN clientes c ON c.id = p.id_cliente WHERE c.nombre = 'Ana' ORDER BY p.id;

Resultado:

id nombre ciudad
101 Ana Vigo
102 Ana Vigo

2 filas

Es el problema que la normalización resolvía. Las bases no relacionales eligen ceder en esto a cambio de otra cosa (velocidad de lectura, flexibilidad, escala) y asumen que el programador mantiene la coherencia.

ACID, BASE y el teorema CAP

ACID: las promesas de una transacción relacional

Letra Significa Ejemplo
Atomicidad Todo o nada Una transferencia resta y suma, o no hace ninguna de las dos
Consistencia Las reglas (claves, CHECK) siempre se cumplen No queda un pedido de un cliente que no existe
Aislamiento Dos usuarios a la vez no se pisan Visto en la U6 y en la SA4
Durabilidad Lo confirmado no se pierde aunque se apague el servidor Tras el COMMIT, el dato está a salvo

BASE: otra filosofía

Para repartir los datos entre muchos servidores y seguir funcionando aunque alguno falle, muchas bases no relacionales ofrecen garantías más débiles, resumidas en BASE:

  • Basically Available: el sistema casi siempre responde.
  • Soft state: el estado puede cambiar sin que nadie escriba mientras las copias se sincronizan.
  • Eventually consistent: las copias acabarán coincidiendo, pero no de inmediato.

En la práctica: si escribes en un servidor y lees un instante después de otro, puede que aún no veas tu cambio. Para un «me gusta» o un contador de visitas es aceptable; para un saldo bancario, no.

El teorema CAP

Cuando los datos están repartidos en varios servidores hay tres propiedades deseables, y el teorema CAP dice que ante un fallo de la red entre ellos no se pueden tener las tres:

Propiedad Significa
Consistency (consistencia) Todos ven el mismo dato a la vez
Availability (disponibilidad) Todas las peticiones reciben respuesta
Partition tolerance (tolerancia a particiones) El sistema sigue funcionando aunque se corte la comunicación entre servidores

Como los cortes de red ocurren de verdad, la elección real es entre consistencia (negarse a responder hasta estar seguro) y disponibilidad (responder con lo que se tiene, aunque pueda estar desfasado). No es «relacional = bueno, NoSQL = malo»: cada sistema elige según lo que su aplicación puede permitirse.

Cuándo usar cada una

Si tu caso es… Piensa en… Por qué
Datos con muchas relaciones y reglas (facturas, notas, banca) Relacional Integridad y transacciones, sin duplicar
Datos que cambian de forma (catálogos con atributos distintos, contenidos) Documental Esquema flexible; se lee un elemento completo de golpe
Caché, sesiones, contadores, colas Clave-valor Lecturas y escrituras ultrarrápidas
Millones de eventos (sensores, registros, mensajes) Columnas anchas Escritura masiva y escala horizontal
Redes de relaciones (amigos, recomendaciones, rutas) Grafos Recorrer relaciones es su especialidad

No tienes que elegir solo una

Las aplicaciones grandes usan varias a la vez (persistencia políglota): por ejemplo, PostgreSQL para pedidos y pagos, Redis como caché de sesiones y un motor de grafos para las recomendaciones. Cada herramienta, para lo que mejor hace.

Errores y mitos frecuentes

Mito o error La realidad
«NoSQL es más moderno, así que es mejor» Resuelve otros problemas; para datos con reglas y relaciones, una relacional suele ser la mejor
«Sin esquema, me ahorro diseñar» El esquema sigue existiendo, pero en tu código: lo pagas después
«NoSQL siempre es más rápido» Lo es para lo que se diseñó; las consultas «raras» pueden ser muy lentas
«Las relacionales no escalan» Escalan mucho (y hoy muchas se reparten entre servidores); simplemente es más complejo
«En NoSQL no hay transacciones» Depende del motor: MongoDB las admite entre documentos (más costosas que en una relacional)
Copiar datos «por si acaso» Cada copia es una contradicción en potencia: decide cuál es la fuente de la verdad

Para practicar

Los ejercicios S7.1 a S7.3 de S7 · Ejercicios practican la lectura de documentos JSON con SQLite.