Chapter 4

Graph Query Languages (Cypher, Gremlin)

Graph query languages are specialized languages designed to express pattern matching and traversal operations on graph data. Unlike SQL, which operates on tables and joins, graph query languages work natively with nodes, relationships, and paths. This chapter explores the major graph query languages, with deep dives into Cypher and Gremlin, and examines the emerging GQL standard.

The Evolution of Graph Query Languages

Graph databases needed specialized query languages because SQL, despite its power, wasn't designed for graph traversals. Early graph databases used imperative APIs that required developers to manually navigate relationships. Modern declarative query languages allow expressing complex patterns concisely.

Timeline of Graph Query Languages

Cypher: The Leading Declarative Graph Query Language

Created by Neo4j, Cypher has become the most widely used graph query language. Its SQL-like syntax and ASCII-art pattern matching make it intuitive and readable.

Cypher Philosophy

Cypher Syntax Fundamentals

Nodes

// Anonymous node
()

// Node with label
(:Person)

// Node with label and property filter
(:Person {name: 'Alice'})

// Node bound to variable
(p:Person)

// Node with multiple labels
(p:Person:Employee)

// Node with variable and properties
(alice:Person {name: 'Alice', age: 30})

Relationships

// Undirected relationship (rare in queries)
-[:KNOWS]-

// Directed relationship
-[:KNOWS]->

// Relationship with variable
-[k:KNOWS]->

// Relationship with properties
-[k:KNOWS {since: 2015}]->

// Multiple relationship types
-[:KNOWS|FRIEND_OF]->

// Variable length path
-[:KNOWS*1..3]->

// Any relationship type
-[*]->

Complete Patterns

// Simple pattern
(alice:Person)-[:KNOWS]->(bob:Person)

// Chain pattern
(alice)-[:KNOWS]->(bob)-[:KNOWS]->(charlie)

// Multiple relationships
(person)-[:LIVES_IN]->(city)<-[:LOCATED_IN]-(company)

// Variable length traversal
(alice)-[:KNOWS*2..4]->(friend)

Core Cypher Clauses

MATCH - Pattern Matching

// Find all people
MATCH (p:Person)
RETURN p

// Find friends of Alice
MATCH (alice:Person {name: 'Alice'})-[:KNOWS]->(friend)
RETURN friend.name

// Find friends of friends
MATCH (alice:Person {name: 'Alice'})-[:KNOWS*2]->(fof)
RETURN DISTINCT fof.name

// Multiple patterns
MATCH (p:Person)-[:WORKS_FOR]->(c:Company),
      (p)-[:LIVES_IN]->(city:City)
WHERE c.name = 'Acme Corp'
RETURN p.name, city.name

WHERE - Filtering

// Simple filter
MATCH (p:Person)
WHERE p.age > 25
RETURN p.name

// Complex conditions
MATCH (p:Person)
WHERE p.age > 25 AND p.city = 'NYC' OR p.salary > 100000
RETURN p.name

// Pattern in WHERE
MATCH (p:Person)
WHERE (p)-[:KNOWS]->(:Person {name: 'Alice'})
RETURN p.name

// String operations
MATCH (p:Person)
WHERE p.email ENDS WITH '@example.com'
RETURN p.name

// Null checks
MATCH (p:Person)
WHERE p.phone IS NOT NULL
RETURN p.name

// List operations
MATCH (p:Person)
WHERE p.age IN [25, 30, 35]
RETURN p.name

CREATE - Creating Data

// Create single node
CREATE (p:Person {name: 'Alice', age: 30})

// Create multiple nodes and relationship
CREATE (alice:Person {name: 'Alice'})-[:KNOWS]->(bob:Person {name: 'Bob'})

// Create pattern with existing nodes
MATCH (alice:Person {name: 'Alice'}), (bob:Person {name: 'Bob'})
CREATE (alice)-[:KNOWS {since: 2020}]->(bob)

// Create multiple relationships
CREATE (p:Person {name: 'Alice'})
CREATE (p)-[:LIVES_IN]->(:City {name: 'NYC'})
CREATE (p)-[:WORKS_FOR]->(:Company {name: 'Acme'})

MERGE - Get or Create

// Create node if it doesn't exist
MERGE (p:Person {email: 'alice@example.com'})
ON CREATE SET p.name = 'Alice', p.createdAt = timestamp()
ON MATCH SET p.lastSeen = timestamp()
RETURN p

// Create relationship if it doesn't exist
MATCH (alice:Person {name: 'Alice'}), (bob:Person {name: 'Bob'})
MERGE (alice)-[k:KNOWS]->(bob)
ON CREATE SET k.since = date()
RETURN k

SET - Updating Properties

