RSS Amplifier

Aayush Ostwal · Jun 10, 2026

Thinking in Graphs: A Cypher Crash Course for SQL Engineers

0
Sign in to vote or save

Aayush Ostwal · Aayush Ostwal

If you’ve spent your career writing SQL, you already know how to model the world in tables, rows, and foreign keys. But when you start working with Neo4j or other graph databases, that relational muscle memory can actually get in your way.

The biggest hurdle in learning Cypher isn’t the syntax — it’s the mental model.

This guide is designed to bridge that gap. We’re going to map what you already know (SQL) directly to what you need to learn (Cypher), starting from the basics and moving up to queries that would make a traditional relational database sweat.

Here is the only translation that really matters:

In SQL, relationships are implied by matching IDs across tables. You have to write the join logic every time.

In Cypher, relationships are physical edges saved in the database. You don’t join; you traverse.

To make this concrete, let’s look at a standard E-commerce/Social platform.

Relational Schema (What you’re used to):

  • users (id, name, city)

  • products (id, name, price)

  • orders (id, user_id, date)

  • order_items (order_id, product_id) — join table

  • reviews (user_id, product_id, rating, text) — join table with metadata

  • follows (follower_id, followee_id) — self-join table

Graph Schema (What we’re building):

In a graph, those join tables disappear. Instead, we have Nodes (entities) and Relationships (verbs connecting them).

(:User)-[:PLACED]->(:Order)
(:Order)-[:CONTAINS]->(:Product)
(:User)-[:REVIEWS {rating, text}]->(:Product)
(:User)-[:FOLLOWS]->(:User)

Before we translate, here’s how you draw graph patterns in Cypher using ASCII art.

Nodes are wrapped in parentheses:

(u:User)

  • u is the variable name (like a SQL alias).

  • User is the Label (like a table name).

Relationships are wrapped in brackets:

-[r:PLACED]->

  • r is the variable.

  • PLACED is the relationship type.

  • -> indicates the direction of the relationship.

Combine them to form a path:

(u:User)-[:PLACED]->(o:Order)

Let’s translate common SQL patterns directly into Cypher.

SQL:

SELECT id, name FROM users LIMIT 10;

Cypher:

MATCH (u:User)
RETURN u.id, u.name
LIMIT 10

SQL:

SELECT name FROM users WHERE city = ‘Seattle’ ORDER BY name;

Cypher:

MATCH (u:User)
WHERE u.city = “Seattle”
RETURN u.name
ORDER BY u.name

(Note: Cypher can also do inline filtering: MATCH (u:User {city: "Seattle"}))

SQL:

SELECT name FROM products WHERE LOWER(name) LIKE ‘%laptop%’;

Cypher:

MATCH (p:Product)
WHERE toLower(p.name) CONTAINS “laptop”
RETURN p.name

Foreign keys disappear here. We just describe the visual connection.

SQL:

SELECT u.name, o.id, o.date
FROM users u
JOIN orders o ON o.user_id = u.id;

Cypher:

MATCH (u:User)-[:PLACED]->(o:Order)
RETURN u.name, o.id, o.date

Join tables (order_items) completely vanish.

SQL:

SELECT o.id, p.name
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id;

Cypher:

MATCH (o:Order)-[:CONTAINS]->(p:Product)
RETURN o.id, p.name

This is where Cypher starts to look much cleaner than SQL. Let’s find out what a user actually bought.

SQL:

SELECT u.name, p.name
FROM users u
JOIN orders o ON o.user_id = u.id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id;

Cypher:

MATCH (u:User)-[:PLACED]->(:Order)-[:CONTAINS]->(p:Product)
RETURN u.name, p.name

In our graph, REVIEWS is a relationship that holds properties (rating).

SQL:

SELECT u.name, r.rating, p.name
FROM users u
JOIN reviews r ON r.user_id = u.id
JOIN products p ON p.id = r.product_id
WHERE r.rating >= 4;

Cypher:

MATCH (u:User)-[r:REVIEWS]->(p:Product)
WHERE r.rating >= 4
RETURN u.name, r.rating, p.name

SQL:

SELECT u.name, o.id
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;

Cypher:

MATCH (u:User)
OPTIONAL MATCH (u)-[:PLACED]->(o:Order)
RETURN u.name, o.id

Cypher infers the grouping key automatically based on what you RETURN.

SQL:

SELECT city, COUNT(*) as user_count
FROM users
GROUP BY city;

Cypher:

MATCH (u:User)
RETURN u.city, count(u) AS user_count

Cypher uses the WITH clause to pass intermediate results down the pipeline. Think of WITH as a temporary CTE.

