Skip to content
wannacode

Graph databases

In short: A graph database such as Neo4j stores nodes and relationships instead of tables, and Cypher queries match patterns and paths through them.

Problem

People who bought coffee also bought

Under every product the shop wants a line “People who bought this also bought”. For the coffee beans the query on the SQL database is this:

SQLdatabase: shop

It walks through six tables: from the product to the order lines, to the orders, to the customer, to their other orders, to the lines of those, and to the products. orders and order_items take part twice each. The next wish is heavier still: “the products of the people whose recommendations you follow, and of the people they follow”. Every step is more joins, and “up to three steps” needs WITH RECURSIVE. The question is about connections between things, and in tables the connections are scattered across foreign keys.

The idea

Nodes and relationships

A graph database keeps the data as a graph: nodes (the things, such as a customer and a product) and relationships between them. Every relationship has a type (BOUGHT) and a direction (from the customer to the product). Nodes have labels (:Customer), and both nodes and relationships have properties (name, price, quantity). The relationship is stored in the database, not looked up with a JOIN in every query: from a node the database goes straight to its neighbours.

The best known graph database is Neo4j, and its language is Cypher. Its patterns are drawings made of characters: (c:Customer) is a node, and -[:BOUGHT]-> is an arrow.

graph (like Neo4j)simulator

The query ends with ;, so it can span several lines. CREATE makes nodes and relationships. MATCH finds everything that fits the pattern, and RETURN says what to show. Under the output the simulator draws the nodes and relationships it found.

More precisely, if you are curious

The simulator knows part of Cypher: MATCH, WHERE, CREATE, MERGE, SET, DELETE, DETACH DELETE, RETURN, ORDER BY, SKIP, LIMIT and a few functions. There is no WITH, OPTIONAL MATCH, UNWIND, no indexes and no constraints. The output looks like the cypher-shell console, without the line with the query time, and the error messages are shorter. The simulator shows the prompt before every line of a query. Neo4j Browser does not need ; at the end and draws the result by itself. Without ORDER BY the simulator returns the rows in the order the nodes were made, and Neo4j promises no order at all.

Try it

Georgi and the bread

The database is empty. Record that the customer Georgi Petrov bought white bread: a node labelled Customer with a name property, a node labelled Product with a name property, and a BOUGHT relationship from the customer to the product.

graph (like Neo4j)simulator

The idea

Patterns

From here on the database is the shop: 20 customers, 25 products in 6 categories and 109 BOUGHT relationships, one for every customer and product from the orders that were not cancelled. They come from the course's SQL database. A product is connected to its category with an IN relationship.

A pattern can be as long as you need: from the customer through the product to the category, without a single join.

graph (like Neo4j)simulator

The direction in the pattern has to match the direction of the relationship:

graph (like Neo4j)simulator

There are no BOUGHT arrows from the product to a customer, so the first query finds nobody. <-[:BOUGHT]- reads the same relationship from its other end. -[:BOUGHT]- without an arrowhead finds the relationship in either direction.

More precisely, if you are curious

The graph does not keep the individual orders: a BOUGHT relationship only says that the customer has bought the product. If you need the orders too, they become nodes: (:Customer)-[:PLACED]->(:Order)-[:CONTAINS {quantity: 2}]->(:Product). What is a node and what is a relationship depends on the questions you will ask, just like the schema of an SQL database.

See it

The same question, one pattern

Here is “people who bought coffee also bought” in Cypher:

graph (like Neo4j)simulator

The answer is the same as the SQL query's at the start, row by row. The pattern reads from left to right: the coffee, bought by a customer who also bought another product. The coffee is not counted against itself: in one MATCH the same relationship cannot take part twice, and the relationship to the coffee is already taken. In SQL that was a separate condition (p2.id <> p1.id). Only three people bought coffee, so all five products have two buyers each and are ordered by name.

The difference is not only the length. The SQL database looks up every join in an index or in the whole table. The graph database starts from the coffee and follows only its relationships: the work depends on how many relationships are around the coffee, not on how many orders the shop has.

The idea

Variable-length paths

Say the shop has profiles, and every customer can follow the recommendations of other customers: (:Customer)-[:FOLLOWS]->(:Customer). These relationships are invented for the lesson; the SQL database does not have them.

-[:FOLLOWS*1..2]-> is a path of one to two FOLLOWS steps. p = … gives the whole path a name, and length(p) is its number of steps:

graph (like Neo4j)simulator

In the first result Viktoria is there twice: Elena follows Maria and Dimitar, and both of them follow Viktoria. MATCH returns every path, not every person. In the second query WHERE f <> me removes Elena herself: in three steps the path comes back to her (Elena, Maria, Viktoria, Elena).

Who is on the path shows with nodes(p):

graph (like Neo4j)simulator

In SQL the same is WITH RECURSIVE over a follows table: a start, UNION ALL and a step that joins follows with the previous rows, plus a step counter. Here it is *1..3.

See it

A path without a bound

A pattern with no type and no direction, “everything connected to Elena”, with a longer and longer path:

graph (like Neo4j)simulator

98 routes, then 688, then 4,370. Every step multiplies them by six to seven, because a product like white bread is connected to many customers, and each of them to many products. * without an upper bound tries every route. The simulator stops after 100,000 steps, and Neo4j does not stop by itself: the query runs until it runs out of time or memory. So always write an upper bound, and when you can, the type and direction of the relationship too.

Exercise

Recommendations

1. The cheese also needs a “People who bought this also bought” line.

Which three products did the people who bought white cheese buy most? Show the name and the number of buyers (buyers), from the most down, and by name when the numbers are equal.

graph (like Neo4j)simulator

2. Maria Ivanova follows a few people, and they follow others.

Show the customers Maria Ivanova reaches through FOLLOWS in one or two steps, each once, ordered by name.

graph (like Neo4j)simulator

3. Now the real recommendation.

Find the products bought by the people Elena Dimitrova follows in one or two steps, which she has not bought yet. Show the three bought by the most different people (people), and by name when the numbers are equal.

graph (like Neo4j)simulator

The idea

When a graph database, and when not

A graph database is good when the question is about connections, often several steps long: recommendations (“people who bought this also bought”), social networks, fraud detection (accounts that share a phone or an address), routes and dependencies, access rights through groups. It is not good for reports that add up numbers from all records, such as revenue by month: such a query does not follow relationships, it reads everything in a row. That is a job for SQL with GROUP BY or for a database built for reports (OLAP). So a graph usually sits next to the SQL database: the orders and the money stay in SQL, and a copy of the connections goes to the graph for the recommendations.

More precisely, if you are curious

Besides Neo4j, Cypher is understood by Memgraph and Amazon Neptune (there the language is called openCypher; Neptune also understands Gremlin and SPARQL), and the Apache AGE extension adds Cypher to PostgreSQL. In 2024 the international standard GQL (ISO/IEC 39075) came out, largely based on the ideas of Cypher. Another kind of graph database is the RDF store with the SPARQL language, where everything is a triple of “subject, predicate, object”.

Test

Check yourself

1. What does the pattern (c:Customer)-[:BOUGHT]->(p:Product) say?
2. MATCH (me)-[:FOLLOWS*1..2]->(f) RETURN f.name returns Viktoria twice. Why?
3. What happens to a pattern -[*]- without an upper bound in a large graph?
4. You need the revenue by month for the last two years. Where does this query belong?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·