diff --git a/modules/ROOT/images/with_clause.svg b/modules/ROOT/images/with_clause.svg new file mode 100644 index 000000000..f77938445 --- /dev/null +++ b/modules/ROOT/images/with_clause.svg @@ -0,0 +1 @@ +BOUGHTdate:DATESUPPLIESCustomerfirstName:STRINGlastName:STRINGemail:STRINGdiscount:FLOATProductname:STRINGprice:INTEGERSuppliername:STRINGemail:STRING \ No newline at end of file diff --git a/modules/ROOT/pages/clauses/with.adoc b/modules/ROOT/pages/clauses/with.adoc index c2939fa21..33cf54228 100644 --- a/modules/ROOT/pages/clauses/with.adoc +++ b/modules/ROOT/pages/clauses/with.adoc @@ -1,244 +1,550 @@ -:description: The `WITH` clause allows query parts to be chained together, piping the results from one to be used as starting points or criteria in the next. +:description: Information about Cypher's `WITH` clause, which allows query parts to be chained together, piping the results from one part to be used as the starting point of the next. +:table-caption!: -[[query-with]] = WITH -The `WITH` clause allows query parts to be chained together, piping the results from one to be used as starting points or criteria in the next. +The `WITH` clause serves multiple purposes in Cypher: -[NOTE] -==== -It is important to note that `WITH` affects variables in scope. -Any variables not included in the `WITH` clause are not carried over to the rest of the query. -The wildcard `*` can be used to include all variables that are currently in scope. -==== +* xref:clauses/with.adoc#create-new-variables[Create new variables] +* xref:clauses/with.adoc#variable-scope[Control variables in scope] +* xref:clauses/with.adoc#bind-values-to-variables[Bind the results of expressions to new variables] +* xref:clauses/with.adoc#aggregations[Perform aggregations] +* xref:clauses/with.adoc#remove-duplicate-values[Remove duplicate values] +* xref:clauses/with.adoc#ordering-and-pagination[Order and paginate results] +* xref:clauses/with.adoc#filter-results[Filter results] +* xref:clauses/with.adoc#combine-write-and-read-clauses[Combine write and read clauses] -Using `WITH`, you can manipulate the output before it is passed on to the following query parts. -Manipulations can be done to the shape and/or number of entries in the result set. +[[example-graph]] +== Example graph +A graph with the following schema is used for the examples below: -One common usage of `WITH` is to limit the number of entries passed on to other `MATCH` clauses. -By combining `ORDER BY` and `LIMIT`, it is possible to get the top X entries by some criteria and then bring in additional data from the graph. +image::with_clause.svg[width="600",role="middle"] -`WITH` can also be used to introduce new variables containing the results of expressions for use in the following query parts (see xref::clauses/with.adoc#with-introduce-variables[Introducing variables for expressions]). -For convenience, the wildcard `*` expands to all variables that are currently in scope and carries them over to the next query part (see xref::clauses/with.adoc#with-wildcard[Using the wildcard to carry over variables]). +To recreate the graph, run the following query against an empty Neo4j database. -Another use is to filter on aggregated values. -`WITH` is used to introduce aggregates which can then be used in predicates in `WHERE`. -These aggregate expressions create new bindings in the results. +[source, cypher, role=test-setup] +---- +CREATE (techCorp:Supplier {name: 'TechCorp', email: 'contact@techcorp.com'}), + (foodies:Supplier {name: 'Foodies Inc.', email: 'info@foodies.com'}), + + (laptop:Product {name: 'Laptop', price: 1000}), + (phone:Product {name: 'Phone', price: 500}), + (headphones:Product {name: 'Headphones', price: 250}), + (chocolate:Product {name: 'Chocolate', price: 5}), + (coffee:Product {name: 'Coffee', price: 10}), + + (amir:Customer {firstName: 'Amir', lastName: 'Rahman', email: 'amir.rahman@example.com', discount: 0.1}), + (keisha:Customer {firstName: 'Keisha', lastName: 'Nguyen', email: 'keisha.nguyen@example.com', discount: 0.2}), + (mateo:Customer {firstName: 'Mateo', lastName: 'Ortega', email: 'mateo.ortega@example.com', discount: 0.05}), + (hannah:Customer {firstName: 'Hannah', lastName: 'Connor', email: 'hannah.connor@example.com', discount: 0.15}), + (leila:Customer {firstName: 'Leila', lastName: 'Haddad', email: 'leila.haddad@example.com', discount: 0.1}), + (niko:Customer {firstName: 'Niko', lastName: 'Petrov', email: 'niko.petrov@example.com', discount: 0.25}), + (yusuf:Customer {firstName: 'Yusuf', lastName: 'Abdi', email: 'yusuf.abdi@example.com', discount: 0.1}), + + (amir)-[:BUYS {date: date('2024-10-09')}]->(laptop), + (amir)-[:BUYS {date: date('2025-01-10')}]->(chocolate), + (keisha)-[:BUYS {date: date('2023-07-09')}]->(headphones), + (mateo)-[:BUYS {date: date('2025-03-05')}]->(chocolate), + (mateo)-[:BUYS {date: date('2025-03-05')}]->(coffee), + (mateo)-[:BUYS {date: date('2024-04-11')}]->(laptop), + (hannah)-[:BUYS {date: date('2023-12-11')}]->(coffee), + (hannah)-[:BUYS {date: date('2024-06-02')}]->(headphones), + (leila)-[:BUYS {date: date('2023-05-17')}]->(laptop), + (niko)-[:BUYS {date: date('2025-02-27')}]->(phone), + (niko)-[:BUYS {date: date('2024-08-23')}]->(headphones), + (niko)-[:BUYS {date: date('2024-12-24')}]->(coffee), + (yusuf)-[:BUYS {date: date('2024-12-24')}]->(chocolate), + (yusuf)-[:BUYS {date: date('2025-01-02')}]->(laptop), + + (techCorp)-[:SUPPLIES]->(laptop), + (techCorp)-[:SUPPLIES]->(phone), + (techCorp)-[:SUPPLIES]->(headphones), + (foodies)-[:SUPPLIES]->(chocolate), + (foodies)-[:SUPPLIES]->(coffee) +---- -`WITH` is also used to separate reading from updating of the graph. -Every part of a query must be either read-only or write-only. -When going from a writing part to a reading part, the switch must be done with a `WITH` clause. +[[create-new-variables]] +== Create new variables -image:graph_with_clause.svg[] +`WITH` can be used in combination with the `AS` keyword to bind new variables which can then be passed to subsequent clauses. -//// -[source, cypher, role=test-setup] +.Create a new variable +[source, cypher] ---- -CREATE - (a {name: 'Anders'}), - (b {name: 'Bossman'}), - (c {name: 'Caesar'}), - (d {name: 'David'}), - (e {name: 'George'}), - (a)-[:KNOWS]->(b), - (a)-[:BLOCKS]->(c), - (d)-[:KNOWS]->(a), - (b)-[:KNOWS]->(e), - (c)-[:KNOWS]->(e), - (b)-[:BLOCKS]->(d) +WITH [1, 2, 3] AS list +RETURN list ---- -//// +.Result +[role="queryresult",options="header,footer",cols="1*(:Product {name: 'Chocolate'}) +WITH c AS customer +RETURN customer.firstName AS chocolateCustomer ---- -// end::clauses_with_variables[] +// end::clauses_with_new_variable[] -This query returns the name of persons connected to *'George'* whose name starts with a `C`, regardless of capitalization. .Result [role="queryresult",options="header,footer",cols="1*(otherPerson) -WITH *, type(r) AS connectionType -RETURN person.name, otherPerson.name, connectionType +MATCH (c:Customer)-[:BUYS]->(p:Product {name: 'Chocolate'}) +WITH c.name AS chocolateCustomers +RETURN chocolateCustomers, + p.price AS chocolatePrice ---- -// end::clauses_with_wildcard[] -This query returns the names of all related persons and the type of relationship between them. +.Error message +[source, error] +---- +Variable `p` not defined +---- + +.Retain all variables with `WITH *` +// tag::clauses_with_all_variables[] +[source, cypher] +---- +MATCH (supplier:Supplier)-[r]->(product:Product) +WITH * +RETURN supplier.name AS company, + type(r) AS relType, + product.name AS product +---- +// end::clauses_with_all_variables[] .Result [role="queryresult",options="header,footer",cols="3*() -WITH otherPerson, count(*) AS foaf -WHERE foaf > 1 -RETURN otherPerson.name +WITH 11 AS x +CALL (x) { + UNWIND [2, 3] AS y + WITH y + RETURN x*y AS a +} +RETURN x, a ---- +// end::clauses_with_subquery[] -The name of the person connected to *'David'* with the at least more than one outgoing relationship will be returned by the query. .Result -[role="queryresult",options="header,footer",cols="1* Import variables]. -[[with-sort-results-before-using-collect-on-them]] -== Sort results before using collect on them +[[bind-values-to-variables]] +== Bind values to variables -You can sort your results before passing them to collect, thus sorting the resulting list. +`WITH` can be used to assign the values of expressions to variables. +In the below query, the value of the `STRING` concatenation expression is bound to a new variable `customerFullName`, and the value from the expression `chocolate.price * (1 - customer.discount)` is bound to `chocolateNetPrice`, both of which are then available in the `RETURN` clause. -.Query -[source, cypher, indent=0] +.Bind values to variables +// tag::clauses_with_bind_values[] +[source, cypher] ---- -MATCH (n) -WITH n -ORDER BY n.name DESC -LIMIT 3 -RETURN collect(n.name) +MATCH (customer:Customer)-[:BUYS]->(chocolate:Product {name: 'Chocolate'}) +WITH customer.firstName || ' ' || customer.lastName AS customerFullName, + chocolate.price * (1 - customer.discount) AS chocolateNetPrice +RETURN customerFullName, + chocolateNetPrice ---- +// end::clauses_with_bind_values[] -A list of the names of people in reverse order, limited to 3, is returned in a list. .Result -[role="queryresult",options="header,footer",cols="1*= 500 AS isExpensive +WITH p, isExpensive, NOT isExpensive AS isAffordable +WITH p, isExpensive, isAffordable, + CASE + WHEN isExpensive THEN 'High-end' + ELSE 'Budget' + END AS discountCategory +RETURN p.name AS product, + p.price AS price, + isAffordable, + discountCategory +ORDER BY price +---- +// end::clauses_with_chain_expressions[] + +.Result +[role="queryresult",options="header,footer", cols="4*(p:Product) +WITH c.firstName AS customer, + sum(p.price) AS totalSpent, + collect(p.name) AS productsBought +RETURN customer, + totalSpent, + productsBought +ORDER BY totalSpent DESC ---- +// end::clauses_with_aggregations[] -Starting at *'Anders'*, find all matching nodes, order by name descending and get the top result, then find all the nodes connected to that top result, and return their names. .Result -[role="queryresult",options="header,footer",cols="1* 2 -RETURN x +MATCH (c:Customer) +WITH DISTINCT c.discount AS discountRates +RETURN discountRates +ORDER BY discountRates ---- +// end::clauses_with_remove_duplicates[] -The limit is first applied, reducing the rows to the first 5 items in the list. The filter is then applied, reducing the final result as seen below: +.Result +[role="queryresult",options="header,footer", cols="1*(p:Product) +WITH c, + sum(p.price) AS totalSpent + ORDER BY totalSpent DESC +RETURN c.firstName AS customer, totalSpent +---- .Result -[role="queryresult",options="header,footer",cols="1*(p:Product) +WITH c, + sum(p.price) AS totalSpent + ORDER BY totalSpent DESC + LIMIT 3 +SET c.topSpender = true +RETURN c.firstName AS customer, + totalSpent, + c.topSpender AS topSpender +---- +// end::clauses_with_ordering_pagination[] + + +[role="queryresult",options="header,footer", cols="3*(p:Product) +WITH c, + sum(p.price) AS totalSpent + ORDER BY totalSpent DESC + SKIP 3 +SET c.topSpender = false +RETURN c.firstName AS customer, + totalSpent, + c.topSpender AS topSpender +---- + +[role="queryresult",options="header,footer", cols="3*(p:Product) +WITH p + ORDER BY p.price DESC + LIMIT 1 +MATCH (p)<-[:BUYS]-(c:Customer) +RETURN p.name AS product, + p.price AS price, + collect(c.firstName) AS customers +---- + + +[role="queryresult",options="header,footer", cols="3* 2 -WITH x -LIMIT 5 + WHERE x > 2 RETURN x ---- -This time the filter is applied first, reducing the rows to consist of the list `[3, 4, 5, 6]`. -Then the limit is applied. -As the limit is larger than the total number of remaining rows, all rows are returned. - -.Result -[role="queryresult",options="header,footer",cols="1*(p:Product)<-[:BUYS]-(c:Customer) +WITH s, + sum(p.price) AS totalSales, + count(DISTINCT c) AS uniqueCustomers + WHERE totalSales > 1000 +RETURN s.name AS supplier, + totalSales, + uniqueCustomers +---- +// end::clauses_with_filtering[] + +[role="queryresult",options="header,footer", cols="3*(coffee), + (yusuf)-[:BUYS {date: date('2025-04-15')}]->(headphones) +WITH yusuf +MATCH (yusuf)-[r:BUYS]->(p:Product) +RETURN collect(p.name) AS yusufPurchases, + r.date AS date +ORDER BY date DESC +---- +// end::clauses_with_combine_write_read[] + +[role="queryresult",options="header,footer", cols="2*