ORDER BY

ORDER BY is a subclause that determines how the results of a RETURN or WITH clause are ordered. As of Neo4j 5.24, it can also be used as a standalone clause, either on its own or in combination with SKIP/OFFSET or LIMIT.

ORDER BY defaults to sorting results in an ascending order, though it can be modified to sort results in a descending order.

ORDER BY relies on comparisons to sort the output (see Equality, ordering, and comparison of value types for more details). You can sort on different values, such as node or relationship properties, IDs, or the result of expressions.

Unless ORDER BY is used, Neo4j does not guarantee the row order of a query result.

Example graph

A graph with the following schema is used for the examples below:

graph order by clause

To recreate it, run the following query against an empty Neo4j database:

CREATE (o1:Order {id: 'ORD-001', orderDate: datetime('2024-05-01T10:00:00'), total: 550, status: 'shipped'}),
       (o2:Order {id: 'ORD-002', orderDate: datetime('2024-05-02T14:30:00'), total: 1000, status: 'pending'}),
       (o3:Order {id: 'ORD-003', orderDate: datetime('2024-05-03T09:15:00'), total: 550, status: 'pending'}),
       (o4:Order {id: 'ORD-004', orderDate: datetime('2024-05-04T12:45:00'), total: 200}),
       (o5:Order {id: 'ORD-005', orderDate: datetime('2024-05-05T15:00:00'), total: 800, status: 'shipped'}),

       (i1:Item {name: 'Phone', price: 500}),
       (i2:Item {name: 'Laptop', price: 1000}),
       (i3:Item {name: 'Headphones', price: 250}),
       (i4:Item {name: 'Charger', price: 50}),
       (i5:Item {name: 'Keyboard', price: 200}),

       (o1)-[:CONTAINS]->(i1),
       (o1)-[:CONTAINS]->(i4),
       (o2)-[:CONTAINS]->(i2),
       (o3)-[:CONTAINS]->(i1),
       (o3)-[:CONTAINS]->(i4),
       (o4)-[:CONTAINS]->(i5),
       (o5)-[:CONTAINS]->(i1),
       (o5)-[:CONTAINS]->(i3),
       (o5)-[:CONTAINS]->(i4)

Basic examples

Example 1. Order by property values

ORDER BY can be used to sort the result by property values.

Order by node property
MATCH (o:Order)
RETURN o.id AS order,
       o.total AS total
  ORDER BY total

The nodes are returned, sorted by the value of the total properties in an ascending order.

Result
order total

"ORD-004"

200

"ORD-001"

550

"ORD-003"

550

"ORD-005"

800

"ORD-002"

1000

Rows: 5

Example 2. Order by multiple property values

Order by multiple property values by listing two or more properties in the ORDER BY subclause. Cypher® sorts by the first property, and if values are equal, it moves to the next property, and so on.

Order by multiple properties
MATCH (o:Order)
RETURN o.id AS order,
       o.total AS total,
       o.orderDate AS orderDate
  ORDER BY total,
           orderDate

This returns the nodes, sorted first by their total property, and then, for equal values, by their orderDate property.

Result
order total orderDate

"ORD-004"

200

2024-05-04T12:45Z

"ORD-001"

550

2024-05-01T10:00Z

"ORD-003"

550

2024-05-03T09:15Z

"ORD-005"

800

2024-05-05T15:00Z

"ORD-002"

1000

2024-05-02T14:30Z

Rows: 5

Example 3. Order by ID

ORDER BY can be used to sort nodes or relationships by their ID (retrieved by either the elementId() or id() functions).

Order by element ID
MATCH (o:Order)
RETURN o.id AS order,
       elementId(o) AS elementId
  ORDER BY elementId
Result
order elementId

"ORD-001"

"4:9350eddd-5a35-413d-8684-708b1da35d23:0"

"ORD-002"

"4:9350eddd-5a35-413d-8684-708b1da35d23:1"

"ORD-003"

"4:9350eddd-5a35-413d-8684-708b1da35d23:2"

