XPath performance: cost, indexes and //
Descendant search is the expensive part of XPath. Knowing which constructs force a full scan tells you where to spend your optimisation effort.
What actually costs time
| Construct | Work done | Cost on a large document |
|---|---|---|
/a/b/c | One child step per level | Low, proportional to depth |
//x | Visit every descendant of the context | High, proportional to document size |
//x[@id='1'] | Full scan, then filter | High — no early exit |
id('1') | Index lookup | Constant, when the parser built an index |
//x[1] | Scan, keep the first per parent | High, and often not what you meant |
count(//x) | Scan and count | High, use a streaming reader instead |
//a/following-sibling::a | Quadratic in sibling count | Very high, avoid |
# Measure rather than guess
time xmllint --xpath 'count(//item)' big.xml
# Narrow the context node so // has less to scan
time xmllint --xpath 'count(//feed/item)' big.xml
# A path that never leaves the top level is the cheapest form
time xmllint --xpath 'count(/*/*/item)' big.xml(//x)[1]scans everything then takes one node;//x[1]takes the first matchingxchild of every parent. Neither is a shortcut for the other.- Every additional
//in one expression multiplies the traversal. - Repeating the same expression instead of storing the result in a variable is the most common waste in XSLT.
Making a query fast
<!-- Keys give you a hash index you control -->
<xsl:key name="byId" match="item" use="@id"/>
<xsl:template match="/">
<!-- one key call per lookup, instead of scanning every item -->
<xsl:value-of select="key('byId', '42')/title"/>
</xsl:template>from lxml import etree
import time
doc = etree.parse("big.xml")
root = doc.getroot()
def timed(label, fn):
t = time.perf_counter()
n = len(fn())
print(label, n, "in", round((time.perf_counter() - t) * 1000), "ms")
# Full descendant scan on every call
timed("//item ", lambda: doc.xpath("//item"))
# Restrict the context node so the scan is smaller
timed("//b/item ", lambda: doc.xpath("//b/item"))
# Direct children of the root element
timed("/*/b/item", lambda: doc.xpath("/*/b/item"))💡
Optimise the query shape before the engine. A single
// removed from a hot expression usually beats any library-level tuning, and the change is measurable with a five-line timing harness like the one above.FAQ
Is id() really constant time?
When the parser recorded ID attributes, which requires a DTD or schema declaration. Without one, id() behaves like any other scan, so wrap your real lookup key in xsl:key or a dictionary instead.
Should I avoid // altogether?
No. Use it for one-off queries and during development. Give it a narrow context node and avoid it inside loops, which is where it becomes quadratic.
Related
XPath from the shell: xmllint and xmlstarlet The XPath data model: nodes, order and values
Last refreshed 2026-09-18.