Element Range Queries — Scalar Type Implications
Why Data Types Matter More Than You Think
Range indexes look simple in the Admin UI: pick an element, pick a type, save the configuration, and move on. In practice, that scalar type is one of the most consequential choices you make in a MarkLogic database. It determines what gets indexed, how values are compared, which queries match, which values silently disappear from the index, and how much memory and disk the index consumes.
In other words, it is a tiny dropdown with surprisingly expensive consequences.
That is why element range query bugs often feel mysterious. The query syntax is usually fine. The hidden issue is typically that the index type does not reflect the shape of the data. If you treat numbers as strings, comparisons become lexicographic. If you treat messy user input as integers, invalid values may be skipped or may cause the entire ingest to fail depending on database configuration — and decimals are truncated. If you treat local times as dateTime values without a timezone strategy, UTC normalisation can surprise you later.
The central lesson is simple: scalar types determine comparison semantics. The database is not just storing a value, it is storing a typed interpretation of that value. Once you understand that, range query behaviour stops feeling arbitrary and starts feeling predictable.
Working Assumptions for the Examples
The examples below use the llamaverse sample data wherever possible. The examples assume the llamaverse (v2.1+) is deployed. The llamaverse sample data is freely available from github.com/cleverllamas/llamaverse — see the llamaverse article for full setup instructions.
Raw llama documents live under /cleverllamas/llamaverse/raw/wild-llamas/llamas/{uuid}.json and are in the wild-llamas collection. The examples assume queries are executed as the cleverllamas-llama user, which keeps them aligned with the rest of the site.
Some examples also show XML fragments because element range indexes are an XML-native concept and the contrast with JSON path range indexes is instructive. The underlying ideas are the same in both models — only the index definition and constructor name differ.
The llamaverse path range indexes used in this article are:
| Path | Scalar type | Collation |
|---|---|---|
/heightCm | int | — |
/weightKg | int | — |
/name | string | http://marklogic.com/collation/ |
Supported Scalar Types at a Glance
MarkLogic supports a broad set of scalar types for range indexes. Choosing between them is not just a storage question — it is also a question of semantics, query expressiveness, and operational safety.
| Scalar type | What it represents | Typical use | When to avoid it |
|---|---|---|---|
int | 32-bit signed integer | Counts, measurements, small identifiers | When fractions or very large numbers are possible |
unsignedInt | 32-bit unsigned integer | Non-negative counters | When negative values are meaningful |
long | 64-bit signed integer | Large identifiers, event counters | When you need fractions |
unsignedLong | 64-bit unsigned integer | Very large non-negative IDs | When negatives or fractions appear |
float | Single-precision floating point | Approximate scientific values | When exact decimal precision matters |
double | Double-precision floating point | Measurements, scoring, telemetry | When financial-style precision matters |
decimal | Exact decimal number | Prices, weights, business values | When approximate binary floating point is acceptable |
dateTime | Date and time | Timestamps, event times, audits | When you only need date granularity |
time | Time of day | Daily schedules, opening hours | When the date matters |
date | Calendar date | Birth dates, expiry dates, partitions | When time-of-day matters |
gYearMonth | Year and month | Monthly periods, billing cycles | When day precision is required |
gYear | Year only | Tax year, cohort year | When month or day precision is needed |
duration | General duration | Mixed month/day duration comparisons | When you specifically need dayTime or yearMonth duration |
dayTimeDuration | Day/time duration | SLAs, elapsed runtime, TTL windows | When business periods are month-based |
yearMonthDuration | Year/month duration | Subscriptions, contract terms | When you need day-level precision |
string | Text compared by collation | Codes, names, lexical ordering | When numeric or temporal semantics are required |
anyURI | URI value | Canonical identifiers and links | When plain string matching is enough |
point | Geospatial point scalar | Stored points used by point-aware queries | When you need richer geospatial region indexes |
A practical rule: choose the narrowest type that matches the real domain of the value, not the prettiest type in your sample data. If the value is conceptually numeric, use a numeric index. If it is conceptually a date, use a date-aware index. If it is conceptually a code, title, or label, use a string index and pay attention to collation.
Scalar Type Determines Comparison Semantics
The scalar type controls how operators such as =, <, >, <=, and >= are interpreted. The exact same stored text can produce very different results depending on whether the index sees it as a number or a string.
The Classic String Gotcha
Suppose an element or JSON property contains values that look numeric but are indexed as strings. The comparison is then lexicographic, not numeric. That means "9" is greater than "10" because the comparison starts with the first character.
xquery version "1.0-ml";
(: Illustrates lexicographic vs numeric ordering. :)
(: With a string range index, comparisons are lexicographic, not numeric. :)
(: "9" is greater than "10" because "9" sorts after "1" as text. :)
for $value in ("2", "9", "10", "42")
where $value gt "10"
return $value
"2"
"9"
"42"
That behaviour is not a MarkLogic quirk. It is the expected consequence of asking a string index to do string work. The bug is modelling numeric data as text and then expecting numeric ordering from it. No database can guess your intent here.
Numeric Indexes Behave the Way Humans Expect
xquery version "1.0-ml";
(: With a numeric range index, comparisons are numeric. :)
for $value in (2, 9, 10, 42)
where $value gt 10
return $value
42
Exactly the same principle applies to temporal values. A dateTime index compares timestamps as timestamps. A string index compares their textual representation. ISO 8601 strings often sort in a useful way, but only if the formatting is consistent and you never need true temporal normalisation.
Index Typing Is Not Schema Typing
There is one boundary that is worth making explicit, especially for XML teams that use schema-aware content. The scalar type on a range index is not the same thing as the runtime type of a value in an XQuery expression.
If an XML document has been schema-validated, element and attribute values can carry typed values rather than behaving like untyped text. That can change the behaviour of FLWOR clauses, where conditions, order by logic, and ordinary comparison expressions. In other words, the same lexical content can behave differently in query code depending on whether it is untyped, schema-typed, or explicitly cast.
That matters because developers often see one comparison behave correctly in a FLWOR expression and then assume a range query will behave the same way automatically. It may not. A schema can influence how XQuery evaluates values at runtime, but a range query still depends on the configured scalar type of the index that backs it.
For example, schema-typed XML may cause <age>10</age> to behave as an integer in a comparison, while a string range index over the same field still compares lexically. That is not a contradiction. It is two different typing systems operating at two different layers.
The practical rule is simple: if you care about predictable range-query behaviour, align the index scalar type with the real domain of the value even when schema-aware XML already gives you helpful runtime typing in FLWOR expressions.
Scope Note
This distinction is mainly an XML and schema-awareness issue. JSON content does not gain the same kind of schema-derived XDM typing at query time, so the confusion shows up most often in XML-heavy codebases.
What Gets Indexed, Dropped, or Truncated
Range indexes are not magic mirrors of your source documents. At index time, MarkLogic coerces values to the configured scalar type. That has three common outcomes: the value is indexed successfully; the value is dropped because it cannot be coerced; or the value is coerced with loss of precision, such as truncating a decimal into an integer.
| Source value | Index type | Indexed outcome | Consequence |
|---|---|---|---|
25 | int | 25 | Matches integer range queries normally |
"30" | int | 30 | String content that parses cleanly still participates |
"unknown" | int | Not indexed | Document disappears from integer range results |
35.7 | int | 35 | Fraction is truncated, not rounded |
35.7 | decimal | 35.7 | Exact decimal semantics preserved |
2024-01-15 | date | 2024-01-15 | Date-only comparison preserved |
2024-01-15T10:00:00-05:00 | dateTime | Indexed as UTC-equivalent instant | Timezone offsets normalise during comparison |
The silent-drop behaviour is the one that catches teams most often. If a document contains the element, but the value does not coerce to the configured type, the document effectively has no index entry for that value. A later range query cannot match what was never indexed.
The core llamaverse llama data is intentionally clean, so the first step is to verify that profile directly on the existing wild-llamas collection. If you want to reproduce coercion edge cases locally, modify one llama's heightCm value in your local dataset copy and rerun the check.
const wild = cts.collectionQuery('wild-llamas');
const heightRef = cts.pathReference('/heightCm');
let ints = 0;
let floats = 0;
let strings = 0;
let missing = 0;
const uris = cts.uris('', null, wild).toArray();
for (const uri of uris) {
const doc = fn.doc(uri).toObject();
const height = doc.heightCm;
if (height === null || height === undefined) {
missing += 1;
} else if (typeof height === 'number') {
if (Number.isInteger(height)) {
ints += 1;
} else {
floats += 1;
}
} else if (typeof height === 'string') {
strings += 1;
}
}
const sampleUri = '/cleverllamas/llamaverse/raw/wild-llamas/llamas/00048384-cb13-4557-805e-a6b4e57f7eab.json';
const sampleDoc = fn.doc(sampleUri).toObject();
const sampleIndex = cts.values(heightRef, null, cts.documentQuery(sampleUri)).toArray();
xdmp.log('wild-llamas docs=' + uris.length);
xdmp.log('heightCm type profile: int=' + ints + ', float=' + floats + ', string=' + strings + ', missing=' + missing);
xdmp.log(sampleUri + ' => sourceValue=' + sampleDoc.heightCm + ' => index=' + (sampleIndex.length ? sampleIndex[0] : 'no heightCm index entry'));
// Optional local experiment for coercion edge cases:
// Update one wild-llama document's heightCm in a local dev copy, reindex, and rerun this script.
// For example set heightCm to 'unknown' and compare sourceValue vs index entry.
wild-llamas docs=3000
heightCm type profile: int=3000, float=0, string=0, missing=0
/cleverllamas/llamaverse/raw/wild-llamas/llamas/00048384-cb13-4557-805e-a6b4e57f7eab.json => sourceValue=145 => index=145
If preserving original numeric fidelity matters, do not use int just because the current data sample looks mostly whole-numbered. Use decimal or double instead.
Practical Guidance on Invalid Values
MarkLogic gives you explicit control over what happens when a value cannot be coerced to the configured scalar type. Each range index definition includes an invalid-values property with two possible settings:
| Setting | Behaviour |
|---|---|
ignore | The value is silently skipped. The document is still saved but contributes no index entry for that field. |
reject | The ingest transaction is aborted with an error. The document is not saved. |
Neither setting is universally correct. The right choice depends on your data quality guarantees and how much you can tolerate silent index gaps versus hard failures at write time.
ignore (the default): The database accepts any document regardless of whether it can index a value. This is forgiving but dangerous: messy data lands silently, the document exists in the database, and the only symptom is that it never appears in range query results. In a high-volume pipeline this can mean thousands of documents are effectively invisible to range-based queries before anyone notices. Use ignore only when you have a separate data quality gate and are prepared to tolerate and monitor gaps.
reject: The write transaction fails if any indexed field contains a value that cannot be coerced. This surfaces data quality problems immediately at the point of ingest, before bad data enters your database. The trade-off is that a single malformed field in an otherwise valid document blocks the entire save. Use reject when your schema is stable, your source data is under your control, or when invisible index gaps are more dangerous than failed writes.
Choosing a Strategy
For integration data from external systems: start with reject during onboarding so problems are visible early. It is much better to have an awkward conversation in test than a mysterious report in production. Once the feed is stable and clean, consider whether ignore is acceptable or whether the hard-fail guarantee is worth keeping.
For user-generated content: ignore is usually more appropriate — validate and normalise in your application layer rather than letting index configuration act as a schema enforcer.
The invalid-values setting lives on each individual index definition, so you can use reject on critical fields and ignore on optional ones within the same database.
When a field is user-entered or integration-sourced, assume messy data will arrive eventually. If you choose ignore, validate before insert or normalise into a clean indexed field — do not rely on the index silently absorbing bad data without visibility into what was dropped.
String Indexes and Collation
A string range index is not just a string range index — it is a string range index under a specific collation. That collation determines ordering, equality behaviour, case handling, and language-specific comparison rules.
| Collation style | Typical URI | Effect | Best use |
|---|---|---|---|
| Codepoint | http://marklogic.com/collation/ | Binary-ish lexical order, case-sensitive | Stable technical ordering and exact code ordering |
| Language-specific | http://marklogic.com/collation/en | Language-aware sorting rules | User-facing names and titles |
| Case-insensitive | http://marklogic.com/collation/en?strength=secondary | Upper/lower case normalise for comparison | Search UIs where Bradley and bradley should group together |
| Primary-strength | .../en?strength=primary | Often ignores case and diacritics | Loose matching or broad lexical grouping |
If you configure a string range index with one collation and query it with another, MarkLogic will throw an error because the requested index does not exist for that collation. This is why string range query examples often include the collation option explicitly.
dateTime Indexes Normalise to UTC
dateTime range indexes deserve special attention because the index stores instants, not presentation formats. When values include timezone offsets, MarkLogic normalises them to their UTC-equivalent instant during comparison. That is usually correct and desirable — it only feels surprising when developers compare displayed local times rather than the underlying instant.
xquery version "1.0-ml";
(: dateTime range indexes normalise to UTC. These two values represent the :)
(: same instant: 10am Eastern Standard Time = 3pm UTC. :)
let $stored := xs:dateTime("2024-01-15T10:00:00-05:00")
let $query := xs:dateTime("2024-01-15T15:00:00Z")
return $stored eq $query
true
The operational implication is straightforward: store explicit timezones whenever possible. If one system sends 2024-01-15T10:00:00-05:00 and another sends 2024-01-15T15:00:00Z, a dateTime index treats those as equivalent. If a producer sends a time without timezone information, you now have an ambiguity that no index can solve later.
Query Implications
If you are doing "all events since 9am Dublin time" style logic, convert the boundary once into a real xs:dateTime with timezone before building the range query. Do not compare textual local-time fragments and hope the sort order remains meaningful across offsets.
Multiple Indexes on the Same Field
Sometimes a single field needs more than one semantic interpretation. A common example is a value that is mostly numeric but also needs exact string-style matching, prefix grouping, or human-readable sorting. MarkLogic lets you define multiple range indexes on the same element or property with different scalar types where that makes sense operationally.
That does not mean you should index everything twice by default. It means you should model the access patterns honestly. In many systems, the cleaner design is two fields: index one as an integer for computation and one as a string for presentation. That keeps each index honest and avoids subtle coercion surprises.
Performance Characteristics by Scalar Type
Performance is never just about raw speed — it is about storage density, cache behaviour, comparison cost, and index selectivity. Some broad patterns hold consistently enough to use as design guidance.
| Scalar family | Typical semantics | Relative storage cost | General profile |
|---|---|---|---|
Integers (int, long, unsigned variants) | Numeric exact/range | Lowest | Most compact and fastest for pure numeric thresholds |
Floating point (float, double) | Approximate numeric | Medium | Good for measurements, less exact than integers |
decimal | Exact numeric | Medium to higher | Excellent for precision, slightly heavier than integers |
Temporal (date, dateTime, etc.) | Temporal ordering | Medium | Very efficient when the chosen type matches the use case |
string / anyURI | Collation-based lexical | Higher | Flexible but larger and less compact than numeric indexes |
point | Point-aware scalar | Specialised | Driven by geospatial access patterns rather than generic sorting |
If all you need is a count or threshold, an integer index is hard to beat. If you need exact decimal comparisons, accept the slightly higher cost and use decimal. If you need text ordering, pay the string cost but configure collation intentionally rather than accidentally.
Element Range Queries vs JSON Path Range Queries
The concepts are the same in XML and JSON. The constructor names differ because the indexed nodes differ. For XML elements, use cts:element-range-query(). For JSON path expressions, use cts:path-range-query(). Both require the corresponding range index to exist.
| Data model | Index type | Query constructor | Example |
|---|---|---|---|
| XML | Element range index | cts:element-range-query() | cts:element-range-query(xs:QName("age"), ">=", 5) |
| JSON | Path range index | cts:path-range-query() | cts:path-range-query("/heightCm", ">=", 150) |
What Happens Without the Index
This is one of the most important corrections to common MarkLogic folklore. Range queries do not gracefully degrade into a slow filter-only fallback when the required range index is missing. For cts:element-range-query() and cts:path-range-query(), MarkLogic throws an exception because the range index is mandatory.
xquery version "1.0-ml";
(: Attempting a range query on an element with no range index configured. :)
cts:search(
fn:collection(),
cts:element-range-query(xs:QName("age"), ">=", 5)
)
XDMP-ELEMRIDXNOTFOUND: cts:element-range-query(xs:QName("age"), ">=", 5) --
Element range index for age not found
That distinction matters operationally. If a deployment pipeline forgets to promote an index, the application does not merely become slower — it fails. That is good in one sense because silent slowness is hard to diagnose, but it also means index management is part of application correctness.
If You Truly Need a Fallback
Use explicit application logic. Catch the missing-index exception and run a slower manual pass, or provide a feature flag that disables range-query features until the index is present. Do not assume MarkLogic will improvise on your behalf for range semantics.
Nulls, Missing Values, and Empty Structures
An element or property that is absent simply does not contribute an index entry. That sounds obvious, but it has practical implications for analytics and faceting logic. If half of your documents omit a field, a range query only sees the half that actually have an indexable value.
This is also why "find documents where a field is missing" is not a range-query problem. It is a presence-query problem, often expressed as a negated element/property existence or a structured query that combines the business scope with cts:not-query().
xquery version "1.0-ml";
(: Pattern for finding documents without a particular property. :)
(: The missing value is not in the range index, so query for absence :)
(: explicitly using cts:not-query and a property scope query. :)
cts:search(
fn:collection("wild-llamas"),
cts:not-query(cts:json-property-scope-query("heightCm", cts:true-query()))
)
Index Configuration via the Management API
Serious teams keep index definitions in source control rather than only in click-path memory. MarkLogic supports this through the Management REST API. The payload structure for path range indexes is:
{
"range-path-index": [
{
"scalar-type": "int",
"path-expression": "/heightCm",
"collation": "",
"range-value-positions": false,
"invalid-values": "reject"
}
]
}
Send this as a PUT to /manage/v2/databases/{database-name}/properties. Note that a PUT replaces the entire array, so always include all existing indexes in the payload when adding a new one. Adding indexes to a populated database triggers reindexing — plan this the same way you plan schema migrations.
The equivalent for XML element range indexes uses the range-element-index key with namespace-uri and localname fields in place of path-expression.
Llamaverse Examples
The llamaverse llama documents have heightCm (int) and name (string) path range indexes configured. These examples run against the wild-llamas collection.
Querying by height
xquery version "1.0-ml";
(: Query llamas taller than 180cm using a path range index on /heightCm. :)
(: The index must exist before this query will run. :)
for $doc in cts:search(
fn:collection("wild-llamas"),
cts:path-range-query("/heightCm", ">", xs:int(180))
)[1 to 5]
let $root := $doc/node()
order by xs:int(fn:string($root/heightCm)) descending
return fn:string($root/name) || " — " || fn:string($root/heightCm) || "cm"
Jade — 200cm
Sean — 198cm
Sean — 195cm
Debra — 188cm
David — 186cm
Querying by name
xquery version "1.0-ml";
(: Query llamas whose name sorts at or after "S" using a codepoint string collation. :)
(: Requires a path range index on /name with collation http://marklogic.com/collation/ :)
for $doc in cts:search(
fn:collection("wild-llamas"),
cts:path-range-query(
"/name",
">=",
"S",
("collation=http://marklogic.com/collation/")
)
)[1 to 5]
let $root := $doc/node()
order by fn:string($root/name)
return fn:string($root/name)
Sean
Sean
Susan
Todd
Xavier
Quick Reference
| Scalar type | Best use case | Comparison semantics | Relative storage cost |
|---|---|---|---|
int | Counts, measurements, thresholds | Numeric integer | Low |
decimal | Money, exact quantities | Exact numeric | Medium |
double | Measurements and telemetry | Approximate numeric | Medium |
dateTime | Precise timestamps | UTC-normalised temporal | Medium |
date | Calendar-only data | Date ordering | Medium |
string | Names, labels, codes | Collation-based lexical | Higher |
anyURI | Canonical identifiers | Collation-based lexical | Higher |
point | Point-valued geospatial content | Point-aware scalar | Specialised |
Common Design Patterns
Pattern 1: Separate storage field from search field
If upstream systems send dirty values, preserve the raw string for auditing and index a cleaned numeric or temporal field for search. That gives you reliable range semantics without losing the original source payload.
Pattern 2: Multiple indexes for one business concept
Sometimes the business concept is singular while the search semantics are plural. A code might need both lexical prefix searches and numeric ordering. Either dual-index the same field carefully or split it into explicit search-oriented fields.
Pattern 3: Treat index changes as application changes
A missing or mis-typed range index changes correctness, not just speed. That means index configuration belongs in deployment automation, code review, and release planning.
Troubleshooting Checklist
| Symptom | Likely cause | What to inspect first |
|---|---|---|
"9" > "10" style results | String index on numeric-looking data | Scalar type and collation |
| Documents missing from results | Invalid values not indexed | Source data quality and coercion rules |
| Unexpected timestamp matches | UTC normalisation | Timezone handling and literal construction |
| Query throws missing-index error | Index not configured or wrong type | Database index definitions |
| Too much index storage | Over-indexing with broad string types | Whether a narrower numeric or temporal type is sufficient |
Final Takeaways
Range queries are only as correct as the scalar types beneath them. An index is not just acceleration, it is interpretation. Use numeric types for numeric questions. Use temporal types for temporal questions. Use string types when lexical semantics are genuinely what you want, and configure collation explicitly. If the data is messy, clean it before it reaches the index or index a normalised companion field.
That discipline pays off twice: your results are more trustworthy, and your indexes are typically smaller, faster, and easier to reason about. It also means fewer late-night mysteries, which every human and every clever llama can appreciate.
Need Some Help?
Looking for more information on this subject or any other topic related to MarkLogic? Contact Us (info@cleverllamas.com) to find out how we can assist you with consulting or training!
- Working Assumptions for the Examples
- Supported Scalar Types at a Glance
- Scalar Type Determines Comparison Semantics
- The Classic String Gotcha
- Numeric Indexes Behave the Way Humans Expect
- Index Typing Is Not Schema Typing
- What Gets Indexed, Dropped, or Truncated
- Practical Guidance on Invalid Values
- String Indexes and Collation
- dateTime Indexes Normalise to UTC
- Query Implications
- Multiple Indexes on the Same Field
- Performance Characteristics by Scalar Type
- Element Range Queries vs JSON Path Range Queries
- What Happens Without the Index
- If You Truly Need a Fallback
- Nulls, Missing Values, and Empty Structures
- Index Configuration via the Management API
- Llamaverse Examples
- Quick Reference
- Common Design Patterns
- Pattern 1: Separate storage field from search field
- Pattern 2: Multiple indexes for one business concept
- Pattern 3: Treat index changes as application changes
- Troubleshooting Checklist
- Final Takeaways