Neo4j Cypher Cheat Sheet

Graph databases like Neo4j are gaining traction as powerful alternatives to traditional relational databases—especially when navigating complex relationships in data. Unlike SQL databases, which rely on foreign keys and multi-table joins to express connections, graph databases store data as nodes and relationships, enabling intuitive and high-performance traversal of deeply interconnected structures. It makes them ideal for use cases like recommendation engines, fraud detection, knowledge graphs, and social networks—where relationships matter as much as the data.

Neo4j, the most popular graph database, uses a declarative query language called Cypher, designed to make relationship-focused queries both expressive and readable. But how does this graph-based approach compare to traditional SQL in practice?

To answer that, this post will take the classic Northwind database, a well-known sample dataset originally built for relational systems, and reimagine it as a graph. I’ll describe how common queries are expressed in SQL and Cypher, highlighting the differences in modeling, query complexity, and real-world usability. Whether you’re a SQL veteran curious about graph databases or looking to understand where Neo4j fits in the broader database ecosystem, this comparison offers a practical side-by-side view.

The Northwind database models the internal operations of a fictional company that imports and exports specialty foods worldwide. It includes data about customers, orders, products, employees, suppliers, and shipping details, offering a realistic yet manageable snapshot of a typical business’s sales and supply chain workflow. For example, customers place orders that contain multiple products, each supplied by different vendors. Employees are organized hierarchically and are assigned to manage specific orders, while shipments are routed through carriers to fulfill customer requests.

Despite its relatively small size, Northwind’s schema reflects a rich network of relationships. Products belong to categories and are supplied by vendors. Orders are linked to customers, employees, and shipping methods. It creates a network of interconnected entities that mimics the real-world complexity of business data. In a traditional SQL database, these relationships are enforced through foreign keys and JOIN operations. In a graph database like Neo4j, they become direct relationships between nodes—making Northwind an ideal candidate for comparing the relational and graph data models side-by-side.

Northwind Database Structure

In Neo4j, the Northwind database is modeled as a graph, where each entity, such as a customer, order, product, or employee, is represented as a node, and the connections between them become relationships (edges). Instead of using foreign keys, relationships in Neo4j are explicit and stored directly between nodes, allowing for fast and intuitive traversal. Each node is assigned one or more labels to indicate its type, such as :Customer, :Order, :Product, or :Supplier. Relationships between these nodes use relationship types like :PURCHASED (between a customer and an order), :ORDERS (between an order and its products), or :SUPPLIES (between a supplier and a product).

Northwind Database Graph

This approach mirrors the business logic naturally, making queries about connected data more expressive and often more efficient than traditional SQL joins.

To demonstrate the practical differences between SQL and Cypher, the next section of this post will serve as a Cypher cheat sheet, translating common SQL queries into their graph-based equivalents. These examples will be drawn directly from typical use cases in the Northwind database—such as finding a customer’s order history, retrieving products from a specific supplier, or analyzing co-purchased items. By comparing each SQL query side-by-side with its Cypher counterpart, you’ll see how graph queries eliminate complex joins and bring clarity to relationship-driven logic. Whether you’re just starting with Neo4j or looking to deepen your understanding, this cheat sheet offers a hands-on look at how Cypher approaches problems differently from traditional relational databases.

