Skip to main content

Cypher Query Language Mastery

Module Duration: 6-8 hours Learning Style: Hands-On + Pattern Recognition + Real-World Queries Outcome: Write efficient, expressive Cypher queries for any graph problem

Introduction: ASCII Art for Graphs

Cypher uses ASCII art to represent graph patterns:
-- Nodes: ()
(alice)                    -- node variable
(:Person)                  -- node with label
(alice:Person)             -- node variable with label
(alice:Person {name: "Alice"})  -- node with properties

-- Relationships: -[]->
-->                        -- directed relationship
-[:KNOWS]->               -- relationship with type
-[k:KNOWS]->              -- relationship variable
-[k:KNOWS {since: 2020}]-> -- relationship with properties
-[:KNOWS|:FOLLOWS]->      -- multiple types (OR)

-- Patterns: Combine nodes and relationships
(alice:Person)-[:KNOWS]->(bob:Person)
(a)-[:FRIEND*1..3]->(b)   -- variable length path (1-3 hops)
This visual syntax makes queries intuitive and readable.

Part 1: Basic Pattern Matching

MATCH: Find Patterns

Simple node match:
MATCH (p:Person)
RETURN p.name, p.age
Relationship match:
MATCH (alice:Person {name: "Alice"})-[:KNOWS]->(friend)
RETURN friend.name
Multi-hop:
-- Friends of friends
MATCH (alice:Person {name: "Alice"})-[:KNOWS]->(friend)-[:KNOWS]->(fof)
RETURN fof.name
Undirected relationship (match either direction):
MATCH (alice:Person {name: "Alice"})-[:KNOWS]-(friend)
RETURN friend.name

WHERE: Filter Results

MATCH (p:Person)-[:KNOWS]->(friend)
WHERE p.name = "Alice"
  AND friend.age > 25
RETURN friend.name, friend.age
Pattern predicates:
-- Find people who DON'T know Bob
MATCH (p:Person)
WHERE NOT (p)-[:KNOWS]->(:Person {name: "Bob"})
RETURN p.name
String operations:
MATCH (p:Person)
WHERE p.name STARTS WITH "A"   -- or ENDS WITH, CONTAINS
RETURN p.name
Regular expressions:
MATCH (p:Person)
WHERE p.email =~ ".*@example\\.com"
RETURN p.name, p.email
List membership:
MATCH (p:Person)
WHERE p.name IN ["Alice", "Bob", "Charlie"]
RETURN p.name

CREATE: Add Data

Create node:
CREATE (p:Person {name: "Alice", age: 30})
RETURN p
Create relationship:
MATCH (alice:Person {name: "Alice"})
MATCH (bob:Person {name: "Bob"})
CREATE (alice)-[:KNOWS {since: 2020}]->(bob)
Create pattern:
CREATE (alice:Person {name: "Alice"})-[:KNOWS]->(bob:Person {name: "Bob"})

MERGE: Create if not exists

Idempotent create (prevents duplicates):
MERGE (p:Person {name: "Alice"})
ON CREATE SET p.created = timestamp()
ON MATCH SET p.accessed = timestamp()
RETURN p
Merge relationship:
MATCH (alice:Person {name: "Alice"})
MATCH (bob:Person {name: "Bob"})
MERGE (alice)-[k:KNOWS]->(bob)
ON CREATE SET k.since = 2020
RETURN k

SET: Update Properties

MATCH (p:Person {name: "Alice"})
SET p.age = 31,
    p.city = "San Francisco"
RETURN p
Add label:
MATCH (p:Person {name: "Alice"})
SET p:Engineer
RETURN labels(p)  -- ["Person", "Engineer"]
Copy properties:
MATCH (alice:Person {name: "Alice"})
MATCH (bob:Person {name: "Bob"})
SET bob = alice  -- Copy all properties

DELETE: Remove Data

Delete node (must delete relationships first!):
MATCH (p:Person {name: "Alice"})
DETACH DELETE p  -- Delete node and all relationships
Delete relationship:
MATCH (alice)-[k:KNOWS]->(bob)
DELETE k
Conditional delete:
MATCH (p:Person)
WHERE p.age > 100
DETACH DELETE p

