Eine Frage: Welche Patienten haben eine Diabetes-Diagnose?
Beginnen wir mit einer einfachen Aufgabe:
Geben Sie Patienten zurück, deren Diagnosecode zum Diabetes-ValueSet gehört.
Wir benötigen zwei Eingaben: die Diagnosecodes der Patienten und die im ValueSet enthaltenen Codes. Eine Tabelle mit Conditions liefert uns die erste Eingabe, aber nicht die zweite.
Hier sind drei Diagnosezeilen und die beiden Mitglieder unseres Beispiel-Diabetes-ValueSets:
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 lauten. Der Diagnosecode für p3 ist nicht in diesem ValueSet enthalten. Wir werden beide Eingaben aufbauen und zwei Wege ausprobieren, dieselbe Frage zu stellen.
Die Beispiele verwenden YAML zur besseren Lesbarkeit und illustrative Terminologieversionen. Dieses minimale ValueSet ist keine vollständige klinische Definition. Ressourcenauszüge bilden kein ausführbares Paket, und die ValueSet-Schnittstelle ist noch ein Vorschlag.
Ich sehe hier zwei Phasen. Wenn wir FHIR-Ressourcen abflachen, können wir Terminologie normalisieren — zum Beispiel Diagnosecodes mithilfe von FHIRPath-Funktionen übersetzen. Wenn wir später eine Kohorte erstellen, müssen wir Terminologie innerhalb einer SQL-Abfrage verwenden. Das sind verwandte Aufgaben, die jedoch unterschiedliche Schnittstellen benötigen.
Für einige Maßzahlen 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 dass jede Implementierung es selbst erfindet.
John Grimes hat dies mit dem Problem verbunden, das Terminologieserver bereits lösen:
„Das ist das Problem, das der Terminologieserver für die Vorbereitung von Terminologiedaten zur Laufzeit löst. Wir versuchen im Wesentlichen dasselbe für analytische Anwendungsfälle zu tun."
— John Grimes, 15. September (leicht redigiert)
Zunächst: Conditions in Zeilen umwandeln
Eine SQL on FHIR ViewDefinition beschreibt, wie mithilfe von FHIRPath Zeilen und Spalten aus FHIR-Ressourcen extrahiert werden. Sie enthält kein SQL.
Hier ist eine vereinfachte Ansicht über Condition:
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 Patientenidentifikator aus der Condition mit jedem Element in code.coding. Für diese Beispiele wird angenommen, dass Patientenreferenzen Patient/p1, Patient/p2 und Patient/p3 sind und der Runner ihre Schlüssel als p1, p2 und p3 darstellt. Die tatsächliche Darstellung der Schlüssel ist runner-definiert; sie muss mit der entsprechenden Patient-Ansicht konsistent sein. getReferenceKey(Patient) schränkt den Referenztyp auf Patient ein.
Wir verwenden versionierte Codings, um den Abgleich explizit zu machen; reale 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-Ansicht deklarieren
Eine SQL-Ansicht oder -Abfrage arbeitet auf den Zeilen, die von ViewDefinitions erzeugt werden. SQL on FHIR repräsentiert SQL-Abfragen mithilfe einer FHIR-Library-Ressource. Ihre relatedArtifact-Liste deklariert Abhängigkeiten: type: depends-on gibt an, 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-Ansicht über relatedArtifact auf eine ViewDefinition verweisen, ihr einen Tabellennamen geben und sie in einem Join verwenden. Für das ValueSet können wir einen ähnlichen Ansatz verwenden: Meine SQL-Ansicht hängt von einem ValueSet ab, und ich gebe ihm einen Namen."
— Nikolai Ryzhikov, 1. September (leicht redigiert)
Für unsere Diabetes-Abfrage sieht die vorgeschlagene Abhängigkeitsdeklaration in diesem Library-Auszug so aus:
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-Mitgliedschaft. 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 als base64 kodierte SQL enthalten. Die beiden folgenden Abfragen sind alternative Inhalte dieser Datei. Wir zeigen sie als lesbare YAML-sql: |-Blöcke, nicht als zusätzliche Library-Felder.
Das ist, was ich mit first-class ValueSets meine: Die Abfrage deklariert explizit die benötigte Terminologie — neben ihren Datenansichten. Der Runner — die Software, die die Abfrage ausführt — löst die Abhängigkeiten auf. SQL verwendet ihre lokalen Namen, anstatt URLs selbst aufzulösen.
John beschrieb dieselbe Trennung aus der Perspektive des Runners:
„Sie sagen nur: Ich habe eine Abhängigkeit von diesem ValueSet. […] Für die SQL-Abstraktion wollen wir es so einfach wie möglich halten, insbesondere für die einfachen Fälle."
— John Grimes, 15. September (leicht redigiert)
Mitgliedschaft, kein Erweiterungsalgorithmus
Die zweite Abhängigkeit liefert die zu Beginn gezeigte diabetes_codes-Mitgliedschaft.
Owen Loveluck fragte, ob die Abstraktion eher eine Ansicht als eine Tabelle sein sollte. Diese Unterscheidung ist wichtig: Eine Relation muss keine physische Tabelle sein. Sie kann eine Ansicht, gecachte Daten oder etwas sein, das bei Bedarf ausgewertet wird. Wie ich es am 8. September formulierte:
„Wir können das vorab erweiterte ValueSet als Relation behandeln. Es spielt keine Rolle, wie wir dorthin gekommen sind — ob es eine dynamische Erweiterungsabfrage ist oder vorab erweitertes Material, das in die Datenbank geladen wurde. Ich kann es joinen und meine Maßzahl erstellen."
— Nikolai Ryzhikov, 8. September (leicht redigiert)
Der Vorschlag standardisiert nicht, wie ValueSet.compose, ECL oder VCL ausgewertet werden. Er beschreibt, wie die resultierende Mitgliedschaft für SQL verfügbar gemacht wird.
Option A: Das ValueSet als Relation joinen
Der Runner stellt diabetes_codes als benannte Relation bereit. Unsere Abfrage joined dagegen:
# Abfragetext zur Veranschaulichung; `sql` ist kein FHIR-Library-Feld.
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: Mitgliedschaft mit einer Funktion prüfen
Dieselbe Abhängigkeit könnte stattdessen über member_of verfügbar gemacht werden:
# Alternativer Abfragetext unter Verwendung der vorgeschlagenen Funktion.
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 sich zwischen den beiden Optionen?
Beide Abfragen geben p1 und p2 zurück. Der Unterschied liegt darin, wie wir die Abfrage schreiben und was die Schnittstelle darüber hinaus ermöglicht.
Option A ist gewöhnliches SQL. Die Mitgliedschaft ist sichtbar: Wir können die Codes einsehen, sie zählen oder display in unsere Ausgabe aufnehmen. 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 besprach, die nicht auswählbar sind:
„Wenn not-selectable eine Eigenschaft ist, die Sie interessiert, machen Sie daraus während der Extraktion eine Spalte — dann haben Sie in SQL direkten Zugriff darauf. Die Funktionen erlauben das nicht so schön."
— Gino Canessa, 15. September (leicht redigiert)
Der Nachteil ist ein wiederholter mehrspaltige Join. Der Entwurf benötigt außerdem eine klare Eindeutigkeitsregel für (system, version, code): Doppelte Mitgliedschaftszeilen 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 statt eines Joins verwenden. Welche Option besser abschneidet, hängt von der Engine ab.
Der Kompromiss liegt bei der 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 Mitgliedschaftsfrage, legt die Mitglieder aber nicht offen und gibt auch keinen Anzeigetext zurück.
Wenn ein Runner beide Schnittstellen unterstützt, sollten sie für dieselben Eingaben und denselben Terminologie-Snapshot dieselbe Mitgliedschaft liefern. Unsere beiden Abfragen sollten dieselben Patienten zurückgeben.
Ein Runner muss außerdem angeben, ob er die Relation, die Funktion oder beides unterstützt. Übereinstimmende Mitgliedschaftsergebnisse machen eine Abfrage nicht automatisch portabel.
Beide Optionen benötigen klare Versionsregeln
Im Beispiel gibt es zwei unterschiedliche Versionen: diabetes|2026 identifiziert die ValueSet-Definition, während jede Mitgliedschaftszeile eine CodeSystem-Version trägt. Allein das Festlegen der ValueSet-Version fixiert nicht zwingend jede zur Erweiterung verwendete Terminologieabhängigkeit.
Das ist mir wichtig, weil wir in Aidbox einmal die Validierung durch die Verwendung von „latest" gebrochen haben. Der Validator übernahm geänderte Encounter-Statuses aus R5, und die Validierung schlug auf produktiven R4-Servern fehl. Seitdem pinnen wir so viel wie möglich und aktualisieren die Pins bewusst.
Für unsere Abfrage besteht das Risiko in einer geänderten Patientenliste, obwohl sich das SQL nicht geändert hat. Wenn zwei Runner eine unversionierte Abhängigkeit unterschiedlich auflösen, können sie verschiedene Antworten liefern. Owen hat genau diese Sorge in der Besprechung geäußert.
Der Entwurf schlägt eine konsistente Mitgliedschaft pro Auftrag vor, die aufgelösten Versionen aufzeichnet und eine mehrdeutige Auflösung ablehnt. Wie unversionierte Abhängigkeiten behandelt werden sollen, ist jedoch noch offen. Unser Beispiel vermeidet diese Mehrdeutigkeit bewusst; eine Implementierung muss damit explizit umgehen, anstatt stillschweigend zu raten.
Implementieren, erproben, dann entscheiden
Die Arbeitsgruppe tendiert derzeit zur Relation als Grundlage, mit der Funktion als optionaler Erleichterung. Das ist eine Richtung, keine Entscheidung. Beide Optionen haben nützliche Eigenschaften, und wir müssen sie an realen Abfragen ausprobieren.
John Grimes schlug einen praktischen nächsten Schritt vor:
„Ich denke, wir sollten einen Branch erstellen und beginnen, 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 Patienten zurück, wie gehen sie mit Versionen um, und wie einfach sind sie zu implementieren und zu verwenden? Wir müssen außerdem zusätzliche Eigenschaften testen und die Leistung messen. Dann können wir entscheiden, was jeder Runner unterstützen muss.
Das unmittelbare Ziel ist einfach: Eine Abfrage soll angeben können, ich hänge von diesem ValueSet ab. Das Experiment soll uns helfen zu entscheiden, wie Runner es für SQL bereitstellen und was jede Implementierung unterstützen muss.
Beteiligen Sie sich an der Diskussion zu Terminology in SQL on FHIR auf Zulip und folgen Sie den SQL on FHIR-Arbeitsgruppentreffen. Bringen Sie eine Abfrage mit, die Sie ausführen müssen, ein ValueSet, das schwer 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 und nutzen Sie Termbox, um mit Terminologie und ValueSet-Erweiterungen 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 Versuche ermöglichen es Ihnen, die vorhandenen Funktionen der beiden Produkte zu erkunden, nicht eine bereits veröffentlichte Version dieses Vorschlags.
Basierend auf den SQL on FHIR-Arbeitsgruppendiskussionen vom 1., 8. und 15. September 2026 sowie meinem ValueSet-Abstraktionsentwurf. Besprechungsauszüge wurden zur besseren Lesbarkeit leicht redigiert.
Siehe auch: SQL on FHIR WG-Treffen und interoperable Analysen mit SQL on FHIR für das vollständige Bild aus ViewDefinition, SQLView und SQLQuery, in das diese ValueSet-Arbeit eingebettet ist.


![Terminology is fun: CodeableConcept.coding[]](/assets/images/articles/horizontal-4.avif?v=3egb.mw5)

