---
tags:
  - woql
  - reference
  - advanced
title: Range Queries with triple_slice and quad_slice
nextjs:
  metadata:
    title: Range Queries with triple_slice and quad_slice
    keywords: woql range query triple_slice quad_slice dateTime integer string performance
    description: How to use triple_slice and quad_slice for fast range queries on typed values in TerminusDB, with automatic type inference from the schema
    openGraph:
      images: https://assets.terminusdb.com/docs/technical-documentation-terminuscms-og.png
    alternates:
      canonical: https://terminusdb.com/docs/woql-triple-slice/
media: []
---

Range queries let you efficiently find documents whose property values fall within a specified range. `triple_slice` and `quad_slice` push range constraints directly into the storage engine for O(log n) lookups instead of scanning every triple.

## Why Range Queries Matter

When you need to find documents within a time window, a numeric band, or a lexicographic string range, the traditional WOQL approach requires fetching every value and filtering in the query layer:

```javascript
// The slow way: fetch all timestamps, then filter
let v = Vars("doc", "time");
and(
  triple(v.doc, "timestamp", v.time),
  greater(v.time, literal("2025-01-01T00:00:00Z", "xsd:dateTime")),
  less(v.time, literal("2026-01-01T00:00:00Z", "xsd:dateTime"))
)
```

This works, but it materializes every triple for the `timestamp` predicate before discarding the ones outside the range. For large datasets, this is prohibitively slow.

`triple_slice` solves this by binary-searching the sorted value dictionary and iterating only the matching range. The storage engine never touches values outside your bounds.

### When to Use `triple_slice`

- **Time-series data**: Find events, logs, or measurements within a time window
- **Numeric filtering**: Select records in a price band, age range, or score bracket
- **String ranges**: Lexicographic slicing for alphabetical partitions
- **Membership checks**: Verify that a known value falls within expected bounds

## What `triple_slice` Does

### Signature

```
triple_slice(Subject, Predicate, Object, Low, High)
quad_slice(Subject, Predicate, Object, Low, High, Graph)
```

`triple_slice` is a superset of `triple`. It adds two optional bound parameters, `Low` and `High`, that constrain the Object to a half-open range `[Low, High)`. When both bounds are unbound, it behaves identically to `triple`.

`quad_slice` is the same predicate with an explicit graph selector (like `quad` extends `triple`).

### Half-Open Range `[Low, High)`

- **Low is inclusive**: the first matching value is >= Low
- **High is exclusive**: all matching values are strictly < High
- Adjacent slices `[A, B)` and `[B, C)` partition the space cleanly with no overlap and no gaps

### Binding Modes

`triple_slice` works as both a **generator** (when Object is unbound, it produces matching triples) and a **pattern matcher** (when Object is ground, it checks membership).

| Object | Low | High | Behavior |
|--------|-----|------|----------|
| unbound | unbound | unbound | Same as `triple` — generates all triples. Low and High unify with Object. |
| unbound | bound | unbound | Generates triples where Object >= Low. |
| unbound | unbound | bound | Generates triples where Object < High. |
| unbound | bound | bound | Generates triples where Low <= Object < High. |
| ground | unbound | unbound | Same as `triple` — membership check. Low and High unify with Object. |
| ground | bound | unbound | Checks triple exists AND Object >= Low. Fails if Object < Low. |
| ground | unbound | bound | Checks triple exists AND Object < High. Fails if Object >= High. |
| ground | bound | bound | Checks triple exists AND Low <= Object < High. **Fails if out of range**, even if the triple exists. |

The key subtlety: when Object is ground and the triple exists but the value falls outside the range, the predicate **fails**. The range constraint always takes precedence.

### Automatic Type Inference

When the predicate is ground (which is the common case), `triple_slice` infers the correct XSD type from the schema and automatically casts untyped bounds. You do not need explicit `typecast` calls.

For example, if the schema declares `timestamp` as `xsd:dateTime`, then the string `"2025-01-01"` is automatically cast to `xsd:dateTime`. If the cast fails (e.g., `"not-a-date"` to `xsd:dateTime`), an error is raised — there are no silent failures.