Part 2: Variable-Length Paths

Syntax: *min..max

Friends within 1-3 hops:
MATCH (alice:Person {name: "Alice"})-[:KNOWS*1..3]->(friend)
RETURN DISTINCT friend.name
Any depth (use carefully!):
MATCH (alice:Person {name: "Alice"})-[:KNOWS*]->(friend)
RETURN friend.name
LIMIT 100  -- Always limit unbounded paths!

Shortest Path

Find shortest path between two nodes:
MATCH path = shortestPath(
  (alice:Person {name: "Alice"})-[:KNOWS*]-(bob:Person {name: "Bob"})
)
RETURN length(path) AS hops, nodes(path) AS people
All shortest paths:
MATCH path = allShortestPaths(
  (alice:Person {name: "Alice"})-[:KNOWS*]-(bob:Person {name: "Bob"})
)
RETURN path

Path Functions

MATCH path = (alice:Person {name: "Alice"})-[:KNOWS*1..3]->(friend)
RETURN
  length(path) AS hops,           -- Number of relationships
  nodes(path) AS all_nodes,       -- All nodes in path
  relationships(path) AS all_rels -- All relationships in path
LIMIT 10

Part 3: Aggregations

Basic Aggregations

MATCH (p:Person)
RETURN
  count(p) AS total,
  avg(p.age) AS avg_age,
  min(p.age) AS min_age,
  max(p.age) AS max_age,
  sum(p.salary) AS total_salary

GROUP BY (implicit)

Cypher groups by non-aggregated columns:
-- Count friends per person (grouped by p.name)
MATCH (p:Person)-[:KNOWS]->(friend)
RETURN p.name, count(friend) AS friend_count
ORDER BY friend_count DESC

COLLECT: Aggregate to List

-- Collect all friends into a list
MATCH (p:Person {name: "Alice"})-[:KNOWS]->(friend)
RETURN p.name, collect(friend.name) AS friends
Collect distinct:
MATCH (p:Person)-[:KNOWS]->(friend)
RETURN p.name, collect(DISTINCT friend.name) AS unique_friends

UNWIND: List to Rows

-- Expand list into rows
UNWIND ["Alice", "Bob", "Charlie"] AS name
CREATE (p:Person {name: name})
Flatten nested data:
WITH [[1,2], [3,4], [5]] AS nested
UNWIND nested AS list
UNWIND list AS value
RETURN value  -- Returns: 1, 2, 3, 4, 5

Part 4: Advanced Patterns

OPTIONAL MATCH: Left Outer Join

-- Find all people, with their friends (if any)
MATCH (p:Person)
OPTIONAL MATCH (p)-[:KNOWS]->(friend)
RETURN p.name, collect(friend.name) AS friends
-- People with no friends will have empty list []

Multiple Patterns

Comma separates independent patterns:
MATCH (alice:Person {name: "Alice"}),
      (bob:Person {name: "Bob"})
CREATE (alice)-[:KNOWS]->(bob)
Chained patterns:
MATCH (alice:Person {name: "Alice"})-[:KNOWS]->(friend)-[:LIKES]->(movie:Movie)
RETURN movie.title, count(friend) AS popularity
ORDER BY popularity DESC

NOT Pattern

-- Find people who don't know Bob
MATCH (p:Person)
WHERE NOT (p)-[:KNOWS]->(:Person {name: "Bob"})
RETURN p.name

EXISTS Subquery (Neo4j 4.0+)

-- Find people who know someone in San Francisco
MATCH (p:Person)
WHERE EXISTS {
  MATCH (p)-[:KNOWS]->(friend:Person)
  WHERE friend.city = "San Francisco"
}
RETURN p.name

CASE Expressions

MATCH (p:Person)
RETURN p.name,
  CASE
    WHEN p.age < 18 THEN "Minor"
    WHEN p.age < 65 THEN "Adult"
    ELSE "Senior"
  END AS age_group

Part 5: Functions

String Functions