Get all columns from the Customers table.
SELECT * FROM Customers
MATCH (c:Customer) RETURN c
Get the top 25 Customers alphabetically by Country and name.
SELECT TOP 25 *
FROM Customers
ORDER BY Country, ContactName
MATCH (c:Customer)
ORDER BY c.country, c.contactName
RETURN c
LIMIT 25;
Get the count of all Orders made during 1997.
SELECT COUNT(*)
FROM Orders
WHERE YEAR(OrderDate) = 1997
MATCH (o:Order)
WHERE o.orderDate.year = 1997
RETURN COUNT(o);
Get all orders placed on the 19th of May, 1997.
SELECT *
FROM Orders
WHERE OrderDate = '19970319'
MATCH (o:Order)
WHERE o.orderDate = date('1997-03-19')
RETURN o;
Create a report for all the orders of 1996 and their Customers.
SELECT *
FROM Orders o
INNER JOIN Customers c ON o.CustomerID = c.CustomerID
WHERE YEAR(o.OrderDate) = 1996
MATCH p=(c:Customer)-[:PURCHASED]->(o:Order)
WHERE o.orderDate.year = 1996
RETURN p;
Create a report for all 1996 orders and their Customers. Return only the Order ID, Order Date, Customer ID, Name, and Country.
SELECT o.OrderID, o.OrderDate, c.CustomerID, c.ContactName, c.Country
FROM Orders o
INNER JOIN Customers c ON o.CustomerID = c.CustomerID
WHERE YEAR(o.OrderDate) = 1996
MATCH (c:Customer)-[:PURCHASED]->(o:Order)
WHERE o.orderDate.year = 1996
RETURN o.orderID, o.orderDate, c.customerID, c.contactName, c.country;
Create a report that shows the number of customers from each city.
SELECT c.City, COUNT(*)
FROM Orders o
INNER JOIN Customers c ON o.CustomerID = c.CustomerID
GROUP BY c.City
MATCH (c:Customer)-[:PURCHASED]->(o:Order)
RETURN c.city, COUNT(*);
Create a report that shows the total quantity of products ordered. Only show records for products for which the quantity ordered is fewer than 200
SELECT p.ProductName, SUM(od.Quantity) as Quantity
FROM OrderDetails od
INNER JOIN Products p ON od.ProductID = p.ProductID
GROUP BY p.ProductName
HAVING SUM(od.Quantity) < 200
ORDER BY Quantity
MATCH (o:Order)-[od:ORDERS]->(p:Product)
WITH p.productName as productName, SUM(od.quantity) as quantity
WHERE quantity < 200
ORDER BY quantity
RETURN productName, quantity;
Create a report that shows the total number of orders by Customer since December 31, 1996. The report should only return rows for which the total number of orders is greater than 15
SELECT c.ContactName, COUNT(o.OrderID) as TotalOrders
FROM Orders o
INNER JOIN Customers c ON o.CustomerID = c.CustomerID
WHERE OrderDate > '1996-12-31'
GROUP BY c.ContactName
HAVING COUNT(o.OrderID) > 15
MATCH (c:Customer)-[:PURCHASED]->(o:Order)
WHERE o.orderDate > date("1996-12-31")
WITH c.contactName as contactName, COUNT(o.orderID) as totalOrders
WHERE totalOrders > 15
RETURN contactName, totalOrders;
Which UK Customers have paid more than 1000 dollars
SELECT c.ContactName, SUM(od.UnitPrice * od.Quantity * (1 - od.Discount)) as Paid
FROM Customers c
INNER JOIN Orders o ON c.CustomerID = o.CustomerID
INNER JOIN OrderDetails od ON o.OrderID = od.OrderID
WHERE c.Country = 'UK'
GROUP BY c.ContactName
HAVING SUM(od.UnitPrice * od.Quantity * (1 - od.Discount)) > 1000
MATCH (c:Customer)-[:PURCHASED]->(o:Order)-[od:ORDERS]->(p:Product)
WHERE c.country = 'UK’
WITH c.contactName as contactName, SUM(toFloat(od.unitPrice) * od.quantity * (1 - toFLoat(od.discount))) as paid
WHERE paid > 1000
RETURN contactName, paid;
Insert yourself into the Customers table Include the following fields: CustomerID, CompanyName, ContactName, ContactTitle, Address, City, Region, PostalCode, Country, Phone, Fax
INSERT INTO Customers (CustomerID, CompanyName, ContactName, ContactTitle, Address, City, Region, PostalCode, Country, Phone, Fax)
VALUES ('ILYA1', 'Acme Corp', 'Ilya Verbitskiy', 'Manager', '123 Main St', 'New York', 'NY', '10001', 'USA', '555-1234', '555-5678')