When the predicate is unbound, bounds must either be already typed or be unbound variables.

### Supported Types

All data is stored in ordered form with succinct data structures, including the XSD types. Examples of the ordered types that can be used with `triple_slice`:

- **Date/Time**: `xsd:dateTime`, `xsd:dateTimeStamp`, `xsd:date`, `xsd:time`
- **Numeric**: `xsd:integer`, `xsd:decimal`, `xsd:float`, `xsd:double`, `xsd:nonNegativeInteger`, `xsd:positiveInteger`, `xsd:long`, `xsd:int`, `xsd:short`, `xsd:byte`
- **String**: `xsd:string`

## How to Use `triple_slice` — Worked Examples

The following examples use a schema with a `SensorReading` class:

```json
{
  "@type": "Class",
  "@id": "SensorReading",
  "@key": { "@type": "Random" },
  "sensor_id": "xsd:string",
  "timestamp": "xsd:dateTime",
  "temperature": "xsd:decimal",
  "label": { "@type": "Optional", "@class": "xsd:string" }
}
```

With sample data: five readings across January 2025 with temperatures from 18.5 to 23.1 and labels "A" through "E".

---

### Example 1 — Classic DateTime Range

Find all sensor readings from the first half of January 2025.

```javascript
let v = Vars("doc", "time");
triple_slice(v.doc, "timestamp", v.time, "2025-01-01T00:00:00Z", "2025-01-15T00:00:00Z")
```

**What happens**: The engine looks up `timestamp` in the schema, sees it is `xsd:dateTime`, casts the string bounds to `xsd:dateTime`, and binary-searches the value dictionary. Only readings with timestamps in `[Jan 1, Jan 15)` are returned.

---

### Example 2 — Open-Ended High: From a Date Onward

Find all readings from January 20 onward (no upper bound).

```javascript
let v = Vars("doc", "time");
triple_slice(v.doc, "timestamp", v.time, "2025-01-20T00:00:00Z")
```

With Low bound and High unbound, this generates all triples where timestamp >= Jan 20.

---

### Example 3 — Open-Ended Low: Before a Date

Find all readings before January 10.

```javascript
let v = Vars("doc", "time", "low");
and(
  eq(v.low, literal("2025-01-10T00:00:00Z", "xsd:dateTime")),
  triple_slice(v.doc, "timestamp", v.time, null, v.low)
)
```

With Low unbound and High bound, this generates all triples where timestamp < Jan 10.

---

### Example 4 — No Bounds: Degenerates to `triple`

When neither Low nor High are set, `triple_slice` behaves identically to `triple`.

```javascript
let v = Vars("doc", "time", "low", "high");
triple_slice(v.doc, "timestamp", v.time, v.low, v.high)
```

Returns all `timestamp` triples. For each result, `low` and `high` both unify with the timestamp value, so `low == high == time`.

---

### Example 5 — Membership Check: Object In Range

Check that a specific reading's timestamp falls within the expected window.

```javascript
let v = Vars("doc");
and(
  isa(v.doc, "SensorReading"),
  triple_slice(v.doc, "timestamp",
    literal("2025-01-10T08:30:00Z", "xsd:dateTime"),
    "2025-01-01T00:00:00Z", "2025-01-15T00:00:00Z")
)
```

The Object is ground. The predicate checks: does this triple exist AND does `2025-01-10T08:30:00Z` fall in `[Jan 1, Jan 15)`? If yes, succeeds. If the timestamp exists but is outside the range, **fails**.

---

### Example 6 — Membership Check: Object Out of Range

This demonstrates the key subtlety: a triple can exist but still fail the range check.

```javascript
let v = Vars("doc");
and(
  isa(v.doc, "SensorReading"),
  triple_slice(v.doc, "timestamp",
    literal("2025-01-25T14:00:00Z", "xsd:dateTime"),
    "2025-01-01T00:00:00Z", "2025-01-15T00:00:00Z")
)
```

The timestamp `Jan 25` exists in the database, but it is outside `[Jan 1, Jan 15)`, so the predicate **fails**. This is not an error — it is correct behavior. The range constraint takes precedence over triple existence.

---

### Example 7 — Numeric Range: Integer Values