RETURN
  toUpper("hello") AS upper,      -- "HELLO"
  toLower("WORLD") AS lower,      -- "world"
  substring("Hello", 1, 3) AS sub, -- "ell"
  replace("Hello", "l", "L") AS r, -- "HeLLo"
  split("a,b,c", ",") AS parts,   -- ["a","b","c"]
  trim("  hello  ") AS trimmed    -- "hello"

Math Functions

RETURN
  abs(-5) AS absolute,       -- 5
  ceil(4.3) AS ceiling,      -- 5
  floor(4.7) AS floor,       -- 4
  round(4.5) AS rounded,     -- 5
  sqrt(16) AS square_root,   -- 4
  rand() AS random           -- 0.0 to 1.0

List Functions

WITH [1, 2, 3, 4, 5] AS numbers
RETURN
  size(numbers) AS length,           -- 5
  head(numbers) AS first,            -- 1
  tail(numbers) AS rest,             -- [2,3,4,5]
  last(numbers) AS last_elem,        -- 5
  range(1, 10) AS one_to_ten,        -- [1,2,...,10]
  [x IN numbers WHERE x > 3] AS filtered  -- [4,5]

Date/Time Functions

RETURN
  date() AS today,                    -- 2024-01-15
  datetime() AS now,                  -- 2024-01-15T10:30:00Z
  timestamp() AS unix_timestamp,      -- 1705317000000
  duration.between(date("2020-01-01"), date()) AS days_since

Graph Functions

MATCH (p:Person)-[r:KNOWS]->(friend)
RETURN
  id(p) AS node_id,          -- Internal node ID
  id(r) AS rel_id,           -- Internal relationship ID
  labels(p) AS node_labels,  -- ["Person"]
  type(r) AS rel_type,       -- "KNOWS"
  keys(p) AS property_keys,  -- ["name", "age"]
  properties(p) AS all_props -- {name: "Alice", age: 30}

Part 6: Performance Optimization

1. Use Indexes

Create index:
CREATE INDEX person_name FOR (p:Person) ON (p.name)
Check query plan:
EXPLAIN
MATCH (p:Person {name: "Alice"})
RETURN p
-- Look for "NodeIndexSeek" (good) vs "NodeByLabelScan" (bad)

2. Filter Early

Bad (filter after expand):
MATCH (p:Person)-[:KNOWS]->(friend)
WHERE p.name = "Alice" AND friend.age > 25
RETURN friend.name
Good (filter before expand):
MATCH (p:Person {name: "Alice"})-[:KNOWS]->(friend:Person)
WHERE friend.age > 25
RETURN friend.name

3. Limit Results

MATCH (p:Person)-[:KNOWS]->(friend)
RETURN friend.name
LIMIT 100  -- Stop after 100 results

4. Use PROFILE to Find Bottlenecks

PROFILE
MATCH (p:Person {name: "Alice"})-[:KNOWS*2]->(fof)
RETURN fof.name
Look for:
  • High DB Hits → Optimize
  • NodeByLabelScan → Add index
  • Cartesian Products → Fix query logic

5. Avoid Cartesian Products

Bad (creates all combinations):
MATCH (p:Person), (m:Movie)  -- Cartesian product!
WHERE p.name = "Alice" AND m.title = "The Matrix"
RETURN p, m
Good (connected pattern):
MATCH (p:Person {name: "Alice"})-[:WATCHED]->(m:Movie {title: "The Matrix"})
RETURN p, m

Part 7: Common Patterns

Pattern 1: Recommendations

“People like you who bought X also bought Y”:
MATCH (user:User {id: $userId})-[:PURCHASED]->(product:Product)
MATCH (product)<-[:PURCHASED]-(other:User)-[:PURCHASED]->(recommendation:Product)
WHERE NOT (user)-[:PURCHASED]->(recommendation)
RETURN recommendation.name, count(*) AS score
ORDER BY score DESC
LIMIT 10

Pattern 2: Shortest Path

“How are Alice and Bob connected?”:
MATCH path = shortestPath(
  (alice:Person {name: "Alice"})-[:KNOWS*]-(bob:Person {name: "Bob"})
)
RETURN [node IN nodes(path) | node.name] AS connection_path