Insert node:

CREATE (c:Customer {
  customerID: 'ILYA1',
  companyName: 'Acme Corp',
  contactName: 'Ilya Verbitskiy',
  contactTitle: 'Manager',
  address: '123 Main St',
  city: 'New York',
  region: 'NY',
  postalCode: '10001',
  country: 'USA',
  phone: '555-1234',
  fax: '555-5678’
})

Upsert node:

MERGE (c:Customer {customerID: 'ILYA1'})
SET c.companyName = 'Acme Corp',
  c.contactName = 'Ilya Verbitskiy',
  c.contactTitle = 'Manager',
  c.address = '123 Main St',
  c.city = 'New York',
  c.region = 'NY',
  c.postalCode = '10001',
  c.country = 'USA',
  c.phone = '555-1234',
  c.fax = '555-5678'
Insert multiple entities within a transaction
BEGIN TRANSACTION

INSERT Orders(CustomerID, EmployeeID, OrderDate)
VALUES ('ILYA', 5, GETDATE())

DECLARE @LastOrderID INT = SCOPE_IDENTITY()
DECLARE @ProductId INT
SELECT @ProductId = ProductID FROM Products WHERE ProductName = 'Tofu'

INSERT OrderDetails(OrderID, ProductID, UnitPrice, Quantity, Discount)
VALUES (@LastOrderID, @ProductId, 10, 8, 0)

COMMIT
MATCH (c:Customer {customerID: 'ILYA’})
MATCH (p:Product {productName: 'Tofu’})
MERGE (o:Order {orderID: "9090"}) SET o.orderDate = date()
MERGE u=(c)-[:PURCHASED]->(o)
MERGE v=(o)-[po:ORDERS {unitPrice: "10", quantity: 8, discount: "0", productID: p.productID, orderID: o.orderID}]->(p)
RETURN u, v
Change Order.orderDate data type from String to Date
MERGE (o:Order)
SET o.orderDate = date(substring(o.orderDate, 0, 10))
Update the phone number.
UPDATE Customers SET Phone = '000-4321' WHERE CustomerID = 'ILYA'
MERGE (c:Customer {customerID: 'ILYA'})
SET c.phone = '000-4321';
Double the quantity of the order details record you inserted before
UPDATE od
SET Quantity = od.Quantity * 2
FROM OrderDetails od
INNER JOIN Orders o ON od.OrderID = o.OrderID
WHERE o.OrderID = 11084
MATCH (o:Order {orderID: "9090"})-[od:ORDERS]->(p:Product {productID: "14"})
SET od.quantity = od.quantity * 2
RETURN o, od, p;
Delete the records you inserted before. Don't delete any other records.
BEGIN TRANSACTION
DELETE od
FROM OrderDetails od
INNER JOIN dbo.Orders O on O.OrderID = od.OrderID
INNER JOIN Customers c ON o.CustomerID = c.CustomerID
WHERE c.CustomerID = 'ILYA'

DELETE o
FROM Orders o INNER JOIN Customers c ON o.CustomerID = c.CustomerID
WHERE c.CustomerID = 'ILYA'

DELETE Customers WHERE CustomerID = 'ILYA'
COMMIT
MATCH (c:Customer {customerID: "ILYA1"})-[:PURCHASED]->(o:Order)-[:ORDERS]->(p:Product)
DETACH DELETE c, o;

This cheat sheet highlights how graph databases simplify working with connected data by reimagining the Northwind database in Neo4j and translating common SQL queries into Cypher. Neo4j’s intuitive node-and-relationship model eliminates complex joins and makes relationship-driven queries more expressive, maintainable, and performant. Whether you’re exploring Neo4j for the first time or looking to deepen your understanding of Cypher, this post offers a practical, side-by-side comparison showing how graph thinking can transform how we query and structure data.

If you’re considering adopting Neo4j or need help modernizing your data architecture, feel free to contact me. I offer consulting services to help teams design and implement graph-based solutions that scale.

Rererences