diff --git a/modules/ROOT/images/graph_order_by_clause.svg b/modules/ROOT/images/graph_order_by_clause.svg index a42046fcc..a571834d7 100644 --- a/modules/ROOT/images/graph_order_by_clause.svg +++ b/modules/ROOT/images/graph_order_by_clause.svg @@ -1 +1 @@ -KNOWSKNOWSPersonname:'Charlotte'age:32length:185Personname:'Bernard'age:36Personname:'Andy'age:34length:170 \ No newline at end of file +CONTAINSOrderid:STRINGordeDate:DATEtotal:INTEGERstatus:STRINGItemname:STRINGprice:INTEGER \ No newline at end of file diff --git a/modules/ROOT/pages/clauses/order-by.adoc b/modules/ROOT/pages/clauses/order-by.adoc index 913580b34..06287833d 100644 --- a/modules/ROOT/pages/clauses/order-by.adoc +++ b/modules/ROOT/pages/clauses/order-by.adoc @@ -1,119 +1,154 @@ -:description: `ORDER BY` is a sub-clause following `RETURN` or `WITH`, and it specifies that the output should be sorted and how. - -[[query-order]] = ORDER BY +:description: Information about Cypher's `ORDER BY` subclause. +:table-caption!: -`ORDER BY` specifies how the output of a clause should be sorted. -It be used as a sub-clause following `RETURN` or `WITH`. +`ORDER BY` is a subclause that determines how the results of a xref:clauses/return.adoc[`RETURN`] or xref:clauses/with.adoc[`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` relies on comparisons to sort the output, see xref:values-and-types/ordering-equality-comparison.adoc[Ordering and comparison of values]. -You can sort on many different values, e.g. node/relationship properties, the node/relationship ids, or on most expressions. +`ORDER BY` defaults to sorting results in an ascending order, though it can be modified to sort results in a xref:clauses/order-by.adoc#ascending-descending-order[descending order]. -[NOTE] -==== +`ORDER BY` relies on comparisons to sort the output (see xref:values-and-types/ordering-equality-comparison.adoc[] for more details). +You can sort on different values, such as node or relationship properties, IDs, or the result of expressions. + +[IMPORTANT] Unless `ORDER BY` is used, Neo4j does not guarantee the row order of a query result. -==== + [[example-graph]] == Example graph -The following graph is used for the examples below: +A graph with the following schema is used for the examples below: -image::graph_order_by_clause.svg[width="600", role="middle"] +image::graph_order_by_clause.svg[width="400", role="middle"] To recreate it, run the following query against an empty Neo4j database: [source, cypher, role=test-setup] ---- -CREATE - (andy: Person {name: 'Andy', age: 34, length: 170}), - (bernard: Person {name: 'Bernard', age: 36}), - (charlotte: Person {name: 'Charlotte', age: 32, length: 185}), - (andy)-[:KNOWS]->(bernard), - (bernard)-[:KNOWS]->(charlotte) +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) ---- -[[order-nodes-by-property]] -== Order nodes by property -`ORDER BY` is used to sort the output. +[[basic-examples]] +== Basic examples -.Query +.Order by property values +===== + +`ORDER BY` can be used to sort the result by property values. + +.Order by node property // tag::clauses_order_by[] [source, cypher] ---- -MATCH (n) -RETURN n.name, n.age -ORDER BY n.name +MATCH (o:Order) +RETURN o.id AS order, + o.total AS total + ORDER BY total ---- // end::clauses_order_by[] -The nodes are returned, sorted by their name. +The nodes are returned, sorted by the value of the `total` properties in an ascending order. .Result [role="queryresult",options="header,footer",cols="2*(:Item) } AS itemCount + ORDER BY itemCount +---- .Result -[role="queryresult",options="header,footer",cols="3* -| "Andy" | 34 | 170 -| "Charlotte" | 32 | 185 -3+d|Rows: 3 +| order | itemCount + +| "ORD-002" | 1 +| "ORD-004" | 1 +| "ORD-001" | 2 +| "ORD-003" | 2 +| "ORD-005" | 3 + +2+d|Rows: 5 |=== +===== -[[order-nodes-in-descending-order]] -== Order nodes in descending order +[[order-by-values-not-in-result]] +== Order by values not in the result -By adding `DESC[ENDING]` after the variable to sort on, the sort will be done in reverse order. +`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. -.Query +.Order by values not in the returned results [source, cypher] ---- -MATCH (n) -RETURN n.name, n.age -ORDER BY n.name DESC +MATCH (o:Order) +RETURN o.id AS order + ORDER BY COUNT { (o)-[:CONTAINS]->(:Item) } ---- -The example returns the nodes, sorted by their name in reverse order. +.Result +[role="queryresult",options="header,footer",cols="1*(i:Item) +RETURN o.id AS order, + o.total, + collect(i.name) AS items +---- .Result [role="queryresult",options="header,footer",cols="3* | "Bernard" | 36 -3+d|Rows: 3 +| order | total | items + +| "ORD-005" | 800 | ["Phone", "Headphones", "Charger"] + +3+d|Rows: 1 +|=== + +[[null]] +== Null values + +When sorting, `null` values appear last in ascending order and first in descending order. + +.Sort on null property +[source, cypher] +---- +MATCH (o:Order) +RETURN o.id AS order, + o.status AS status + ORDER BY status DESC +---- + +.Result +[role="queryresult",options="header,footer",cols="2*(i:Item) +WITH o, i + ORDER BY i.price DESC +RETURN o.id AS order, + collect(i.name || " ($" || toString(i.price) || ")") AS orderedListOfItems ---- -The list of names built from the `collect` aggregating function contains the names in order of the `age` property. - .Result -[role="queryresult",options="header,footer",cols="1*(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. -It is also not allowed to use aggregating expressions in the `ORDER BY` sub-clause if they are not also listed in the projecting clause. -This rule is to make sure that `ORDER BY` does not change the results, only the order of them. +.`ORDER BY` following a `WITH` clause projecting an aggregated value +[source, cypher, role=test-fail] +---- +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 +[source, error] +---- +In a WITH/RETURN with DISTINCT or an aggregation, it is not possible to access variables declared before the WITH/RETURN: o +---- + +[[indexes]] == 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. @@ -248,43 +467,45 @@ Read more about this capability in xref::indexes/search-performance-indexes/usin `ORDER BY` can be used as a standalone clause, or in conjunction with `SKIP`/`OFFSET` or `LIMIT`. - .Standalone use of `ORDER BY` // tag::clauses_order_by_standalone[] [source, cypher] ---- -MATCH (n) -ORDER BY n.name -RETURN collect(n.name) AS names +MATCH (i:Item) +ORDER BY i.price +RETURN collect(i.name || " ($" || toString(i.price) || ")") AS orderedPriceList ---- // end::clauses_order_by_standalone[] .Result [role="queryresult",options="header,footer",cols="1*