Find readings with temperature between 19.0 and 22.0.

```javascript
let v = Vars("doc", "temp");
triple_slice(v.doc, "temperature", v.temp, "19.0", "22.0")
```

The engine infers `xsd:decimal` from the schema for `temperature` and casts the string bounds accordingly.

---

### Example 8 — String Range: Lexicographic Slice

Find readings with labels in the lexicographic range `[B, D)`.

```javascript
let v = Vars("doc", "lbl");
triple_slice(v.doc, "label", v.lbl, "B", "D")
```

Returns labels "B" and "C" (but not "D", since the high bound is exclusive).

---

### Example 9 — Adjacent Non-Overlapping Slices

Two adjacent slices partition the data cleanly — no double-counting, no gaps.

```javascript
let v = Vars("doc", "time");

// First half of January
let slice1 = triple_slice(v.doc, "timestamp", v.time,
  "2025-01-01T00:00:00Z", "2025-01-15T00:00:00Z");

// Second half of January
let slice2 = triple_slice(v.doc, "timestamp", v.time,
  "2025-01-15T00:00:00Z", "2025-02-01T00:00:00Z");
```

The union of `slice1` and `slice2` equals the full month of January. A reading at exactly `2025-01-15T00:00:00Z` appears only in `slice2` (since Low is inclusive and High is exclusive).

---

### Example 10 — Unbound Predicate with Typed Bounds

When the predicate is unbound, auto-type-inference cannot determine the target type. Bounds must be explicitly typed.

```javascript
let v = Vars("doc", "pred", "val");
triple_slice(v.doc, v.pred, v.val,
  literal("2025-01-01T00:00:00Z", "xsd:dateTime"),
  literal("2025-01-15T00:00:00Z", "xsd:dateTime"))
```

This iterates over all predicates on each document, returning any triple whose object is a `xsd:dateTime` value in the given range.

---

### Example 11 — Before and After: The Old Way vs `triple_slice`

**Before** (full scan + filter):

```javascript
let v = Vars("doc", "time");
and(
  triple(v.doc, "timestamp", v.time),
  greater(v.time, literal("2025-01-01T00:00:00Z", "xsd:dateTime")),
  less(v.time, literal("2025-01-15T00:00:00Z", "xsd:dateTime"))
)
```

**After** (range pushed to storage):

```javascript
let v = Vars("doc", "time");
triple_slice(v.doc, "timestamp", v.time,
  "2025-01-01T00:00:00Z", "2025-01-15T00:00:00Z")
```

Both return the same results. The `triple_slice` version binary-searches the value dictionary and never touches values outside the range.

---

### Example 12 — `quad_slice` on Instance Graph

`quad_slice` adds an explicit graph parameter, just like `quad` extends `triple`.

```javascript
let v = Vars("doc", "time");
quad_slice(v.doc, "timestamp", v.time,
  "2025-01-01T00:00:00Z", "2025-01-15T00:00:00Z",
  "instance")
```

Equivalent to `triple_slice` but explicitly targets the instance graph.

---

### Example 13 — `quad_slice` on Schema Graph

Query the schema graph for range information.

```javascript
let v = Vars("cls", "pred", "val");
quad_slice(v.cls, v.pred, v.val,
  literal("A", "xsd:string"),
  literal("M", "xsd:string"),
  "schema")
```

Searches the schema graph for any triple whose object is a string in `[A, M)`.

## Reference

### Error Conditions

- **Invalid cast**: Bounds that cannot be cast to the predicate's type produce a clear error (e.g., `"not-a-date"` for an `xsd:dateTime` predicate)
- **Unbound predicate + untyped bounds**: If the predicate is unbound and bounds are plain strings (not typed), an error is raised since the target type cannot be inferred
- **Non-existent predicate**: Behaves like `triple` — no results, no error

### Performance Characteristics

- **Binary search**: O(log n) to locate range bounds in the sorted value dictionary
- **Streaming**: Results are yielded one at a time — memory usage is O(1) regardless of range size
- **No full scan**: Values outside the range are never touched
- **Layer stack**: Each layer in the stack requires a separate binary search; rollups improve performance for deep stacks