"ORD-004"

"4:9350eddd-5a35-413d-8684-708b1da35d23:3"

"ORD-005"

"4:9350eddd-5a35-413d-8684-708b1da35d23:4"

Rows: 5

Neo4j reuses its internal IDs when nodes and relationships are deleted. Applications relying on internal Neo4j IDs are, as a result, brittle and can be inaccurate. It is recommended to use application-generated IDs instead.
Example 4. Order by expressions

ORDER BY can be used to sort according to the results of an expression. The below query calculates a 10% discount on each order’s total property value, and then orders the results by the discounted total.

Order by an expression result
MATCH (o:Order)
RETURN o.id AS order,
       o.total * 0.9 AS discountedTotal
  ORDER BY discountedTotal
Result
order discountedTotal

"ORD-004"

180.0

"ORD-001"

495.0

"ORD-003"

495.0

"ORD-005"

720.0

"ORD-002"

900.0

Rows: 5

This next query counts the number of items contained in each order and then orders the results by the item count.

Order by an expression result
MATCH (o:Order)
RETURN o.id AS order,
      COUNT { (o)-[:CONTAINS]->(:Item) } AS itemCount
  ORDER BY itemCount
Result
order itemCount

"ORD-002"

1

"ORD-004"

1

"ORD-001"

2

"ORD-003"

2

"ORD-005"

3

Rows: 5

Order by values not in the result

ORDER BY can sort by values that are not included in the result set. That is, the sort key does not need to be part of the preceding RETURN or WITH clause. For example, the query below sorts orders based on how many items they contain, even though that count is not returned.

Order by values not in the returned results
MATCH (o:Order)
RETURN o.id AS order
  ORDER BY COUNT { (o)-[:CONTAINS]->(:Item) }
Result
order

"ORD-002"

"ORD-004"

"ORD-001"

"ORD-003"

"ORD-005"

Rows: 5

Ascending and descending order

ORDER BY sorts results in an ascending order by default. To explicitly sort results in an ascending order, append ASC[ENDING].

Explicitly sort results in an ascending order
MATCH (o:Order)
RETURN o.id AS order,
       o.total AS total
  ORDER BY total ASC
Result
order total

"ORD-004"

200

"ORD-001"

550

"ORD-003"

550

"ORD-005"

800

"ORD-002"

1000

Rows: 5

To sort results in a descending order, append DESC[ENDING].

Sort results in a descending order
MATCH (o:Order)
RETURN o.id AS order,
       o.total AS total
  ORDER BY total DESC
Result
order total

"ORD-002"

1000

"ORD-005"

800

"ORD-001"

550

"ORD-003"

550

"ORD-004"

200

Rows: 5

ORDER BY can combine ascending and descending ordering. In the example below, results are sorted first by total values in descending order and then by orderDate values in ascending order.

Combine ascending and descending result ordering
MATCH (o:Order)
RETURN o.id AS order,
       o.total AS total,
       o.orderDate AS orderDate
  ORDER BY total DESC,
           orderDate ASC
Result
order total orderDate

"ORD-002"

1000

2024-05-02T14:30Z

"ORD-005"

800

2024-05-05T15:00Z

"ORD-001"

550

2024-05-01T10:00Z

"ORD-003"

550

2024-05-03T09:15Z

"ORD-004"

200

2024-05-04T12:45Z

Rows: 5

ORDER BY and pattern matching

ORDER BY can be used to sort results before continuing with additional pattern matching. In the example below, it is combined with the LIMIT to first sort Order nodes by their orderDate property values, limit the result to the most recent Order, and then match any connected Item nodes. Also note that ORDER BY and LIMIT are used as standalone clauses and not as subclauses in this example.

Find the items contained in the most recently placed order
MATCH (o:Order)
ORDER BY o.orderDate DESC
LIMIT 1
MATCH (o)-[:CONTAINS]->(i:Item)
RETURN o.id AS order,
       o.total,
       collect(i.name) AS items
Result
order total items

