Files
haemka.de/content/en/howto-fhir-date-parameter.md
T
2026-08-13 15:00:56 +02:00

301 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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:0024: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:
![Date without a prefix]({static}/images/fhir-date-no-prefix_en.svg)
*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]({static}/images/fhir-date-all-prefixes_en.svg)
*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]({static}/images/fhir-date-period-range-matching_en.svg)
*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 1020 (overlaps the boundary) | ✅ Match (overlap) | ❌ No match (not entirely after) |
| June 1620 (entirely after) | ✅ Match | ✅ Match |
| June 110 (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 Period/Range]({static}/images/fhir-date-open-periods_en.svg)
*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]({static}/images/fhir-date-combinations_en.svg)
*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&lt2023-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&lt2023-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]({static}/images/fhir-date-matrix-3x3_en.svg)
*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]({static}/images/fhir-date-edge-cases_en.svg)
*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.