Storing XML in a DB2 for z/OS column is only half the job. You still need to ask questions inside the document: does this order contain a rush item? What is the customer name element? Can I turn every <line> into a relational row? SQL/XML gives you four workhorses: XMLEXISTS (filter rows), XMLQUERY (extract XML), XMLTABLE (shred to a table), and XMLCAST (convert XML to SQL types). Under the hood they all evaluate XPath or the larger XQuery language, including FLWOR loops.
Relational SQL sees an XML column as one value per row. It does not automatically know about nested elements. To walk inside the document you embed an XQuery expression as a character string constant in SQL/XML. Db2 evaluates that expression against XML you pass in, then hands the result back as either a yes/no (predicate), an XML sequence (XMLQUERY), or a table (XMLTABLE).
| Tool | Kind | Typical place | Returns |
|---|---|---|---|
| XMLEXISTS | Predicate | WHERE / JOIN ON / HAVING | True if XQuery sequence is non-empty |
| XMLQUERY | Scalar function | SELECT list, SET, VALUES | XML value (sequence of items) |
| XMLTABLE | Table function | FROM clause | Relational rows and columns |
| XMLCAST | Cast specification | Anywhere a typed SQL value is needed | SQL type or XML, depending on AS clause |
A practical pattern used in IBM samples: filter with XMLEXISTS, then project with XMLQUERY (or shred with XMLTABLE). That keeps the WHERE clause cheap and index-friendly while the SELECT list does the construction work.
123456789101112SELECT CID, XMLQUERY( 'declare default element namespace "http://posample.org"; $doc/customerinfo/name' PASSING INFO AS "doc" ) AS CUST_NAME FROM MYCUSTOMER AS C WHERE XMLEXISTS( 'declare default element namespace "http://posample.org"; $i/customerinfo/addr[city="Toronto"]' PASSING C.INFO AS "i" );
The declare default element namespace prolog is required when the stored document uses a default namespace. If you omit it, a path like /customerinfo looks for elements in no namespace and matches nothing—a classic “query returns empty but the XML is right there” bug.
An XML predicate tests XML the way LIKE tests strings. XMLEXISTS is the main one: it is true when the XQuery expression yields a non-empty sequence, false when the sequence is empty. It does not return the nodes; it only answers “did anything match?”
123456DELETE FROM MYCUSTOMER WHERE XMLEXISTS( 'declare default element namespace "http://posample.org"; /customerinfo/phone[@type="cell"]' PASSING INFO );
When the XQuery uses a context item (a path that starts at the document root without a variable), PASSING can supply that context XML directly. When you name a variable with AS "i", write $i/... in the expression. Names are case-sensitive in XQuery even though SQL identifiers might not be.
Pair XMLEXISTS with a relational predicate when you can: WHERE STATUS = 'OPEN' AND XMLEXISTS(...). Db2 can use a relational index on STATUS first, then apply XML logic to fewer documents.
XPath is the path language: steps separated by slashes, predicates in square brackets, @ for attributes. It is the subset of XQuery you should memorize first.
1234567891011121314-- Element children '/order/item' -- Predicate on a child '/order/item[qty > 10]' -- Attribute '/order/item[@sku="W42"]' -- Text value of an element '/customerinfo/name/text()' -- Abbreviated descendant '//phone'
XPath predicates in XMLEXISTS are the forms most likely to match an XML index defined with an XMLPATTERN. Keep paths as specific as the documents allow. Wild descendant searches and functions inside predicates often block index matching.
XQuery includes XPath and adds expressions that look more like a small programming language. In Db2 for z/OS, XQuery is not a standalone statement you type at SPUFI by itself. You place an XQuery expression inside XMLQUERY, XMLEXISTS, or XMLTABLE as a string constant. That constant must not be empty or all blanks, and it must not be an updating XQuery expression in XMLQUERY.
Besides path expressions, common XQuery expression kinds include:
IBM’s performance note is blunt: XQuery FLWOR is not indexable. Even if an XML index exists for /Order/OrderLine/ID, an XMLEXISTS written as a FLWOR over that path will not use the index the way a plain XPath predicate can. Preferred practice: XPath in XMLEXISTS (and in XMLTABLE row expressions when you want index use), then XQuery in XMLQUERY or in XMLTABLE column PATH expressions to reshape only the rows that already qualified.
FLWOR stands for for, let, where, order by, return. Think of it as XQuery’s SELECT: for/let are FROM, where is WHERE, order by is ORDER BY, return is the SELECT list.
| Clause | What it does |
|---|---|
| for | Iterate: bind a variable to each item in a sequence |
| let | Assign: bind a variable to an entire expression result (no extra loop) |
| where | Filter items after for/let bindings |
| order by | Sort the tuples before return |
| return | Build the result sequence (often new XML or selected nodes) |
123456789SELECT XMLQUERY( 'declare default element namespace "http://posample.org"; for $d in $doc/customerinfo where $d/addr/city = "Toronto" order by $d/name return $d/name' PASSING INFO AS "doc" ) FROM MYCUSTOMER;
for $d in ... creates one tuple per matching node. let $x := ... binds once per tuple without iterating. Use let for computed values you would otherwise repeat. where drops tuples. order by sorts remaining tuples. return builds the output sequence—often the original node, a constructed element, or a concatenation of several pieces.
Nested FLWOR expressions are legal. Keep them in XMLQUERY after you have already restricted the row set. Evaluating a heavy FLWOR against every XML column value in a large table is a classic elapsed-time surprise.
XMLQUERY returns the XML value of evaluating the XQuery constant with the PASSING arguments. Typical options you will see in the syntax:
PASSING arguments must not be ROWID, TIMESTAMP, binary string, REAL, DECFLOAT, FOR BIT DATA character, or a sequence expression. Character and graphic arguments must not be LOBs. If you need a CLOB document as input, store or cast it to XML first (XMLPARSE), then PASSING the XML value.
12345-- Named variable XMLQUERY('$d/order/comment' PASSING ORDER_DOC AS "d") -- Empty vs null when nothing matches XMLQUERY('$d/order/missing' PASSING ORDER_DOC AS "d" NULL ON EMPTY)
XMLQUERY is not indexable. Do not use it as your only filter in WHERE (and you generally cannot use it as a boolean predicate anyway—that is XMLEXISTS’s job). Select the XML fragment you need after the row has qualified.
XMLTABLE belongs in the FROM clause. It evaluates a row XQuery expression and, for each item in that sequence, builds one result row whose columns come from PATH expressions (or defaults). That is how you turn repeating XML into something you can JOIN, GROUP BY, and load into a report.
1234567891011121314SELECT O.ORDER_ID, X.SKU, X.QTY, X.PRICE FROM ORDER_XML O, XMLTABLE( XMLNAMESPACES(DEFAULT 'http://example.org/po'), '$d/order/item' PASSING O.ORDER_DOC AS "d" COLUMNS SKU VARCHAR(20) PATH '@sku', QTY INTEGER PATH 'qty', PRICE DECIMAL(9,2) PATH 'price' ) AS X;
Important pieces:
If the row expression is a simple XPath that matches an XML index pattern, XMLTABLE can participate in index usage. If you write a FLWOR as the row expression, you usually give that up. Column PATH expressions can be richer XQuery because they run only for rows already produced.
XMLTABLE is a join: if a document has zero matching items, that ORDER_XML row does not appear (like an inner join). If you need documents with no items, use a left outer join to the XMLTABLE reference.
XMLCAST(expression AS data-type) converts between XML and SQL types. Either the operand or the target must be XML. Casting XML to XML is a no-op. You cannot XMLCAST to a distinct type. NULL AS XML yields a null XML value.
123456SELECT ORDER_ID, XMLCAST( XMLQUERY('$d/order/total' PASSING ORDER_DOC AS "d") AS DECIMAL(15,2) ) AS ORDER_TOTAL FROM ORDER_XML;
Common targets from XML atomics include VARCHAR, CHAR, integers, DECIMAL, DATE, TIME, and TIMESTAMP. When the target is XML, SQL DATE becomes xs:date, TIME becomes xs:time, TIMESTAMP WITH TIME ZONE becomes xs:dateTime (trailing zeros in fractional seconds are not kept). Default encoding for character results is Unicode; you can specify CCSID on the target type when you need a different scheme.
If XMLQUERY returns an empty sequence, XMLCAST to a non-XML type typically yields null—handle that with COALESCE if the business wants zero. If it returns more than one item, the cast is not a simple atomic conversion; extract a single value in XQuery first (for example ($d/order/total)[1]).
A production query often layers all four tools:
COBOL and other host languages receive XMLQUERY results as XML (or serialized strings if you XMLSERIALIZE). XMLTABLE columns of DECIMAL and VARCHAR map like any other SELECT list item. Do not fetch a multi-megabyte document into a PIC X(80) just to read one element—extract in SQL.
An XML document is a tree house with labeled rooms. XPath is the hallway directions: “second floor, room named item, the one with sku W42.” XMLEXISTS asks “is anyone in that room?” and only keeps houses where the answer is yes. XMLQUERY brings back the furniture from that room still packed as XML. XMLTABLE unpacks every toy in the toy chest onto a spreadsheet, one row per toy. XMLCAST puts a price tag number into your calculator so it is a real number, not a scrap of XML paper. FLWOR is walking through every room with a checklist, maybe building a new smaller tree house from what you found—fun, but slower than just asking “is the rush flag on?” at the front door.
1. Where do you typically use XMLEXISTS?
2. What does XMLQUERY return?
3. Which form is usually indexable for XML indexes on z/OS?
4. What is XMLTABLE for?
5. Why use XMLCAST with XMLQUERY?