<!-- Canonical: https://lucioduran.com/es/blog/graph-databases-neo4j-surrealdb-age-comparison
Language: es
Content-SHA256: 55f78f73a701b43a42531f789f662500fca4a7596a3a69d5a6815773ef1689e1
-->

Databases

# Bases de Datos de Grafos: Neo4j vs SurrealDB vs Apache AGE, Cypher, GQL y Optimización Real de Queries de Red Social

2025-11-248 min lectura

Lucio Durán
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/graph-databases-neo4j-surrealdb-age-comparison>)

## El Modelo de Datos

Una feature de red social requiere: usuarios, follows, posts, likes, comentarios e "intereses" (temas con los que los usuarios se etiquetan). El modelo de grafo:

```
(:User)-[:FOLLOWS]->(:User)
(:User)-[:POSTED]->(:Post)
(:User)-[:LIKED]->(:Post)
(:User)-[:COMMENTED {text: "..."}]->(:Post)
(:User)-[:INTERESTED_IN]->(:Topic)
(:Post)-[:TAGGED_WITH]->(:Topic)
```

Unos 500K usuarios, 2M relaciones de follow, 1.5M posts, 8M likes. No es masivo de ninguna manera, pero suficiente para exponer diferencias de performance.

## Neo4j: El Grafo Nativo

