---
{
  "title": "SQL on FHIR: First-Class ValueSets",
  "description": "Eine Diabetes-Kohorte, zwei Wege zur Nutzung eines ValueSets in SQL. Von ViewDefinition zu deklarierten Abhängigkeiten, Joins und member_of — ein Vorschlag, den wir testen möchten.",
  "date": "2026-09-16",
  "author": "Nikolai Ryzhikov",
  "reading-time": "6 min read",
  "tags": ["SQL on FHIR", "Terminology", "FHIR Standard", "Analytics"],
  "tldr": "Eine SQL-Abfrage benötigt sowohl Patientendaten als auch Terminologie. Wir untersuchen, wie man ein ValueSet als Abhängigkeit neben einer ViewDefinition deklariert und seine Zugehörigkeit dann als Relation oder als member_of-Funktion bereitstellt. Die Arbeitsgruppe tendiert derzeit zur Relation, aber der nächste Schritt ist Implementieren, Experimentieren und Entscheiden."
}
---

> For the complete documentation index, see [llms.txt](https://www.health-samurai.io/llms.txt).
> Use it to discover all available pages before guessing URLs.

---
## Eine Frage: Welche Patientinnen und Patienten haben eine Diabetes-Diagnose?

Beginnen wir mit einer einfachen Aufgabe:

> Gib Patientinnen und Patienten zurück, deren Diagnosecode zum Diabetes-ValueSet gehört.

Wir benötigen zwei Eingaben: die Diagnosecodes der Patientinnen und Patienten sowie die im ValueSet enthaltenen Codes. Eine Tabelle mit Conditions liefert uns das Erste, nicht jedoch das Zweite.

Hier sind drei Diagnosezeilen und die beiden Mitglieder unseres Beispiel-Diabetes-ValueSets:

```yaml
conditions:
  - patient_id: p1
    system: http://snomed.info/sct
    version: "2026-02"
    code: "73211009"
  - patient_id: p2
    system: http://snomed.info/sct
    version: "2026-02"
    code: "44054006"
  - patient_id: p3
    system: http://snomed.info/sct
    version: "2026-02"
    code: "22298006"

diabetes_codes:
  - system: http://snomed.info/sct
    version: "2026-02"
    code: "73211009"
    display: Diabetes mellitus
  - system: http://snomed.info/sct
    version: "2026-02"
    code: "44054006"
    display: Type 2 diabetes mellitus
```

Die Antwort sollte **p1 und p2** sein. Der Diagnosecode für p3 ist nicht in diesem ValueSet enthalten. Wir werden beide Eingaben erstellen und zwei Wege ausprobieren, dieselbe Frage zu stellen.

*Die Beispiele verwenden YAML zur besseren Lesbarkeit und zur Veranschaulichung von Terminologieversionen. Dieses kleine ValueSet ist keine vollständige klinische Definition. Ressourcenauszüge sind kein ausführbares Paket, und die ValueSet-Schnittstelle ist noch ein Vorschlag.*

Ich sehe hier zwei Phasen. Wenn wir FHIR-Ressourcen glätten, können wir Terminologie normalisieren — zum Beispiel Diagnosecodes mithilfe von FHIRPath-Funktionen übersetzen. Später, wenn wir eine Kohorte erstellen, müssen wir Terminologie innerhalb einer SQL-Abfrage verwenden. Das sind verwandte Aufgaben, die aber unterschiedliche Schnittstellen benötigen.

Für einige Kennzahlen nehmen wir bereits ein erweitertes ValueSet, behandeln es als Tabelle und schreiben SQL dagegen. Die Frage ist, wie man das zu einer gemeinsamen Schnittstelle macht, anstatt etwas, das jede Implementierung selbst erfindet.

John Grimes hat dies mit dem Problem verbunden, das Terminologieserver bereits lösen:

> „Dies ist das Problem, das der Terminologieserver beim Vorbereiten von Terminologiedaten für Laufzeitabfragen löst. Wir versuchen im Wesentlichen dasselbe für analytische Anwendungsfälle."
>
> — John Grimes, 15. September (leicht überarbeitet)

## Zuerst: Conditions in Zeilen umwandeln

Eine **ViewDefinition** in SQL on FHIR beschreibt, wie Zeilen und Spalten aus FHIR-Ressourcen mithilfe von FHIRPath extrahiert werden. Sie enthält kein SQL.

Hier ist eine vereinfachte Sicht auf `Condition`:

```yaml
resourceType: ViewDefinition
url: http://example.org/ViewDefinition/conditions
version: 1.0.0
name: conditions
status: draft
resource: Condition
select:
  - column:
      - name: patient_id
        path: subject.getReferenceKey(Patient)
    select:
      - forEach: code.coding
        column:
          - name: system
            path: system
          - name: version
            path: version
          - name: code
            path: code
```

Das verschachtelte `select` kombiniert den Patientenbezeichner aus der Condition mit jedem Element in `code.coding`. Für diese Beispiele nehmen wir an, dass Patientenreferenzen `Patient/p1`, `Patient/p2` und `Patient/p3` lauten und der Runner ihre Schlüssel als `p1`, `p2` und `p3` darstellt. Die tatsächliche Schlüsseldarstellung ist runner-definiert; sie muss mit der entsprechenden Patient-View konsistent sein. `getReferenceKey(Patient)` schränkt den Referenztyp auf Patient ein.

Wir verwenden versionierte Codings, um den Abgleich explizit zu machen; echte Daten lassen die Version häufig weg.

Jetzt haben wir Patientendaten, die wir abfragen können. Wir müssen noch wissen, welche Codes als Diabetes gelten.

## Beide Eingaben für die SQL-View deklarieren

Eine SQL-View oder -Abfrage arbeitet auf den Zeilen, die durch ViewDefinitions erzeugt werden. SQL on FHIR stellt SQL-Abfragen mithilfe einer FHIR-`Library`-Ressource dar. Deren `relatedArtifact`-Liste deklariert Abhängigkeiten: `type: depends-on` besagt, dass eine Eingabe benötigt wird, `resource` identifiziert sie, und `label` gibt ihr einen lokalen Namen für SQL.

Am 1. September habe ich vorgeschlagen, dasselbe Muster auf ValueSets anzuwenden:

> „Wir können in der SQL-View über relatedArtifact auf eine ViewDefinition verweisen, ihr einen Tabellennamen geben und sie in einem Join verwenden. Wir können einen ähnlichen Ansatz für das ValueSet verwenden: Meine SQL-View hängt von einem ValueSet ab, und ich gebe ihm einen Namen."
>
> — Nikolai Ryzhikov, 1. September (leicht überarbeitet)

Für unsere Diabetes-Abfrage sieht die vorgeschlagene Abhängigkeitsdeklaration in diesem Library-Auszug so aus:

```yaml
resourceType: Library
url: http://example.org/Library/patients-with-diabetes
status: draft
type:
  coding:
    - system: http://hl7.org/fhir/uv/sql-on-fhir/CodeSystem/LibraryTypesCodes
      code: sql-query
relatedArtifact:
  - type: depends-on
    label: conditions
    resource: http://example.org/ViewDefinition/conditions|1.0.0
  - type: depends-on
    label: diabetes_codes
    resource: http://example.org/ValueSet/diabetes|2026
content:
  - contentType: application/sql
    url: https://example.org/queries/patients-with-diabetes.sql
```

Die erste Abhängigkeit liefert die Condition-Zeilen. Die zweite liefert die ValueSet-Zugehörigkeit. Der Teil nach `|` fordert eine bestimmte Version dieses Artefakts an.

Das SQL selbst gehört in `Library.content`: ein Attachment mit `contentType: application/sql`. Hier verweist `url` auf eine illustrative SQL-Datei; alternativ kann `data` das SQL base64-kodiert enthalten. Die beiden unten stehenden Abfragen sind alternative Inhalte dieser Datei. Wir zeigen sie als lesbare YAML-`sql: |`-Blöcke, nicht als zusätzliche Library-Felder.

Das meine ich mit **First-Class ValueSets**: Die Abfrage deklariert explizit die benötigte Terminologie, zusammen mit ihren Daten-Views. Der Runner — die Software, die die Abfrage ausführt — löst die Abhängigkeiten auf. SQL verwendet deren lokale Namen, anstatt URLs selbst aufzulösen.

John hat dieselbe Trennung aus der Runner-Perspektive beschrieben:

> „Sie sagen lediglich: Ich habe eine Abhängigkeit von diesem ValueSet. […] Für die SQL-Abstraktion möchten wir es so einfach wie möglich halten, insbesondere für die einfachen Fälle."
>
> — John Grimes, 15. September (leicht überarbeitet)

## Zugehörigkeit, kein Expansionsalgorithmus

Die zweite Abhängigkeit liefert die am Anfang gezeigte `diabetes_codes`-Zugehörigkeit.

Owen Loveluck fragte, ob die Abstraktion eher eine View als eine Tabelle sein sollte. Diese Unterscheidung ist wichtig: Eine Relation muss keine physische Tabelle sein. Sie kann eine View, gecachte Daten oder etwas bei Bedarf Ausgewertetes sein. Wie ich es am 8. September formulierte:

> „Wir können das vorab erweiterte ValueSet als Relation behandeln. Es ist uns egal, wie wir dorthin gelangt sind — ob es sich um eine dynamische Expansionsabfrage oder vorab expandierte Daten handelt, die in die Datenbank geladen wurden. Ich kann joinen und meine Kennzahl erstellen."
>
> — Nikolai Ryzhikov, 8. September (leicht überarbeitet)

Der Vorschlag standardisiert nicht, wie `ValueSet.compose`, ECL oder VCL ausgewertet werden. Er beschreibt, wie die resultierende Zugehörigkeit SQL zur Verfügung gestellt wird.

## Option A: Das ValueSet als Relation joinen

Der Runner stellt `diabetes_codes` als benannte Relation bereit. Unsere Abfrage joint dagegen:

```yaml
# Query text for illustration; `sql` is not a FHIR Library field.
sql: |
  SELECT DISTINCT c.patient_id
  FROM conditions c
  JOIN diabetes_codes vs
    ON vs.system = c.system
   AND vs.version = c.version
   AND vs.code = c.code

expected_patient_ids: [p1, p2]
```

## Option B: Zugehörigkeit mit einer Funktion prüfen

Dieselbe Abhängigkeit könnte stattdessen über `member_of` verfügbar sein:

```yaml
# Alternative query text, using the proposed function.
sql: |
  SELECT DISTINCT c.patient_id
  FROM conditions c
  WHERE member_of(
    c.system, c.version, c.code, 'diabetes_codes'
  )

expected_patient_ids: [p1, p2]
```

## Was unterscheidet die beiden Optionen?

Beide Abfragen geben **p1 und p2** zurück. Der Unterschied liegt darin, wie wir die Abfrage schreiben und was die Schnittstelle sonst noch ermöglicht.

Dies ist gewöhnliches SQL. Die Zugehörigkeit ist sichtbar: Wir können die Codes einsehen, zählen oder `display` in unsere Ausgabe einbeziehen. Wenn die Kohorte falsch ist, können wir beide Eingaben prüfen.

Zusätzliche Eigenschaften können ebenfalls Spalten sein. Gino Canessa wies darauf hin, während er Codes diskutierte, die nicht auswählbar sind:

> „Wenn nicht-auswählbar eine Eigenschaft ist, die Sie interessiert, machen Sie das während der Extraktion zu einer Spalte und haben dann direkt in SQL Zugriff darauf. Die Funktionen erlauben das nicht so schön."
>
> — Gino Canessa, 15. September (leicht überarbeitet)

Der Nachteil ist ein wiederholter mehrspaltenbasierter Join. Der Entwurf benötigt außerdem eine klare Eindeutigkeitsregel für `(system, version, code)`: Doppelte Zugehörigkeitszeilen können Abfrageergebnisse vervielfachen. `DISTINCT` schützt diese Patientenliste, aber nicht jedes Aggregat, das jemand später schreiben könnte.

Die Funktion ist prägnant, passt in boolesche Ausdrücke und vervielfacht keine Eingabezeilen. Ihre Implementierung könnte eine effiziente Suche anstelle eines Joins verwenden. Welche Variante besser abschneidet, hängt von der Datenbank-Engine ab.

Der Kompromiss ist Portabilität: `member_of` ist eine vorgeschlagene SQL on FHIR-Funktion, keine Standard-SQL-Funktion. Engines müssen sie unterstützen oder übersetzen. Sie beantwortet eine Zugehörigkeitsfrage, legt aber die Mitglieder nicht offen und gibt keinen Anzeigetext zurück.

Wenn ein Runner beide Schnittstellen unterstützt, sollten sie bei denselben Eingaben und demselben Terminologie-Snapshot zur selben Zugehörigkeit gelangen. Unsere beiden Abfragen sollten dieselben Patientinnen und Patienten zurückgeben.

Ein Runner muss außerdem deklarieren, ob er die Relation, die Funktion oder beides unterstützt. Übereinstimmende Zugehörigkeitsergebnisse allein machen eine Abfrage noch nicht portabel.

## Beide Optionen benötigen klare Versionsregeln

Im Beispiel gibt es zwei verschiedene Versionen: `diabetes|2026` identifiziert die ValueSet-Definition, während jede Zugehörigkeitszeile eine CodeSystem-Version trägt. Allein das Fixieren des ValueSets fixiert nicht notwendigerweise jede Terminologieabhängigkeit, die für dessen Expansion verwendet wird.

Das ist mir wichtig, weil wir einmal die Validierung in Aidbox durch die Verwendung von „latest" unterbrochen haben. Der Validator übernahm geänderte Encounter-Statuses aus R5, und die Validierung schlug auf Produktions-R4-Servern fehl. Seitdem fixieren wir so viel wie möglich und aktualisieren die Fixierungen bewusst.

Bei unserer Abfrage besteht das Risiko einer geänderten Patientenliste, obwohl sich das SQL nicht geändert hat. Wenn zwei Runner eine nicht-versionierte Abhängigkeit unterschiedlich auflösen, können sie unterschiedliche Ergebnisse zurückgeben. Owen hat genau diese Sorge in der Besprechung angesprochen.

Der Entwurf sieht konsistente Zugehörigkeit für jeden Ausführungslauf vor, zeichnet aufgelöste Versionen auf und lehnt mehrdeutige Auflösungen ab. Wie nicht-versionierte Abhängigkeiten behandelt werden sollen, ist aber noch offen. Unser Beispiel vermeidet diese Mehrdeutigkeit bewusst; eine Implementierung muss sie explizit behandeln, nicht stillschweigend erraten.

## Implementieren, experimentieren, dann entscheiden

Die Arbeitsgruppe tendiert derzeit zur Relation als Ausgangspunkt, mit der Funktion als optionaler Ergänzung. Das ist eine Richtung, keine Entscheidung. Beide Optionen haben nützliche Eigenschaften, und wir müssen sie an echten Abfragen ausprobieren.

John Grimes hat einen praktischen nächsten Schritt vorgeschlagen:

> „Ich denke, wir sollten einen Branch erstellen und anfangen, Dinge in der Spezifikation und der Referenzimplementierung aufzubauen, um ein Gefühl dafür zu bekommen."
>
> — John Grimes, 15. September (Füllwörter entfernt)

Wir beginnen mit Abfragen wie dieser und prüfen drei Dinge: Geben die Schnittstellen dieselben Patientinnen und Patienten zurück, wie gehen sie mit Versionen um, und wie einfach sind sie zu implementieren und zu verwenden? Wir müssen auch zusätzliche Eigenschaften testen und die Performance messen. Dann können wir entscheiden, was jeder Runner unterstützen muss.

Das unmittelbare Ziel ist einfach: Eine Abfrage sollte in der Lage sein zu sagen: **Ich hänge von diesem ValueSet ab**. Das Experiment soll uns helfen zu entscheiden, wie Runner es SQL bereitstellen und was jede Implementierung unterstützen muss.

Nehmen Sie an der [Diskussion zu Terminology in SQL on FHIR auf Zulip](https://chat.fhir.org/#narrow/channel/179219-analytics-on-FHIR/topic/Terminology.20in.20SQL.20on.20FHIR/with/622446640) teil und verfolgen Sie die [SQL on FHIR Working-Group-Meetings](/events/sql-on-fhir). Bringen Sie eine Abfrage mit, die Sie ausführen müssen, ein ValueSet, das schwierig zu handhaben ist, oder Erfahrungen bei der Implementierung eines der beiden Ansätze — solche Beispiele helfen uns, die Optionen zu testen und eine Entscheidung zu treffen.

## SQL on FHIR und Terminologie selbst ausprobieren

Bei Health Samurai entwickeln wir beide Seiten davon: **Aidbox für die Arbeit mit FHIR-Daten und SQL on FHIR sowie Termbox für FHIR-Terminologie**. Das sind Technologien, die wir selbst entwickeln und einsetzen — und als praktische Erfahrung in die Arbeitsgruppe einbringen, nicht nur als Designideen.

Sie können beides kostenlos ausprobieren. Erkunden Sie [SQL on FHIR in Aidbox](/aidbox) und nutzen Sie [Termbox](/termbox), um mit Terminologie und ValueSet-Expansionen zu arbeiten. Beginnen Sie mit Ihren eigenen Daten und einem ValueSet, das Sie tatsächlich verwenden; das ist ein besserer Test als jede Demo.

Die hier diskutierte First-Class-ValueSet-Schnittstelle ist noch ein Vorschlag. Die Tests ermöglichen es Ihnen, die vorhandenen Funktionen der beiden Produkte zu erkunden, nicht eine bereits ausgelieferte Version dieses Vorschlags.

---

Basierend auf den Diskussionen der SQL on FHIR Working Group vom 1., 8. und 15. September 2026 sowie meinem ValueSet-Abstraktionsentwurf. Meeting-Auszüge wurden zur besseren Lesbarkeit leicht überarbeitet.

Siehe auch: [SQL on FHIR WG-Meetings](/events/sql-on-fhir).