Bases de datos de grafos
En resumen: Una base de datos de grafos como Neo4j guarda nodos y relaciones en lugar de tablas, y las consultas de Cypher buscan patrones y caminos en ellos.
Problema
Quienes compraron café también compraron
Bajo cada producto la tienda quiere una línea «Quienes compraron esto también compraron». Para el café en grano la consulta en la base de datos SQL es esta:
Recorre seis tablas: del producto a las líneas de pedido, a los pedidos, al cliente, a sus otros pedidos, a las líneas de estos y a los productos. orders y order_items participan dos veces cada una. El siguiente deseo es aún más pesado: «los productos de las personas cuyas recomendaciones sigues, y de las personas a las que ellas siguen». Cada paso son más joins, y «hasta tres pasos» necesita WITH RECURSIVE. La pregunta trata de conexiones entre cosas, y en las tablas las conexiones están repartidas en claves foráneas.
La idea
Nodos y relaciones
Una base de datos de grafos guarda los datos como un grafo: nodos (las cosas, como un cliente y un producto) y relaciones entre ellos. Cada relación tiene un tipo (BOUGHT) y una dirección (del cliente al producto). Los nodos tienen etiquetas (:Customer), y tanto los nodos como las relaciones tienen propiedades (name, price, quantity). La relación está guardada en la base de datos, no se busca con un JOIN en cada consulta: desde un nodo la base de datos va directa a sus vecinos.
La base de datos de grafos más conocida es Neo4j, y su lenguaje es Cypher. Sus patrones son dibujos hechos con caracteres: (c:Customer) es un nodo, y -[:BOUGHT]-> es una flecha.
La consulta termina con ;, así que puede ocupar varias líneas. CREATE crea nodos y relaciones. MATCH encuentra todo lo que encaja con el patrón, y RETURN dice qué mostrar. Bajo la salida el simulador dibuja los nodos y las relaciones que encontró.
Más precisamente, para curiosos
El simulador conoce una parte de Cypher: MATCH, WHERE, CREATE, MERGE, SET, DELETE, DETACH DELETE, RETURN, ORDER BY, SKIP, LIMIT y unas pocas funciones. No hay WITH, ni OPTIONAL MATCH, ni UNWIND, ni índices, ni restricciones. La salida se parece a la consola cypher-shell, sin la línea con el tiempo de la consulta, y los mensajes de error son más cortos. El simulador muestra el prompt antes de cada línea de una consulta. Neo4j Browser no necesita el ; del final y dibuja el resultado por sí mismo. Sin ORDER BY el simulador devuelve las filas en el orden en que se crearon los nodos, y Neo4j no promete ningún orden.
Pruébalo
Georgi y el pan
La base de datos está vacía. Anota que el cliente Georgi Petrov compró pan blanco: un nodo con la etiqueta Customer y una propiedad name, un nodo con la etiqueta Product y una propiedad name, y una relación BOUGHT del cliente al producto.
Solución
CREATE (g:Customer {name: 'Georgi Petrov'})-[:BOUGHT]->(b:Product {name: 'Pan blanco'});
Una consulta: CREATE (g:Customer {name: 'Georgi Petrov'})-[:BOUGHT]->(b:Product {name: 'Pan blanco'});. La dirección importa: una flecha del producto al cliente dice «el pan compró a Georgi», y la comprobación del cliente al producto no encuentra nada. Los nombres g y b viven solo hasta el ;. Si creas los nodos en tres consultas separadas, la tercera no sabe qué son g y b y crea dos nodos nuevos sin etiqueta y sin propiedades.
La idea
Patrones
A partir de aquí la base de datos es la tienda: 20 clientes, 25 productos en 6 categorías y 109 relaciones BOUGHT, una por cada cliente y producto de los pedidos que no se cancelaron. Vienen de la base de datos SQL del curso. Un producto está conectado con su categoría por una relación IN.
Un patrón puede ser tan largo como necesites: del cliente al producto y a la categoría, sin un solo join.
La dirección del patrón tiene que coincidir con la dirección de la relación:
No hay flechas BOUGHT de un producto a un cliente, así que la primera consulta no encuentra a nadie. <-[:BOUGHT]- lee la misma relación desde su otro extremo. -[:BOUGHT]- sin punta de flecha encuentra la relación en cualquier dirección.
Más precisamente, para curiosos
El grafo no guarda los pedidos individuales: una relación BOUGHT solo dice que el cliente ha comprado el producto. Si necesitas también los pedidos, pasan a ser nodos: (:Customer)-[:PLACED]->(:Order)-[:CONTAINS {quantity: 2}]->(:Product). Qué es un nodo y qué es una relación depende de las preguntas que vayas a hacer, igual que el esquema de una base de datos SQL.
Míralo
La misma pregunta, un patrón
Así es «quienes compraron café también compraron» en Cypher:
La respuesta es la misma que la de la consulta SQL del principio, fila por fila. El patrón se lee de izquierda a derecha: el café, comprado por un cliente que también compró otro producto. El café no se cuenta contra sí mismo: en un MATCH la misma relación no puede participar dos veces, y la relación con el café ya está tomada. En SQL eso era una condición aparte (p2.id <> p1.id). Solo tres personas compraron café, así que los cinco productos tienen dos compradores cada uno y están ordenados por nombre.
La diferencia no es solo la longitud. La base de datos SQL busca cada join en un índice o en toda la tabla. La base de datos de grafos parte del café y sigue solo sus relaciones: el trabajo depende de cuántas relaciones hay alrededor del café, no de cuántos pedidos tiene la tienda.
La idea
Caminos de longitud variable
Digamos que la tienda tiene perfiles, y cada cliente puede seguir las recomendaciones de otros clientes: (:Customer)-[:FOLLOWS]->(:Customer). Estas relaciones están inventadas para la lección; la base de datos SQL no las tiene.
-[:FOLLOWS*1..2]-> es un camino de uno a dos pasos FOLLOWS. p = … le da nombre al camino entero, y length(p) es su número de pasos:
En el primer resultado Viktoria aparece dos veces: Elena sigue a Maria y a Dimitar, y los dos siguen a Viktoria. MATCH devuelve cada camino, no cada persona. En la segunda consulta WHERE f <> me quita a la propia Elena: en tres pasos el camino vuelve a ella (Elena, Maria, Viktoria, Elena).
Quién está en el camino lo muestra nodes(p):
En SQL lo mismo es WITH RECURSIVE sobre una tabla follows: un inicio, UNION ALL y un paso que une follows con las filas anteriores, más un contador de pasos. Aquí es *1..3.
Míralo
Un camino sin límite
Un patrón sin tipo y sin dirección, «todo lo conectado con Elena», con un camino cada vez más largo:
98 recorridos, luego 688, luego 4370. Cada paso los multiplica por seis o siete, porque un producto como el pan blanco está conectado con muchos clientes, y cada uno de ellos con muchos productos. * sin límite superior prueba todos los recorridos. El simulador se detiene tras 100 000 pasos, y Neo4j no se detiene solo: la consulta sigue hasta que se queda sin tiempo o sin memoria. Por eso escribe siempre un límite superior y, cuando puedas, también el tipo y la dirección de la relación.
Ejercicio
Recomendaciones
1. El queso también necesita una línea «Quienes compraron esto también compraron».
¿Qué tres productos compraron más las personas que compraron queso blanco? Muestra el nombre y el número de compradores (buyers), de más a menos, y por nombre cuando los números sean iguales.
Solución
MATCH (:Product {name: 'Queso blanco'})<-[:BOUGHT]-(c:Customer)-[:BOUGHT]->(p:Product)
RETURN p.name, count(c) AS buyers
ORDER BY buyers DESC, p.name
LIMIT 3;
El patrón es el mismo que con el café: (:Product {name: 'Queso blanco'})<-[:BOUGHT]-(c:Customer)-[:BOUGHT]->(p:Product), luego count(c) AS buyers, ORDER BY buyers DESC, p.name y LIMIT 3. Sin DESC obtienes los menos comprados. Si la flecha va del queso al cliente, el patrón no encuentra nada.
2. Maria Ivanova sigue a unas cuantas personas, y ellas siguen a otras.
Muestra los clientes a los que llega Maria Ivanova a través de FOLLOWS en uno o dos pasos, cada uno una vez, ordenados por nombre.
Solución
MATCH (:Customer {name: 'Maria Ivanova'})-[:FOLLOWS*1..2]->(f:Customer)
RETURN DISTINCT f.name
ORDER BY f.name;
-[:FOLLOWS*1..2]-> y RETURN DISTINCT f.name. A Viktoria se llega directamente y a través de Georgi, así que sin DISTINCT aparece dos veces. Con *2 faltan las personas a las que Maria sigue directamente.
3. Ahora la recomendación de verdad.
Encuentra los productos comprados por las personas a las que Elena Dimitrova sigue en uno o dos pasos, que ella aún no ha comprado. Muestra los tres comprados por más personas distintas (people), y por nombre cuando los números sean iguales.
Solución
MATCH (me:Customer {name: 'Elena Dimitrova'})-[:FOLLOWS*1..2]->(f:Customer)-[:BOUGHT]->(p:Product)
WHERE NOT (me)-[:BOUGHT]->(p)
RETURN p.name, count(DISTINCT f) AS people
ORDER BY people DESC, p.name
LIMIT 3;
WHERE NOT (me)-[:BOUGHT]->(p) quita los productos que Elena ya tiene: un patrón en WHERE solo comprueba si existe esa relación. count(DISTINCT f) cuenta personas, no caminos. Sin DISTINCT Viktoria, a quien llevan dos caminos, se cuenta dos veces.
La idea
Cuándo una base de datos de grafos y cuándo no
Una base de datos de grafos es buena cuando la pregunta trata de conexiones, a menudo de varios pasos: recomendaciones («quienes compraron esto también compraron»), redes sociales, detección de fraude (cuentas que comparten un teléfono o una dirección), rutas y dependencias, permisos de acceso a través de grupos. No es buena para informes que suman números de todos los registros, como los ingresos por mes: una consulta así no sigue relaciones, lo lee todo seguido. Eso es trabajo para SQL con GROUP BY o para una base de datos hecha para informes (OLAP). Por eso un grafo suele estar junto a la base de datos SQL: los pedidos y el dinero se quedan en SQL, y una copia de las conexiones va al grafo para las recomendaciones.
Más precisamente, para curiosos
Además de Neo4j, entienden Cypher Memgraph y Amazon Neptune (allí el lenguaje se llama openCypher; Neptune entiende también Gremlin y SPARQL), y la extensión Apache AGE añade Cypher a PostgreSQL. En 2024 salió el estándar internacional GQL (ISO/IEC 39075), basado en gran parte en las ideas de Cypher. Otro tipo de base de datos de grafos es el almacén RDF con el lenguaje SPARQL, donde todo es una tripleta de «sujeto, predicado, objeto».
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Árboles y secuencias: WITH RECURSIVE · Tipos de relación: 1:1, 1:N, M:N · Índices · OLTP y OLAP
- Continúa en: Cómo elegir una base de datos
- Ejercicios sobre el tema: Tipos de bases de datos