"ORD-005"

800

["Phone", "Headphones", "Charger"]

Rows: 1

Null values

When sorting, null values appear last in ascending order and first in descending order.

Sort on null property
MATCH (o:Order)
RETURN o.id AS order,
       o.status AS status
  ORDER BY status DESC
Result
order status

"ORD-004"

null

"ORD-001"

"shipped"

"ORD-005"

"shipped"

"ORD-002"

"pending"

"ORD-003"

"pending"

Rows: 5

ORDER BY and the WITH clause

When ORDER BY is present on a WITH clause, the immediately following clause will receive records in the specified order. This guaranteed order is useful for operations that rely on the sequence in which values are processed. For example, appending ORDER BY to a WITH clause can be used to control the order of items in the list produced by the collect() aggregating function. The MERGE and SET clauses also have ordering dependencies which can be controlled this way.

The below example uses WITH and ORDER BY to sort Item nodes by their price property, then the collect() in the subsequent RETURN clause builds an ordered list per order based on that sort.

WITH, ORDER BY, and collect()
MATCH (o:Order)-[:CONTAINS]->(i:Item)
WITH o, i
  ORDER BY i.price DESC
RETURN o.id AS order,
       collect(i.name || " ($" || toString(i.price) || ")") AS orderedListOfItems
Result
order orderedListOfItems

"ORD-002"

["Laptop ($1000)"]

"ORD-001"

["Phone ($500)", "Charger ($50)"]

"ORD-003"

["Phone ($500)", "Charger ($50)"]

"ORD-005"

["Phone ($500)", "Headphones ($250)", "Charger ($50)"]

"ORD-004"

["Keyboard ($200)"]

Rows: 5

Ordering aggregated or DISTINCT results

The variables available to ORDER BY depend on whether or not the preceding RETURN or WITH clause performs an aggregation to combine results or uses DISTINCT to remove duplicates.

  • If the RETURN or WITH is not aggregating values or using DISTINCT, then ORDER BY can reference any variables referenced in the preceding RETURN or WITH clause.

ORDER BY following a WITH clause excluding aggregation or DISTINCT
MATCH (o:Order)-[:CONTAINS]->(i:Item)
WITH o.id AS order,
     i.name AS item
  ORDER BY o.orderDate
RETURN order, item
  • If the RETURN or WITH performs an aggregation or uses DISTINCT only the projected variables from either operation are available to ORDER BY. This is because these operations alter the number of rows produced by the clause and any variables not explicitly projected are discarded.

ORDER BY following a WITH clause projecting an aggregated value
MATCH (o:Order)-[:CONTAINS]->(i:Item)
WITH collect(o.id) AS orders,
     i.name AS items
  ORDER BY o.orderDate
RETURN orders, items
Error message
In a WITH/RETURN with DISTINCT or an aggregation, it is not possible to access variables declared before the WITH/RETURN: o

ORDER BY and indexes

The performance of Cypher queries using ORDER BY on node properties can be influenced by the existence and use of an index for finding the nodes. If the index can provide the nodes in the order requested in the query, Cypher can avoid the use of an expensive Sort operation. Read more about this capability in Range index-backed ORDER BY.

Using ORDER BY as a standalone clause

ORDER BY can be used as a standalone clause, or in conjunction with SKIP/OFFSET or LIMIT.

Standalone use of ORDER BY
MATCH (i:Item)
ORDER BY i.price
RETURN collect(i.name || " ($" || toString(i.price) || ")") AS orderedPriceList
Result
orderedPriceList

["Charger ($50)", "Keyboard ($200)", "Headphones ($250)", "Phone ($500)", "Laptop ($1000)"]

Rows: 1

ORDER BY used in conjunction with SKIP and LIMIT
MATCH (i:Item)
ORDER BY i.price DESC
SKIP 1
LIMIT 1
RETURN i.name AS secondMostExpensiveItem,
       i.price AS price
Result
secondMostExpensiveItem price

"Phone"

500

Rows: 1