Eine Frage: Welche Patientinnen und Patienten haben eine Diabetes-Diagnose?
Beginnen wir mit einer einfachen Aufgabe:
Geben Sie 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 die erste Eingabe, nicht jedoch die zweite.
Hier sind drei Diagnosezeilen und die zwei 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 sein. Der Diagnosecode für p3 ist in diesem ValueSet nicht 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 kleine ValueSet ist keine vollständige klinische Definition. Ressourcenauszüge bilden kein lauffähiges Paket, und das ValueSet-Interface ist noch ein Vorschlag.
Ich sehe hier zwei Phasen. Wenn wir FHIR-Ressourcen flachklopfen, 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 aber unterschiedliche Schnittstellen benötigen.
Für einige Messgrößen nehmen wir bereits ein expandiertes ValueSet, behandeln es als Tabelle und schreiben SQL dagegen. Die Frage ist, wie man daraus eine gemeinsame Schnittstelle macht, anstatt etwas, das jede Implementierung für sich neu erfindet.
John Grimes hat dies mit dem Problem verknüpft, das Terminologieserver bereits lösen:
„Dies ist das Problem, das der Terminologieserver für die Vorbereitung von Terminologiedaten für Laufzeitabfragen löst. Wir versuchen im Wesentlichen dasselbe für analytische Anwendungsfälle zu tun."
— John Grimes, 15. September (leicht redigiert)
Zuerst: Conditions in Zeilen umwandeln
Eine SQL on FHIR-ViewDefinition 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:
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 lauten und der Runner ihre Schlüssel als p1, p2 und p3 darstellt. Die tatsächliche Darstellung der Schlüssel ist runner-definiert; sie muss konsistent mit der entsprechenden Patient-Sicht 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.
Nun haben wir Patientendaten, die wir abfragen können. Wir müssen noch wissen, welche Codes als Diabetes gelten.
Beide Eingaben der SQL-Sicht deklarieren
Eine SQL-Sicht 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 über relatedArtifact auf eine ViewDefinition in der SQL-Sicht verweisen, ihr einen Tabellennamen geben und sie in einem Join verwenden. Wir können einen ähnlichen Ansatz für das ValueSet verwenden: Meine SQL-Sicht 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 wie dieser Library-Auszug 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 SQL base64-kodiert 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 meine ich mit First-Class ValueSets: Die Abfrage deklariert explizit die benötigte Terminologie neben ihren Datensichten. 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 Sicht des Runners:
„Sie sagen lediglich: Ich habe eine Abhängigkeit von diesem ValueSet. […] Für die SQL-Abstraktion möchten wir es sehr einfach halten, insbesondere für die einfachen Fälle."
— John Grimes, 15. September (leicht redigiert)
Mitgliedschaft, kein Expansionsalgorithmus
Die zweite Abhängigkeit liefert die zu Beginn gezeigte diabetes_codes-Mitgliedschaft.
Owen Loveluck fragte, ob die Abstraktion eher eine Sicht als eine Tabelle sein sollte. Diese Unterscheidung ist wichtig: Eine Relation muss keine physische Tabelle sein. Sie kann eine Sicht, gecachte Daten oder etwas sein, das bei Bedarf ausgewertet wird. Wie ich es am 8. September formulierte:
„Wir können das vorab expandierte ValueSet als Relation behandeln. Es ist uns egal, wie wir dorthin gelangt sind — ob es eine dynamische Expansionsabfrage ist oder vorab expandierte Daten, die in die Datenbank geladen wurden. Ich kann jedoch joinen und meine Messgröße aufbauen."
— Nikolai Ryzhikov, 8. September (leicht redigiert)
Der Vorschlag standardisiert nicht, wie ValueSet.compose, ECL oder VCL ausgewertet werden. Er beschreibt, wie die resultierende Mitgliedschaft SQL zur Verfügung gestellt wird.
Option A: Das ValueSet als Relation joinen
Der Runner stellt diabetes_codes als benannte Relation bereit. Unsere Abfrage joined dagegen:
# Abfragetext zur Illustration; `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 sein:
# 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 die beiden Optionen?
Beide Abfragen geben p1 und p2 zurück. Was sich unterscheidet, ist die Art, wie wir die Abfrage schreiben, und was die Schnittstelle sonst noch ermöglicht.
Dies ist gewöhnliches SQL. Die Mitgliedschaft ist sichtbar: Wir können die Codes einsehen, sie 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 daraus eine Spalte während der Extraktion und haben dann direkt in SQL Zugriff darauf. Die Funktionen erlauben das nicht so elegant."
— Gino Canessa, 15. September (leicht redigiert)
Der Nachteil ist ein wiederholter Multi-Spalten-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. Was besser performt, hängt von der Engine ab.
Der Kompromiss liegt in 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 keinen Anzeigetext zurück.
Wenn ein Runner beide Schnittstellen unterstützt, sollten sie für dieselben Eingaben und denselben Terminologie-Snapshot in der Mitgliedschaft übereinstimmen. 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 Mitgliedschaftsergebnisse machen eine Abfrage nicht für sich allein portabel.
Beide Optionen benötigen klare Versionsregeln
Im Beispiel gibt es zwei verschiedene Versionen: diabetes|2026 identifiziert die ValueSet-Definition, während jede Mitgliedschaftszeile eine CodeSystem-Version trägt. Allein das ValueSet zu pinnen, fixiert nicht notwendigerweise jede Terminologieabhängigkeit, die für dessen Expansion verwendet wurde.
Das ist mir wichtig, weil wir in Aidbox einmal die Validierung durch die Verwendung von „latest" gebrochen haben. Der Validator übernahm geänderte Encounter-Statuse 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 darin, dass sich die Patientenliste ändert, obwohl sich das SQL nicht geändert hat. Wenn zwei Runner eine nicht versionierte Abhängigkeit unterschiedlich auflösen, können sie unterschiedliche Antworten liefern. Owen hat genau diese Sorge im Meeting angesprochen.
Der Entwurf schlägt eine konsistente Mitgliedschaft für jeden Job vor, zeichnet aufgelöste Versionen auf und lehnt mehrdeutige Auflösungen ab. Wie nicht versionierte Abhängigkeiten behandelt werden sollen, ist jedoch noch offen. Unser Beispiel vermeidet diese Mehrdeutigkeit bewusst; eine Implementierung muss damit explizit umgehen und darf nicht stillschweigend raten.
Implementieren, experimentieren, dann entscheiden
Die Arbeitsgruppe tendiert derzeit zur Relation als Grundlage, mit der Funktion als optionaler Komfort. Das ist eine Richtung, keine Entscheidung. Beide Optionen haben nützliche Eigenschaften, und wir müssen sie an echten Abfragen ausprobieren.
John Grimes schlug einen praktischen nächsten Schritt vor:
„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 leicht sind sie zu implementieren und zu verwenden? Wir müssen außerdem 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 soll sagen können, ich hänge von diesem ValueSet ab. Das Experiment soll uns helfen zu entscheiden, wie Runner es SQL gegenüber bereitstellen und was jede Implementierung unterstützen muss.
Beteiligen Sie sich an der Diskussion zu Terminology in SQL on FHIR auf Zulip und verfolgen Sie die SQL on FHIR-Arbeitsgruppentreffen. Bringen Sie eine Abfrage mit, die Sie ausführen möchten, 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-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 Testversionen ermöglichen Ihnen, die vorhandenen Funktionen der beiden Produkte zu erkunden, nicht eine bereits ausgelieferte Version dieses Vorschlags.
Basierend auf den SQL on FHIR-Arbeitsgruppendiskussionen vom 1., 8. und 15. September 2026 sowie meinem ValueSet-Abstraktionsentwurf. Meetingauszüge wurden zur besseren Lesbarkeit leicht redigiert.
Siehe auch: SQL on FHIR WG-Treffen und FHIR-Analyse mit SQL on FHIR: interoperabel von Grund auf für das vollständige ViewDefinition-/SQLView-/SQLQuery-Bild, in das diese ValueSet-Arbeit eingebettet ist.


![Terminology is fun: CodeableConcept.coding[]](/assets/images/articles/horizontal-5.avif?v=2ef8.muw)

