cac298e745
support, tarnslations for existing pages and articles
301 lines
14 KiB
Markdown
301 lines
14 KiB
Markdown
Title: HowTo: The FHIR `date` Search Parameter – Understanding Prefixes, Intervals, and Combinations
|
||
Slug: howto-fhir-date-parameter
|
||
Category: work
|
||
Tags: FHIR, howto
|
||
Date: 2026-08-12
|
||
Lang: en
|
||
Status: published
|
||
|
||
Anyone building or querying a FHIR server sooner or later runs into the
|
||
`date` search parameter. At first glance it looks simple:
|
||
`?date=2023-06-15`. But as soon as prefixes like `ge`, `sa`, or `ap` come
|
||
into play, and multiple `date` parameters get combined, things get
|
||
confusing fast. This article brings order to the chaos: step by step, with
|
||
timeline graphics for every case, and a quick-reference table for fast
|
||
lookups.
|
||
|
||
## Table of Contents
|
||
|
||
[TOC]
|
||
|
||
---
|
||
|
||
## Quick Reference: All Prefixes at a Glance
|
||
|
||
For a fast glance in between. Details and graphics for every row follow in
|
||
the sections below.
|
||
|
||
| Prefix | Name | Short rule | Example |
|
||
|---|---|---|---|
|
||
| *(none)* | implicit `eq` | Precision of the value determines the interval | `date=2023-06-15` |
|
||
| `eq` | equal to | Resource interval fully within the search interval | `date=eq2023-06-15` |
|
||
| `ne` | not equal to | Complement of `eq` | `date=ne2023-06-15` |
|
||
| `gt` | greater than | Extends beyond the end (overlap is sufficient) | `date=gt2023-06-15` |
|
||
| `lt` | less than | Begins before the start (overlap is sufficient) | `date=lt2023-06-15` |
|
||
| `ge` | greater or equal | `gt` or fully contained (`eq`) | `date=ge2023-06-15` |
|
||
| `le` | less or equal | `lt` or fully contained (`eq`) | `date=le2023-06-15` |
|
||
| `sa` | starts after | Entirely after, **no** overlap | `date=sa2023-06-15` |
|
||
| `eb` | ends before | Entirely before, **no** overlap | `date=eb2023-06-15` |
|
||
| `ap` | approximately | Overlap + similar order of magnitude | `date=ap2023-06-15` |
|
||
|
||
**Missing `start`/`end` on a `Period`:** a missing `start` means `-∞`, a
|
||
missing `end` means `+∞`. That's why `sa` can never match when `start` is
|
||
missing, and `eb` can never match when `end` is missing (details in
|
||
[Section 4](#4-open-incomplete-periods)).
|
||
|
||
---
|
||
|
||
## The Core Principle: FHIR Compares Intervals, Not Points in Time
|
||
|
||
The most important mental reset first: FHIR never treats date values as an
|
||
exact point in time. It always treats them as an **interval**. How wide
|
||
that interval is depends on the precision of the given value:
|
||
|
||
- `2023` → the entire year 2023
|
||
- `2023-06` → all of June 2023
|
||
- `2023-06-15` → all of June 15, 2023 (00:00–24:00)
|
||
- `2023-06-15T10:00:00` → (almost) an exact point in time
|
||
|
||
Every comparison operator then compares **two intervals** with each other:
|
||
the search interval from your query parameter, and the resource interval
|
||
from the resource's field (e.g. `Patient.birthDate` or
|
||
`Encounter.period`).
|
||
|
||
### No prefix means `eq`, but precision still matters
|
||
|
||
If you don't specify a prefix, the server assumes `eq`. The resource date
|
||
must then lie **fully** within your search interval. The coarser the
|
||
precision, the wider the interval, and the more resource values fit
|
||
inside it:
|
||
|
||

|
||
*FHIR date search parameter WITHOUT a prefix*
|
||
|
||
`date=2023-06-15` only matches this single day. `date=2023-06`, on the
|
||
other hand, matches every day in June, including the 15th. That's the
|
||
most common trap for beginners: a "less precise" search query isn't
|
||
stricter, it's looser.
|
||
|
||
---
|
||
|
||
## The Nine Prefixes at a Glance
|
||
|
||
FHIR defines nine prefixes for `date` in total. Each describes a different
|
||
relationship between the search interval (red dashed in the graphic) and
|
||
the matching range (colored/shaded):
|
||
|
||

|
||
*All prefixes at a glance*
|
||
|
||
| Prefix | Name | Meaning |
|
||
|---|---|---|
|
||
| `eq` | equal to | Resource interval lies fully within the search interval |
|
||
| `ne` | not equal to | Complement of `eq` |
|
||
| `gt` | greater than | Resource interval **extends beyond** the end of the search interval |
|
||
| `lt` | less than | Resource interval **begins before** the start of the search interval |
|
||
| `ge` | greater or equal | `gt` **or** fully contained within the search interval (`eq`) |
|
||
| `le` | less or equal | `lt` **or** fully contained within the search interval (`eq`) |
|
||
| `sa` | starts after | Begins entirely **after** the end of the search interval, no overlap |
|
||
| `eb` | ends before | Ends entirely **before** the start of the search interval, no overlap |
|
||
| `ap` | approximately | Overlap, plus a similar order of magnitude between the intervals |
|
||
|
||
**Important, containment vs. overlap:** Only `eq` (and, implicitly,
|
||
`sa`/`eb`) require the resource interval to lie **fully** within, or
|
||
**fully separated** from, the search interval. For `gt`, `lt`, `ge`, and
|
||
`le`, an **overlap** is enough: the resource interval doesn't have to lie
|
||
completely outside the search range, it just needs to extend past the
|
||
respective boundary. More on that in the next section.
|
||
|
||
---
|
||
|
||
## Matching Against `Period` and `Range`
|
||
|
||
The graphics above show the resource date as a thin red marker. That
|
||
works for simple `date`/`dateTime` fields like `Patient.birthDate`. As
|
||
soon as the target field is itself an **interval** (a `Period`, e.g.
|
||
`Encounter.period`, `Condition.onsetPeriod`, or a `Range`), the
|
||
distinction between **containment** (fully contained) and **overlap**
|
||
(mere overlap) becomes crucial:
|
||
|
||
- **`eq`** requires containment: the entire resource interval must lie
|
||
within the search interval.
|
||
- **`gt` / `lt` / `ge` / `le`** only require overlap: it's enough for the
|
||
resource interval to extend past the respective boundary, even if part
|
||
of it lies on the "wrong" side.
|
||
- **`sa` / `eb`** require the opposite of overlap: the resource interval
|
||
must lie fully, and without any overlap, on the respective side.
|
||
|
||
For a simple point-in-time field (no `Period`), this distinction barely
|
||
shows, since the resource's start and end are (almost) identical. There,
|
||
`gt` and `sa` behave de facto the same. The difference only becomes
|
||
relevant for actual time spans. That's also why `sa`/`eb` are, per the
|
||
specification, primarily intended for `Period`/`Range` values.
|
||
|
||
**Example:** An encounter runs from June 10 to June 20. The search
|
||
`date=gt2023-06-15` should find it, even though the encounter *started*
|
||
before the 15th. What matters is only that it extends past the 15th
|
||
(overlap with the range "after the 15th"). With `date=sa2023-06-15`, the
|
||
same encounter would **not** match, because it doesn't lie entirely after
|
||
the 15th. That's exactly the practical difference between `gt` and `sa`.
|
||
|
||
The following graphic makes this visible for all four "one-sided" prefixes
|
||
(`gt`, `sa`, `lt`, `eb`) using the same three example periods: one
|
||
entirely before, one overlapping the boundary, and one entirely after:
|
||
|
||

|
||
*Matching against Period/Range (containment vs. overlap)*
|
||
|
||
You can clearly see: `gt` and `lt` (blue shaded area, solid boundary line)
|
||
let the overlapping Period B through as a match, while `sa` and `eb`
|
||
(dashed boundary line) reject it. Only a period lying entirely on the
|
||
correct side counts there.
|
||
|
||
| Encounter timeframe | `date=gt2023-06-15` | `date=sa2023-06-15` |
|
||
|---|---|---|
|
||
| June 10–20 (overlaps the boundary) | ✅ Match (overlap) | ❌ No match (not entirely after) |
|
||
| June 16–20 (entirely after) | ✅ Match | ✅ Match |
|
||
| June 1–10 (entirely before) | ❌ No match | ❌ No match |
|
||
|
||
---
|
||
|
||
## Open (Incomplete) Periods
|
||
|
||
In practice, periods are often **not closed**, for example an ongoing
|
||
encounter without an `end`, or an imported record with an unknown
|
||
`start`. The FHIR standard handles this clearly:
|
||
|
||
> A missing lower bound (`start`) is implicitly treated as **less than any
|
||
> real date** (i.e. `-∞`). A missing upper bound (`end`) is implicitly
|
||
> treated as **greater than any real date** (i.e. `+∞`).
|
||
|
||
This has noticeable consequences, especially for `sa` and `eb`:
|
||
|
||
- An ongoing encounter (`start` set, `end` missing, so `end = +∞`) can
|
||
**never** match `eb` (ends before). Its end never lies before any
|
||
finite date. It will, however, match `gt`, because its (infinite) end
|
||
always extends beyond any search boundary.
|
||
- An encounter with an unknown start (`start` missing, so `start = -∞`)
|
||
can **never** match `sa` (starts after). Its beginning never lies after
|
||
any finite date. It will, however, match `lt`, because its (infinite)
|
||
beginning always reaches before any search boundary.
|
||
- A period with neither `start` nor `end` overlaps practically **every**
|
||
overlap-based filter (`gt`, `lt`, `ge`, `le`). By definition, it
|
||
"extends" past every boundary.
|
||
|
||
The same four prefixes as in [Section 3](#3-matching-against-period-and-range),
|
||
now with open instead of closed periods:
|
||
|
||

|
||
*Matching against open (incomplete) Period/Range*
|
||
|
||
**Practical consequence:** If you want to specifically exclude "ongoing"
|
||
resources (e.g. active encounters, open orders) from a date-range search,
|
||
`eb`/`sa` alone won't do it. You'll additionally need to filter by status,
|
||
or explicitly by `end:missing=true`/`start:missing=true`, depending on
|
||
what your server supports.
|
||
|
||
---
|
||
|
||
## Combining Multiple `date` Parameters
|
||
|
||
In practice, a single prefix is rarely enough. Usually you want to define
|
||
a **range**. FHIR servers always combine multiple `date` parameters in the
|
||
same query with **AND**. Four typical patterns:
|
||
|
||

|
||
*Combining multiple parameters (AND logic)*
|
||
|
||
| Combination | Example | Character |
|
||
|---|---|---|
|
||
| `ge` + `le` | `ge2023-01-01&le2023-12-31` | Closed interval (both bounds inclusive) |
|
||
| `gt` + `lt` | `gt2023-01-01<2023-12-31` | Open interval (both bounds exclusive) |
|
||
| `sa` + `eb` | `sa2023-01-01&eb2023-12-31` | Strictly separated range, primarily for Period values |
|
||
| `ge` + `lt` | `ge2023-06-01<2023-07-01` | Half-open interval `[start, end)`, the standard pattern for "exactly one month" |
|
||
|
||
If you work with `[start, end)` in timestamp-heavy ETL pipelines, `ge` +
|
||
`lt` will look familiar: it's exactly the same pattern used for classic
|
||
time-window queries in SQL.
|
||
|
||
---
|
||
|
||
## The Full 3×3 Matrix of Range Combinations
|
||
|
||
The four patterns above are only the most common ones. In fact, **any**
|
||
lower bound can be combined with **any** upper bound:
|
||
|
||
- Lower bounds: `ge`, `gt`, `sa`
|
||
- Upper bounds: `le`, `lt`, `eb`
|
||
|
||
That gives 3 × 3 = **9 possible combinations**, all spec-compliant:
|
||
|
||

|
||
*3x3 matrix of range combinations (lower bound × upper bound)*
|
||
|
||
They differ only in whether the respective boundary is inclusive (solid
|
||
line) or exclusive (dashed line), and whether it's a simple value
|
||
comparison (`ge`/`gt`/`le`/`lt`) or a strict Period comparison
|
||
(`sa`/`eb`). There is **no explicit exclusion rule** in the FHIR
|
||
standard. Every cell in this matrix is a valid, functioning query.
|
||
|
||
---
|
||
|
||
## Edge Cases: When Combinations Become Redundant or Contradictory
|
||
|
||
Not every syntactically valid combination is practically meaningful. There
|
||
are three categories you should know about before one of them ends up in a
|
||
generated query by accident:
|
||
|
||

|
||
*Edge cases in parameter combinations*
|
||
|
||
### Redundant combinations
|
||
Don't change the result, but aren't technically an error either:
|
||
- `eq` plus a boundary that's already satisfied by the `eq` interval
|
||
(`eq2023-06-15&ge2020-01-01`): the extra condition is superfluous.
|
||
- Two conditions of the same directional type
|
||
(`ge2020-01-01&ge2023-01-01`): only the stricter (later) lower bound
|
||
actually takes effect.
|
||
|
||
### Combinations with a guaranteed empty result
|
||
Structurally impossible to satisfy:
|
||
- `eq` + `ne` on the same value (`eq2023-06-15&ne2023-06-15`): mutually
|
||
exclusive conditions.
|
||
- The lower bound lies after the upper bound in time
|
||
(`ge2023-06-01&le2023-01-01`): no date can satisfy both at once.
|
||
|
||
The server will **syntactically accept** such queries and simply return an
|
||
empty result set. Not an error, but not a helpful response either. It's
|
||
worth validating this client-side before sending the request.
|
||
|
||
### Unusual but valid edge cases
|
||
- **Three or more `date` parameters**, e.g. a year range with a single day
|
||
excluded: `ge2023-01-01&le2023-12-31&ne2023-06-15`.
|
||
- **`ap` combined with a boundary**, e.g.
|
||
`ap2023-06-15&ge2023-01-01`: rarely used, since `ap` is already fuzzy
|
||
by nature, and the tolerance calculation remains server-dependent.
|
||
|
||
---
|
||
|
||
## Practical Recommendations
|
||
|
||
- For simple range filters, **`ge` + `lt`** (half-open interval) is
|
||
usually the most robust pattern. It avoids rounding issues at precision
|
||
boundaries (no off-by-one when you mean "through December 31st
|
||
inclusive").
|
||
- Use **`sa`/`eb`** deliberately when you need *complete* separation on
|
||
`Period`/`Range` fields (see [Section 3](#3-matching-against-period-and-range)),
|
||
e.g. "encounter entirely after discharge X". For a simple "from date X
|
||
onward" filter, you usually mean `ge`/`gt`, not `sa`.
|
||
- For `Period` fields with possible open ends (see
|
||
[Section 4](#4-open-incomplete-periods)), don't forget: `sa`/`eb`
|
||
structurally never match there. Additionally filter by `:missing` or
|
||
status if needed.
|
||
- Before sending a multi-parameter query, it's worth a quick sanity check
|
||
on the logical consistency between the lower and upper bound, to avoid
|
||
empty results caused by configuration mistakes in your own pipeline.
|
||
|
||
With the quick reference above and the six graphics in mind, no prefix,
|
||
all nine prefixes individually, containment vs. overlap for Period/Range
|
||
(both open and closed), typical combinations, the full 3×3 matrix, and the
|
||
edge cases, your next `date` query in FHIR should no longer be a mystery.
|