for $x in expr binds $x to each item in turn, multiplying the tuple stream; at $i adds the position, and two for clauses form a cross product that a where turns into a join. let $y := expr binds a whole value once per tuple without multiplying anything. where keeps the tuples whose condition is true. A sample order file (generated sample data) joins against the catalog:
<!-- BookNest sample orders (generated sample data, not real customers) -->
<orders>
<order id="o1001" date="2026-09-28" sku="BN-0001" qty="2"/>
<order id="o1002" date="2026-09-28" sku="BN-0002" qty="1"/>
<order id="o1003" date="2026-09-29" sku="BN-0005" qty="3"/>
<order id="o1004" date="2026-09-29" sku="BN-0001" qty="1"/>
<order id="o1005" date="2026-09-29" sku="BN-0006" qty="2"/>
<order id="o1006" date="2026-09-30" sku="BN-0002" qty="2"/>
<order id="o1007" date="2026-09-30" sku="BN-0004" qty="1"/>
<order id="o1008" date="2026-10-01" sku="BN-0005" qty="1"/>
</orders>(: Inner join of sample orders to the catalog, then a left outer join for unsold books :)
let $orders := doc("orders.xml")//order
return (
for $o in $orders
let $b := //book[identifier = $o/@sku]
where $b//availability = "in-stock" and $o/@date ge "2026-09-30"
return string-join(($o/@id, $o/@date, $b/title, "x" || $o/@qty), " | "),
for $b in //book
for $o allowing empty in $orders[@sku = $b/identifier]
where empty($o)
return "unsold: " || $b/title
)xquery -s:booknest-catalog.xml join.xqOutput
"o1006 | 2026-09-30 | Patterns of the Deep Web | x2" "o1007 | 2026-09-30 | Small Steps to Big Summits | x1" "o1008 | 2026-10-01 | The Clockmaker's Paradox | x1" "unsold: Salt and Saffron"
doc("orders.xml") resolves against the query file's location, not the -s: document, so keep the two side by side or pass absolute URIs. allowing empty (XQuery 3.0) keeps a tuple when the inner sequence is empty, with $o bound to (), which is how a left outer join finds the book nobody ordered. ISO dates compare correctly as strings; cast them with xs:date() for arithmetic.
FLWOR
At the heart of XQuery are FLWOR expressions: for-let-where-order by-return. A for clause iterates over a sequence, a let clause binds a variable to a value, a where clause filters, an order by clause sorts, and a return clause builds the result. XQuery 3.0 added an optional group by clause and an optional count clause.The examples below query the following document:
merged-01-01-ch05-flwor-data.xml
<?xml version="1.0" encoding="UTF-8"?>
<richestMen>
<billionaire id="1">
<name>Carlos Slim Helu</name>
<age>74</age>
<country>Mexico</country>
<net_worth>86.1</net_worth>
</billionaire>
<billionaire id="2">
<name>Bill Gates</name>
<age>58</age>
<country>USA</country>
<net_worth>81.2</net_worth>
</billionaire>
<billionaire id="3">
<name>Warren Buffett</name>
<age>84</age>
<country>USA</country>
<net_worth>67.6</net_worth>
</billionaire>
<billionaire id="4">
<name>Amancio Ortega</name>
<age>78</age>
<country>Spain</country>
<net_worth>64.2</net_worth>
</billionaire>
<billionaire id="5">
<name>Larry Ellison</name>
<age>70</age>
<country>USA</country>
<net_worth>51.1</net_worth>
</billionaire>
</richestMen>
Basic FLWOR: where, let, order by
Filters billionaires with a net worth above 70 (in billion USD), binds each one's id attribute to $id, and orders the result by name.for $man in richestMen/billionaire
where $man/net_worth > 70
let $id := $man/@id
order by $man/name/text()
return ' Rank ' || $id || ') ' || ($man/name)
Output
Rank 2) Bill Gates Rank 1) Carlos Slim Helu
group by
Groups billionaires by country and, for each group, builds a document element counting its members.for $man in doc('richestMen.xml')/richestMen/billionaire
let $c := $man/country/text()
group by $c
return document {element {$c} {count($man)}}
Output
<Mexico>1</Mexico><Spain>1</Spain><USA>3</USA>
count clause with stable order by
The count clause binds the current position (before sorting) to $num. stable order by guarantees that ties keep their relative input order; empty least places empty sequences last.for $age in richestMen/billionaire/age
count $num
stable order by $age descending, $num ascending empty least
return <man num="{$num}">{$age}</man>
Output
<man num="3"><age>84</age></man> <man num="4"><age>78</age></man> <man num="1"><age>74</age></man> <man num="5"><age>70</age></man> <man num="2"><age>58</age></man>
Constructing an HTML table
The at clause of a for binds the current iteration position ($i) alongside the item ($man), useful for numbering rows while building markup with node constructors.<table>
{for $man at $i in richestMen/billionaire
return (<tr><td>{$i}.{$man/name/text()}</td>
<td>{$man/net_worth/text()}</td></tr>,
' ') }
</table>
Output
<table> <tr><td>1.Carlos Slim Helu</td><td>86.1</td></tr> <tr><td>2.Bill Gates</td><td>81.2</td></tr> <tr><td>3.Warren Buffett</td><td>67.6</td></tr> <tr><td>4.Amancio Ortega</td><td>64.2</td></tr> <tr><td>5.Larry Ellison</td><td>51.1</td></tr> </table>