SQL:

SELECT u.name, COUNT(o.id) as order_count
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.name
HAVING COUNT(o.id) >= 5;

Cypher:

MATCH (u:User)-[:PLACED]->(o:Order)
WITH u, count(o) AS order_count
WHERE order_count >= 5
RETURN u.name, order_count

SQL:

SELECT u1.name, u2.name
FROM users u1
JOIN follows f ON f.follower_id = u1.id
JOIN users u2 ON u2.id = f.followee_id;

Cypher:

MATCH (u1:User)-[:FOLLOWS]->(u2:User)
RETURN u1.name, u2.name

Find users who have reviewed a specific product.

SQL:

SELECT u.name FROM users u
WHERE EXISTS (
  SELECT 1 FROM reviews r
  JOIN products p ON p.id = r.product_id
  WHERE r.user_id = u.id AND p.name = ‘Mechanical Keyboard’
);

Cypher:

MATCH (u:User)
WHERE EXISTS {
  MATCH (u)-[:REVIEWS]->(:Product {name: “Mechanical Keyboard”})
}
RETURN u.name

Gathering rows into a list/array is a first-class feature in Cypher.

SQL:

SELECT u.name, array_agg(p.name) as reviewed_products
FROM users u
JOIN reviews r ON r.user_id = u.id
JOIN products p ON p.id = r.product_id
GROUP BY u.name;

Cypher:

MATCH (u:User)-[:REVIEWS]->(p:Product)
RETURN u.name, collect(p.name) AS reviewed_products

Translating SQL is great, but graphs exist to solve problems that make relational databases miserable. Here are queries that are natural in Cypher but a nightmare in SQL.

Question: Who are the friends-of-friends-of-friends of Alice? (Up to 3 hops away)

In SQL, this requires recursive CTEs or heavy hardcoded union/join logic. In Cypher, it’s just a number:

MATCH (alice:User {name: “Alice”})-[:FOLLOWS*1..3]->(connection:User)
RETURN DISTINCT connection.name

Question: Show me the exact route of connections between User A and User B.

SQL returns rows and columns. It doesn’t understand “paths” natively. Cypher can return the whole journey as an object:

MATCH path = (u1:User {name: “Alice”})-[:FOLLOWS*]->(u2:User {name: “Dave”})
RETURN path
LIMIT 1

Question: Users who bought this item also bought what else?

In SQL, this is a massive self-join against the heavy order_items table. In Cypher, it’s a simple zigzag pattern:

MATCH (target:Product {name: “Coffee Grinder”})<-[:CONTAINS]-(:Order)<-[:PLACED]-(otherUser:User)
MATCH (otherUser)-[:PLACED]->(:Order)-[:CONTAINS]->(rec:Product)
WHERE rec.id <> target.id
RETURN rec.name, count(rec) as frequency
ORDER BY frequency DESC
LIMIT 5
  1. Forgetting LIMIT: When testing, never just run MATCH (n) RETURN n. In a large graph, the browser will try to render thousands of nodes and crash. Always use LIMIT 25.

  2. Ignoring Direction: Arrows matter. (u)-[:PLACED]->(o) works. (u)<-[:PLACED]-(o) will return 0 rows. If you don’t care about direction, leave the arrowhead off: (u)-[:FOLLOWS]-(u2).

  3. Returning Full Nodes Instead of Properties: RETURN u sends the whole JSON object back. In production APIs, be explicit: RETURN u.id, u.name.

  4. Forgetting DISTINCT on Multi-Hop Queries: When you traverse multiple paths, you will hit the same nodes multiple times. Always use RETURN DISTINCT unless you actually want to count the number of paths.

  5. Thinking in Tables: If your instinct is to artificially map an ID from one node into the properties of another node just to link them — stop. Create a relationship edge instead.

Want to see if it clicked? Try writing the Cypher for these questions based on our schema, without looking up the syntax!

  1. Find the names of all Products that have a price greater than 100.

  2. Return the names of Users who have placed an order, along with the total number of orders they’ve placed, sorted highest to lowest.

  3. Find all Users who gave a 1-star REVIEW to any product, and return the User’s name and the Product’s name.

  4. Challenge: Find “Mutuals” — Users who follow someone, who also follows them back.

(Hint for the challenge: You can draw arrows pointing both ways in the same MATCH block!)

Final Takeaway: Stop visualizing spreadsheets. Start visualizing whiteboards. When you map out your domain as circles connected by lines on a whiteboard, you’ve essentially already written your Cypher query.

Read the original on aayushostwal2.substack.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.