// Set single property
MATCH (p:Person {name: 'Alice'})
SET p.age = 31
RETURN p

// Set multiple properties
MATCH (p:Person {name: 'Alice'})
SET p.age = 31, p.city = 'NYC', p.updatedAt = timestamp()

// Replace all properties
MATCH (p:Person {name: 'Alice'})
SET p = {name: 'Alice', age: 31, email: 'alice@example.com'}

// Add properties
MATCH (p:Person {name: 'Alice'})
SET p += {phone: '555-1234', verified: true}

// Add label
MATCH (p:Person {name: 'Alice'})
SET p:Employee:Manager

DELETE and REMOVE

// Delete node (must delete relationships first)
MATCH (p:Person {name: 'Alice'})
DETACH DELETE p

// Delete relationship
MATCH (alice:Person {name: 'Alice'})-[k:KNOWS]->(bob)
DELETE k

// Remove property
MATCH (p:Person {name: 'Alice'})
REMOVE p.temporaryField

// Remove label
MATCH (p:Person {name: 'Alice'})
REMOVE p:Temporary

RETURN - Projecting Results

// Return nodes
MATCH (p:Person)
RETURN p

// Return properties
MATCH (p:Person)
RETURN p.name, p.age

// Return with alias
MATCH (p:Person)
RETURN p.name AS personName, p.age AS personAge

// Return distinct
MATCH (p:Person)
RETURN DISTINCT p.city

// Return with limit
MATCH (p:Person)
RETURN p.name
ORDER BY p.age DESC
LIMIT 10

// Return with expressions
MATCH (p:Person)
RETURN p.name, p.age, p.age * 2 AS doubleAge

WITH - Chaining Queries

// Chain operations
MATCH (p:Person)
WITH p
ORDER BY p.age DESC
LIMIT 10
MATCH (p)-[:KNOWS]->(friend)
RETURN p.name, collect(friend.name) AS friends

// Aggregation in pipeline
MATCH (p:Person)-[:PURCHASED]->(product)
WITH p, count(product) AS purchaseCount
WHERE purchaseCount > 5
RETURN p.name, purchaseCount

Aggregation Functions

// Count
MATCH (p:Person)
RETURN count(p)

// Collect
MATCH (alice:Person {name: 'Alice'})-[:KNOWS]->(friend)
RETURN alice.name, collect(friend.name) AS friends

// Sum, Avg, Min, Max
MATCH (p:Person)
RETURN avg(p.age), min(p.age), max(p.age), sum(p.age)

// Collect distinct
MATCH (p:Person)-[:LIVES_IN]->(city)
RETURN collect(DISTINCT city.name) AS cities

Advanced Cypher Patterns

Optional Patterns

// LEFT JOIN equivalent
MATCH (p:Person)
OPTIONAL MATCH (p)-[:KNOWS]->(friend)
RETURN p.name, collect(friend.name) AS friends

Shortest Path

// Find shortest path
MATCH path = shortestPath(
  (alice:Person {name: 'Alice'})-[:KNOWS*]-(bob:Person {name: 'Bob'})
)
RETURN path, length(path)

// All shortest paths
MATCH paths = allShortestPaths(
  (alice:Person {name: 'Alice'})-[:KNOWS*]-(bob:Person {name: 'Bob'})
)
RETURN paths

List Comprehensions

// Transform list
MATCH (p:Person)
RETURN [friend IN collect(p) | friend.name] AS names

// Filter list
MATCH (p:Person)
WITH collect(p) AS people
RETURN [person IN people WHERE person.age > 25 | person.name] AS adults

Gremlin: The Traversal Language

Gremlin, part of Apache TinkerPop, is a functional, data-flow language for graph traversal. Unlike Cypher's declarative approach, Gremlin is more imperative, describing the traversal steps explicitly.

Gremlin Philosophy

Gremlin Basics

Starting Traversals

// Get all vertices
g.V()

// Get specific vertex by ID
g.V(123)

// Get all edges
g.E()

// Get vertices by label
g.V().hasLabel('person')

// Get vertices by property
g.V().has('person', 'name', 'Alice')

Traversal Steps

// Out: traverse outgoing edges
g.V().has('person', 'name', 'Alice').out('knows')

// In: traverse incoming edges
g.V().has('person', 'name', 'Bob').in('knows')

// Both: traverse in both directions
g.V().has('person', 'name', 'Alice').both('knows')

// OutE: get outgoing edges
g.V().has('person', 'name', 'Alice').outE('knows')

// InV: get incoming vertices from edge
g.E().hasLabel('knows').inV()

Filtering

// Has: filter by property
g.V().has('person', 'age', gt(25))

// Where: complex filters
g.V().hasLabel('person').where(__.values('age').is(gt(25)))