[Neo4j](<https://neo4j.com/>) almacena datos como nodos y relaciones en un formato de grafo nativo, index-free adjacency significa que cada nodo apunta físicamente a sus vecinos. Esto hace que las operaciones de traversal sean constant-time por hop, independientemente del tamaño total del grafo.

### Setup e Ingesta

```cypher
// Crear constraints e índices
CREATE CONSTRAINT user_email IF NOT EXISTS
FOR (u:User) REQUIRE u.email IS UNIQUE;

CREATE INDEX user_name IF NOT EXISTS
FOR (u:User) ON (u.name);

CREATE INDEX post_created IF NOT EXISTS
FOR (p:Post) ON (p.createdAt);

// Import batch con LOAD CSV
LOAD CSV WITH HEADERS FROM 'file:///users.csv' AS row
CREATE (u:User {
 id: row.id,
 name: row.name,
 email: row.email,
 joinedAt: datetime(row.joinedAt)
});
```

La ingesta de 500K usuarios + relaciones tomó unos 90 segundos usando ` neo4j-admin import ` (bulk load offline). El approach con LOAD CSV tardó 12 minutos. Para migraciones de producción, siempre utilizar la herramienta admin.

### Ejemplos de Queries

El pan y la manteca, "¿a quién debería seguir este usuario?" basado en conexiones mutuas e intereses compartidos:

```cypher
// Recomendación amigo-de-amigo excluyendo follows existentes
MATCH (me:User {id: $userId})-[:FOLLOWS]->(friend)-[:FOLLOWS]->(suggestion)
WHERE NOT (me)-[:FOLLOWS]->(suggestion)
 AND suggestion <> me
WITH suggestion, count(friend) AS mutualFriends
ORDER BY mutualFriends DESC
LIMIT 20
RETURN suggestion.name, suggestion.id, mutualFriends
```

Esto corrió en 15ms en nuestro dataset. El query planner usa el índice en ` User.id `, después traversa dos hops a través de la relación FOLLOWS. La agregación ` count(friend) ` es donde pasa el ranking.

Una más compleja, "encontrá el camino más corto entre dos usuarios a través de su grafo de follows":

```cypher
MATCH path = shortestPath(
 (a:User {id: $userA})-[:FOLLOWS*..6]-(b:User {id: $userB})
)
RETURN [node IN nodes(path) | node.name] AS names,
 length(path) AS degrees
```

Ese ` *..6 ` es crítico. Sin un límite de profundidad, esta query va a explorar todo el componente conectado, que en un grafo social significa potencialmente millones de nodos. Se establece en 6 por la teoría de los seis grados de separación, si no hay camino dentro de 6 hops, los usuarios no están significativamente conectados para propósitos típicos de UX.

### Optimización de Performance

La optimización de mayor impacto en Neo4j es **profilear tus queries**:

```cypher
PROFILE MATCH (me:User {id: $userId})-[:FOLLOWS]->(friend)-[:FOLLOWS]->(suggestion)
WHERE NOT (me)-[:FOLLOWS]->(suggestion)
RETURN suggestion.name, count(friend) AS mutual
ORDER BY mutual DESC LIMIT 20
```

El prefijo ` PROFILE ` te muestra el plan de ejecución de la query con conteos de filas reales. El análisis reveló que una query de "posts trending en tu red" estaba haciendo un ` AllNodesScan ` en Post en vez de usar el índice de fecha, agregar un filtro ` WHERE p.createdAt > datetime() - duration('P7D') ` antes del traversal de relaciones cortó la query de 2.3s a 80ms.

## SurrealDB: El Enfoque Multi-Modelo

[SurrealDB](<https://surrealdb.com/>) maneja grafos a través de su sistema de record links, cualquier campo puede referenciar otro registro usando sintaxis ` record_id `, y es posible traversar estos links con notación de flecha en SurrealQL.

```sql
-- Definir esquema
DEFINE TABLE user SCHEMAFULL;
DEFINE FIELD name ON user TYPE string;
DEFINE FIELD email ON user TYPE string;
DEFINE INDEX email_idx ON user FIELDS email UNIQUE;

DEFINE TABLE follows SCHEMAFULL TYPE RELATION
 FROM user TO user;
DEFINE FIELD since ON follows TYPE datetime DEFAULT time::now();

DEFINE TABLE post SCHEMAFULL;
DEFINE FIELD content ON post TYPE string;
DEFINE FIELD author ON post TYPE record<user>;
DEFINE FIELD createdAt ON post TYPE datetime DEFAULT time::now();
```

### Traversal de Grafo en SurrealQL

La sintaxis de flechas es sorprendentemente limpia:

```sql
-- Follows directos
SELECT name FROM user:alice->follows->user;

-- Amigo-de-amigo (2 hops)
SELECT name, count() AS mutual FROM
 user:$userId->follows->user->follows->user
WHERE id NOT IN (SELECT VALUE out FROM follows WHERE in = user:$userId)
 AND id != user:$userId
GROUP BY id
ORDER BY mutual DESC
LIMIT 20;
```

La misma query de recomendación que tardó 15ms en Neo4j corrió en 45ms en SurrealDB con hardware equivalente. No está mal para una base de datos que también maneja queries de documentos y relacionales, pero la brecha se ensancha en traversals más profundos.

Donde SurrealDB brilla es en workloads mixtos. Considérese una query de feed que necesita tanto traversal de grafo (¿a quién sigo?) como agregación estilo documento (¿cuáles son sus posts recientes, rankeados por engagement?):

```sql
-- Feed: posts de usuarios seguidos, rankeados por engagement
SELECT
 content,
 author.name AS authorName,
 createdAt,
 count(->liked_by) AS likes,
 count(->comments) AS commentCount
FROM post
WHERE author IN (SELECT VALUE out FROM follows WHERE in = user:$userId)
 AND createdAt > time::now() - 7d
ORDER BY likes DESC, createdAt DESC
LIMIT 50;
```

En Neo4j, esto requeriría combinar traversal Cypher con búsqueda full-text y agregación, se puede pero es menos natural.

## Apache AGE: Grafo sobre PostgreSQL

Apache AGE (A Graph Extension) agrega soporte de openCypher a PostgreSQL. Tus datos de grafo viven en tablas de PostgreSQL con un formato interno específico, pero los se consulta con sintaxis Cypher envuelta en SQL.

```sql
-- Habilitar la extensión
CREATE EXTENSION IF NOT EXISTS age;
LOAD 'age';
SET search_path = ag_catalog, "$user", public;

-- Crear un grafo
SELECT create_graph('social');

-- Crear nodos
SELECT * FROM cypher('social', $$
 CREATE (u:User {id: 'alice', name: 'Alice', email: 'alice@example.com'})
 RETURN u
$$) AS (u agtype);

-- Crear relaciones
SELECT * FROM cypher('social', $$
 MATCH (a:User {id: 'alice'}), (b:User {id: 'bob'})
 CREATE (a)-[:FOLLOWS {since: '2025-01-15'}]->(b)
$$) AS (result agtype);
```

### El Puente SQL-Cypher

La feature asesina es mezclar SQL y Cypher en la misma query:

```sql
-- Usar tablas regulares de PostgreSQL junto con queries de grafo
SELECT
 u.display_name,
 u.avatar_url,
 graph_data.mutual_count
FROM users u
JOIN (
 SELECT * FROM cypher('social', $$
 MATCH (me:User {id: $userId})-[:FOLLOWS]->(friend)-[:FOLLOWS]->(suggestion)
 WHERE NOT (me)-[:FOLLOWS]->(suggestion) AND suggestion <> me
 RETURN suggestion.id AS user_id, count(friend) AS mutual_count
 ORDER BY mutual_count DESC
 LIMIT 20
 $$, $1) AS (user_id agtype, mutual_count agtype)
) AS graph_data ON u.id = graph_data.user_id::text;
```

Este es el superpoder de AGE, es posible agregar queries de grafo a una aplicación PostgreSQL existente sin migrar datos a una base de datos separada. La tabla ` users ` con datos de perfil se queda en PostgreSQL regular; el grafo social vive en AGE.

### Realidad de Performance

La misma recomendación amigo-de-amigo: 85ms en AGE vs 15ms en Neo4j. El overhead viene del executor de PostgreSQL planeando la query Cypher como una subquery, cada hop pasa por el planner regular en vez de traversal nativo de grafo.

Para queries de hasta 3 hops, AGE es absolutamente competitivo. Más allá de eso, el overhead se compone:

```
1 hop: AGE 8ms vs Neo4j 3ms (2.7x)
2 hops: AGE 35ms vs Neo4j 15ms (2.3x)
3 hops: AGE 85ms vs Neo4j 25ms (3.4x)
4 hops: AGE 340ms vs Neo4j 55ms (6.2x)
5 hops: AGE 1.8s vs Neo4j 120ms (15x)
```

El ratio empeora porque el index-free adjacency de Neo4j es O(1) por hop mientras AGE hace lookups de índice a través del B-tree de PostgreSQL para cada nivel.

## GQL: El Estándar del Futuro

ISO publicó GQL (ISO/IEC 39075) en 2024 como el lenguaje de query de grafos estándar. Toma mucho de Cypher pero agrega features como tipos de grafo, patrones de camino y un sistema de tipos más riguroso.

```gql
-- Sintaxis GQL (en evolución, esto es aproximado)
GRAPH social
MATCH (me:User WHERE me.id = $userId)
 -/:FOLLOWS/-> (friend)
 -/:FOLLOWS/-> (suggestion)
WHERE NOT EXISTS (me -/:FOLLOWS/-> suggestion)
 AND suggestion <> me
RETURN suggestion.name, COUNT(friend) AS mutual
ORDER BY mutual DESC
LIMIT 20;
```

La diferencia clave con Cypher: GQL usa sintaxis de patrones de camino (` -/:FOLLOWS/-> `) y selección explícita de grafo (` GRAPH social `). Neo4j se comprometió con soporte GQL, y bases de datos más nuevas como Memgraph y Kuzu lo están implementando.

Por ahora, Cypher sigue siendo la opción práctica. La adopción de GQL está donde estaba SQL a principios de los 90, el estándar existe, pero las implementaciones están poniéndose al día.

## Recomendaciones por Caso de Uso

Para una feature de grafo social greenfield: **Neo4j** si las queries de grafo son el workload principal y es posible justificar el costo operacional de una base de datos separada. La performance de queries y el tooling (Neo4j Browser, biblioteca APOC) no tienen rival.

Para agregar queries de grafo a una app PostgreSQL existente: **Apache AGE** sin dudar. La capacidad de hacer JOIN de resultados de grafo con tablas regulares es una ventaja masiva, y la performance es suficientemente buena para queries de 3 hops que cubre la mayoría de case es de uso reales.

Para un proyecto nuevo con requisitos mixtos (grafo + documento + real-time): **SurrealDB** vale la pena evaluarlo. El enfoque multi-modelo reduce la complejidad operacional. Solo entrá con los ojos abiertos sobre la madurez del optimizador de queries.

El modelo de grafo no es la parte difícil. La parte difícil es saber cuándo tus datos son realmente un grafo, y cuándo se está forzando un problema relacional en una forma de grafo porque el lenguaje de queries se ve más lindo.

## Preguntas frecuentes

### ¿Cuándo debería usar una base de datos de grafos en vez de una relacional?

Cuando tus queries son relationship-centric, 'encontrá todos los caminos entre A y B', 'mostrá N grados de conexión', 'detectar ciclos en un grafo de dependencias'. Si mayormente se hace lookups key-value o agregaciones, una base relacional con JOIN está bien. El punto de equilibrio suele estar alrededor de 3+ JOINs en tablas auto-referenciales, donde el traversal de grafo se vuelve más rápido y más legible.

### ¿SurrealDB está listo para producción con workloads de grafo?

A principios de 2026, SurrealDB es usable para workloads de grafo moderados pero le falta la madurez del optimizador de queries de Neo4j. Su fortaleza es el enfoque multi-modelo, si se necesita queries de documentos, grafos y relacionales en el mismo sistema, es atractivo. Para workloads puros de grafo a escala, Neo4j sigue siendo la apuesta más segura.

### ¿Qué es GQL y cómo se relaciona con Cypher?

GQL (Graph Query Language) es el estándar ISO (ISO/IEC 39075) para bases de datos de grafos, publicado en 2024. Está fuertemente influenciado por Cypher (el lenguaje de queries de Neo4j) y openCypher. Considerar en GQL como lo que SQL es para bases de datos relacionales, un estándar vendor-neutral. Neo4j se está moviendo hacia compatibilidad con GQL, y nuevas bases de datos lo están implementando nativamente.

## Tips

1. **Tip:** Nunca traverses más de 4 hops en una query de grafo social sin límite de profundidad. Una lección común: una query de 'amigos de amigos de amigos' en 500K usuarios tardó 47 segundos en Neo4j, agregar ` *1..3 ` la bajó a 200ms.
2. **Tip:** Si tus queries de grafo siempre arrancan desde un nodo conocido, indexá la propiedad de lookup de ese nodo. Un ` MATCH (u:User {email: $email}) ` sin índice hace un full node scan, clusters de Neo4j pueden llegar a 100% CPU por un índice faltante en un campo usado en cada query.
3. **Tip:** Apache AGE es brillante para agregar queries de grafo a una app PostgreSQL existente, pero no intentes migrar un workload de grafo completo a él. El overhead del executor de Postgres en traversals profundos es real, realizar pruebas de rendimiento antes de comprometerte.

- graph-databases
- neo4j
- surrealdb
- apache-age
- cypher
- gql
- property-graphs
- optimizacion-de-queries
- red-social
- bases-de-datos

[Volver al Blog](<https://lucioduran.com/es/blog>)