Pattern 3: Influence/Centrality

“Who has the most friends?” (degree centrality):
MATCH (p:Person)-[:KNOWS]->(friend)
RETURN p.name, count(friend) AS friend_count
ORDER BY friend_count DESC
LIMIT 10

Pattern 4: Community Detection

“Find groups of mutual friends” (triangles):
MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person)-[:KNOWS]->(a)
RETURN a.name, b.name, c.name

Pattern 5: Hierarchical Data

“Find all employees under a manager” (recursive):
MATCH (manager:Employee {name: "Alice"})-[:MANAGES*]->(employee)
RETURN employee.name, length(path) AS levels_down

Pattern 6: Time-Series Analysis

“What did users buy in the last 30 days?”:
MATCH (user:User)-[p:PURCHASED]->(product:Product)
WHERE p.timestamp > datetime() - duration({days: 30})
RETURN user.name, collect(product.name) AS recent_purchases

Part 8: Hands-On Exercises

Exercise 1: Social Network Queries

Setup:
CREATE (alice:Person {name: "Alice", age: 30, city: "NYC"})
CREATE (bob:Person {name: "Bob", age: 28, city: "SF"})
CREATE (charlie:Person {name: "Charlie", age: 35, city: "NYC"})
CREATE (alice)-[:KNOWS {since: 2015}]->(bob)
CREATE (bob)-[:KNOWS {since: 2018}]->(charlie)
CREATE (charlie)-[:KNOWS {since: 2020}]->(alice)
Queries:
  1. Find Alice’s direct friends:
MATCH (alice:Person {name: "Alice"})-[:KNOWS]->(friend)
RETURN friend.name
  1. Friends of friends (excluding Alice):
MATCH (alice:Person {name: "Alice"})-[:KNOWS*2]->(fof)
WHERE fof <> alice
RETURN DISTINCT fof.name
  1. Mutual friends of Alice and Bob:
MATCH (alice:Person {name: "Alice"})-[:KNOWS]->(mutual)<-[:KNOWS]-(bob:Person {name: "Bob"})
RETURN mutual.name
  1. Average age of Alice’s friends:
MATCH (alice:Person {name: "Alice"})-[:KNOWS]->(friend)
RETURN avg(friend.age) AS avg_age

Exercise 2: Movie Recommendations

Setup:
CREATE (alice:User {name: "Alice"})
CREATE (bob:User {name: "Bob"})
CREATE (matrix:Movie {title: "The Matrix", year: 1999})
CREATE (inception:Movie {title: "Inception", year: 2010})
CREATE (alice)-[:WATCHED]->(matrix)
CREATE (alice)-[:WATCHED]->(inception)
CREATE (bob)-[:WATCHED]->(matrix)
Query: Recommend movies Bob hasn’t seen (based on Alice):
MATCH (bob:User {name: "Bob"})-[:WATCHED]->(watched:Movie)
MATCH (watched)<-[:WATCHED]-(other:User)-[:WATCHED]->(rec:Movie)
WHERE NOT (bob)-[:WATCHED]->(rec)
RETURN rec.title, count(*) AS score
ORDER BY score DESC

Exercise 3: Performance Optimization

Original query (slow):
MATCH (p:Person)-[:KNOWS]->(friend)
WHERE p.name = "Alice"
RETURN friend.name
Optimized:
CREATE INDEX person_name FOR (p:Person) ON (p.name);

MATCH (p:Person {name: "Alice"})-[:KNOWS]->(friend)
RETURN friend.name
Verify improvement:
PROFILE [query]
-- Compare DB Hits before and after

Summary

Pattern Matching: Use ASCII art for intuitive graph patterns Filtering: WHERE clause for predicates, pattern-based filtering Aggregations: count, avg, sum, collect for grouped data Variable Paths: *min..max for flexible traversals Functions: Rich library for strings, math, lists, dates Optimization: Indexes, early filtering, PROFILE for analysis Next Steps: Apply Cypher to real-world data modeling scenarios!

What’s Next?

Module 5: Graph Data Modeling

Design efficient graph schemas, handle many-to-many relationships, and model complex domains