// Not: negation
g.V().hasLabel('person').not(__.out('knows'))

// And/Or
g.V().hasLabel('person').and(
  __.has('age', gt(25)),
  __.has('city', 'NYC')
)

Transformations

// Values: extract property values
g.V().hasLabel('person').values('name')

// ValueMap: get all properties
g.V().hasLabel('person').valueMap()

// Project: create custom projections
g.V().hasLabel('person').project('name', 'friendCount')
  .by('name')
  .by(out('knows').count())

// Select: select from previous steps
g.V().hasLabel('person').as('p')
  .out('knows').as('friend')
  .select('p', 'friend')
  .by('name')

Aggregations

// Count
g.V().hasLabel('person').count()

// Group
g.V().hasLabel('person').group().by('city').by(count())

// Fold: collect all results
g.V().hasLabel('person').values('name').fold()

// Group count
g.V().hasLabel('person').groupCount().by('city')

Cypher vs Gremlin Comparison

Aspect Cypher Gremlin
Style Declarative (what) Imperative (how)
Syntax SQL-like, ASCII art Functional, method chaining
Learning Curve Easier for SQL users Easier for programmers
Database Support Neo4j, some others Multiple (TinkerPop-enabled)
Expressiveness Excellent for patterns Excellent for complex logic
Language Binding Query string Native in multiple languages

Example: Same Query in Both Languages

Find friends of friends who like the same products:

Cypher:

MATCH (me:Person {name: 'Alice'})-[:KNOWS*2]->(fof:Person)
MATCH (me)-[:LIKES]->(product:Product)<-[:LIKES]-(fof)
WHERE NOT (me)-[:KNOWS]->(fof)
RETURN fof.name, collect(DISTINCT product.name) AS commonProducts
ORDER BY size(commonProducts) DESC
LIMIT 10

Gremlin:

g.V().has('person', 'name', 'Alice').as('me')
  .out('knows').out('knows').as('fof')
  .where(not(__.as('me').out('knows').as('fof')))
  .where(
    __.as('me').out('likes').as('product')
      .in('likes').where(eq('fof'))
  )
  .select('fof', 'product')
  .by('name')
  .by('name')
  .groupCount()
  .order(local).by(values, desc)
  .limit(10)

SPARQL for RDF Graphs

SPARQL is the W3C standard query language for RDF (Resource Description Framework) triple stores:

PREFIX foaf: 

SELECT ?name ?email
WHERE {
  ?person foaf:name "Alice" .
  ?person foaf:knows ?friend .
  ?friend foaf:name ?name .
  ?friend foaf:mbox ?email .
}

GQL: The Upcoming Standard

GQL (Graph Query Language) is being standardized by ISO/IEC to provide a unified query language for property graphs:

Query Optimization

Cypher Optimization Techniques

  1. Start with specific nodes: Use indexed properties in MATCH
  2. Use labels: Always specify labels for better performance
  3. Limit early: Apply filters as early as possible
  4. Use LIMIT: Prevent returning excessive results
  5. Parameterize: Use parameters for query plan caching
  6. Analyze with PROFILE: Understand execution plans
// Bad: No labels, late filter
MATCH (a)-[:KNOWS*2]->(b)
WHERE a.name = 'Alice'
RETURN b

// Good: Labels, early binding
MATCH (alice:Person {name: 'Alice'})-[:KNOWS*2]->(b:Person)
RETURN b.name
LIMIT 100

Chapter Summary

Graph query languages like Cypher and Gremlin provide powerful tools for expressing complex graph operations. Cypher's declarative, pattern-based approach makes it intuitive for most users, while Gremlin's imperative style offers fine-grained control. Understanding both paradigms and choosing the right language for your use case and database platform is essential for effective graph database development.

Next, we'll explore graph algorithms and analytics, learning how to extract insights from connected data using pathfinding, centrality metrics, and community detection.

Chapter 4 — Notes & References

  1. WIA Standards Public Repository (graph-database folder), MIT License, GitHub: WIA-Official/wia-standards-public/tree/main/graph-database — open standard initiative providing source code for simulator, spec, API, and ebook assets cited throughout this volume; serves as the canonical verification record for all primary-source citations made by the WIA standard committee in this chapter. Canonical ENUM tokens used in this volume include NEO4J, NEPTUNE, DGRAPH, ARANGODB, JANUSGRAPH, TIGERGRAPH, MEMGRAPH, NEBULA_GRAPH, CYPHER, GREMLIN, SPARQL, GQL, PAGERANK, LOUVAIN, NODE2VEC, GRAPHSAGE, ISO_GQL_2024, W3C_RDF, W3C_SPARQL, LDBC_SNB, WIKIDATA, DBPEDIA, YAGO, CONCEPTNET.