Die Arbeitsgruppe âSQL on FHIR v2" steht kurz vor ihrer ersten Veröffentlichung, die fĂŒr Ende Sommer 2024 geplant ist. Die Spezifikation zielt darauf ab, eine BrĂŒcke zwischen FHIR-Daten und modernen Datenbank- und Analyse-Ăkosystemen zu bauen. Der Kerngedanke besteht darin, einen standardisierten Weg einzufĂŒhren, FHIR-Ressourcen in relationale Tabellen zu ĂŒberfĂŒhren. Wir sind ĂŒberzeugt, dass eine flache Darstellung von Gesundheitsdaten Dateningenieure und Analysetools effizienter machen wird.
Diese Abflachungstransformation wird durch einen speziellen Ressourcentyp definiert: ViewDefinition. Obwohl es keine universellen flachen Ansichten fĂŒr die meisten FHIR-Ressourcen gibt, glauben wir, dass viele nĂŒtzliche, anwendungsfallspezifische Ansichten existieren könnten. ViewDefinitions sind CanonicalResources und können als Teil von Implementation Guides veröffentlicht werden. Mit Standard-ANSI-SQL-Abfragen können sie die Grundlage fĂŒr interoperable Analysen und Berichte auf Basis von FHIR bilden. Dieser Beitrag hilft Ihnen zu verstehen, wie ViewDefinition funktioniert.
Eine ViewDefinition ist ein Algorithmus, der die Abflachungstransformation von FHIR-Ressourcen beschreibt und aus Kombinationen weniger Funktionen besteht.
-
column({name:column_name,path: fhirpath},...)Â â das Arbeitspferd der Transformation; diese Funktion extrahiert Elemente mithilfe von FHIRPath-AusdrĂŒcken und legt das Ergebnis in Spalten ab
-
where(fhirpath) â diese Funktion filtert Ressourcen anhand eines FHIRPath-Ausdrucks. So können Sie beispielsweise nur bestimmte Profile, wie Blutdruck, in eine einfache Tabelle ĂŒberfĂŒhren
-
forEach(expr, transform)Â â diese Funktion entschachtelt Sammlungselemente in separate Zeilen
-
select(rows1, rows2) â diese Funktion fĂŒhrt einen Cross-Join von rows1 und rows2 durch und wird hauptsĂ€chlich verwendet, um die Ergebnisse von forEach mit Spalten der obersten Ebene zu verbinden
-
union(rows, rows)Â â diese Funktion verkettet Zeilenmengen. Der Hauptanwendungsfall ist die ZusammenfĂŒhrung von Zeilen aus verschiedenen Zweigen einer Ressource (zum Beispiel telecom und contact.telecom)
Nutzen Sie unseren kostenlosen ViewDefinition Builder, um in JSON-Darstellung gespeicherte FHIR-Daten in ein tabellarisches, flaches Format fĂŒr eine komfortable Datenanalyse umzuwandeln. Zum ViewDefinition Builder
Eine ViewDefinition wird als FHIR-Ressource (JSON-Dokument) dargestellt, bei der die Elemente (SchlĂŒsselwörter) den Funktionen entsprechen:
{
"resourceType": "ViewDefinition",
"resource": "Patient",
// (0)
"where": [{filter: "active = true"}],
// (5)
"select": [
{
// (4)
"column": [
{"path": "getResourceKey()", "name": "id"},
{"path": "identifier.where(system='ssn')", "name": "ssn"},
]
},
{
// (3)
"unionAll": [
{
// (1)
"forEach": "telecom.where(system='phone')",
"column": [{"path": "value", "name": "phone"}]
},
{
// (2)
"forEach": "contact.telecom.where(system='phone')",
"column": [{"path": "value", "name": "phone"}]
}
]}
]
}
Diese Ansicht erzeugt eine Tabelle mit Patientenkontakten, wobei jede Zeile einen Telecom-Eintrag darstellt.
-
âwhere" filtert nur aktive Patienten
-
âforEach" entschachtelt Patient.telecom und wĂ€hlt Telefonnummern aus
-
âforEach" entschachtelt Patient.contact.telecom und wĂ€hlt Telefonnummern aus
-
âunionAll" verkettet die Ergebnisse der beiden âforEach"-Operationen
-
Die âcolumn"-Anweisung extrahiert id und ssn
-
Die âselect"-Anweisung fĂŒhrt einen Cross-Join von id und ssn mit den Telecom-Telefonnummern durch
Nachfolgend ein Beispiel fĂŒr Eingabe und Ausgabe dieser ViewDefinition.
1[
2 {
3 "resourceType": "Patient",
4 "id": "pt1",
5 "identifier": [{"system": "ssn", "value": "s1"}],
6 "telecom": [{"system": "phone", "value": "tt1"}],
7 "contact": [
8 {"telecom": [{"system": "phone", "value": "t12"}]},
9 {"telecom": [{"system": "phone", "value": "t13"}]}
10 ]
11 },
12 {
13 "resourceType": "Patient",
14 "id": "pt2",
15 "identifier": [{"system": "ssn", "value": "s2"}],
16 "telecom": [{"system": "phone", "value": "t21"}],
17 "contact": [
18 {"telecom": [{"system": "phone", "value": "t22"}]},
19 {"telecom": [{"system": "phone", "value": "t23"}]}
20 ]
21 }
22]
Ergebnis
| id | ssn | phone |
|---|---|---|
| pt1 | s1 | t11 |
| pt1 | s1 | t12 |
| pt1 | s1 | t13 |
| pt2 | s1 | t21 |
| pt2 | s1 | t22 |
| pt2 | s1 | t23 |
FHIRPath-Teilmenge
ViewDefinitions verwenden eine minimale Teilmenge von FHIRPath, um die Implementierung so einfach wie möglich zu gestalten. DarĂŒber hinaus fĂŒhrt die Spezifikation einige spezielle Funktionen ein:
-
getResourceKey â ermittelt indirekt die Ressourcen-ID. Dies kann mitunter komplex sein, weshalb diese Indirektionsebene verwendet wird
-
getReferenceKey(resourceType) â eine Ă€hnliche Funktion, die die ID aus einer Referenz ermittelt
Funktionen / SchlĂŒsselwörter
Gehen wir jede Funktion im Detail durch.
column
Die Funktion column extrahiert Elemente mithilfe von FHIRPath-AusdrĂŒcken in Spalten. Der Algorithmus beginnt mit dem Empfang einer Liste von {name, path}-Paaren. FĂŒr jeden Datensatz im gegebenen Kontext wertet er den Pfadausdruck aus, um die gewĂŒnschten Elemente zu extrahieren. Die resultierenden Werte werden dann als Spalten zur Ausgabezeile hinzugefĂŒgt.
{
"column": [
{"name": "id", "path": "getResourceKey()"},
{"name": "bod", "path": "birthDate"},
{"name": "first_name", "path": "name.first().given.join(' ')"},
{"name": "last_name", "path": "name.first().family"},
{"name": "ssn", "path": "identifier.where(system='ssn').value.first()"},
{"name": "phone", "path": "telecom.where(system='phone').value.first()"},
]
}
Hier ist die naive JavaScript-Implementierung:
function column(cols, rows) {
return rows.map((row)=> {
return cols.reduce((res, col ) => {
res[col.name] = fhirpath(col.path, row)
return res
}, {})
})
}
where
Die Funktion where behĂ€lt nur jene DatensĂ€tze, fĂŒr die ihr FHIRPath-Ausdruck âtrue" zurĂŒckgibt.
{
"resourceType": "ViewDefinition",
"resource": "Patient",
"where": [
{"filter": "meta.profile.where($this = 'myprofile').exists()"},
{"filter": "active = 'true'"}
]
}
Einfache JavaScript-Implementierung:
function where(exprs, rows) {
return rows.filter((row)=> {
return exprs.every((expr)=>{
return fhirpath(expr, row) == true;
})
})
}
forEach & forEachOrNull
Die Funktion forEach dient zur Abflachung verschachtelter Sammlungen, indem eine Transformation auf jedes Element angewendet wird. Sie besteht aus einem FHIRPath-Ausdruck fĂŒr die zu iterierende Sammlung und einer Transformation, die auf jedes Element angewendet wird. Diese Funktion Ă€hnelt flatMap oder mapcat in anderen Programmiersprachen.
{
"resourceType": "ViewDefinition",
"resource": "Patient",
"select": [{
"forEach": "name",
"column": [
{"path": "given.join(' ')", "name": "first_name"},
{"path": "family", "name": "last_name"}
]
}]
}
Es gibt zwei Versionen dieser Funktion: forEach und forEachOrNull. Der wesentliche Unterschied besteht darin, dass forEach DatensÀtze entfernt, bei denen der FHIRPath-Ausdruck keine Ergebnisse liefert, wÀhrend forEachOrNull in solchen FÀllen einen leeren Datensatz beibehÀlt.
function forEach(path, expr, rows) {
return rows.flatMap((row)=> {
return fhirpath(expr, row).map((item)=>{
// evalKeyword will call column, select or other functions
return evalKeyword(expr, item)
})
})
}
select
Die Funktion select wird in Kombination mit forEach verwendet, um ĂŒbergeordnete Elemente (wie Patient.id) mit entschachtelten Sammlungselementen (wie Patient.name) per Cross-Join zu verbinden. Diese Funktion fĂŒhrt Spalten aus jeder Zeilenmenge zusammen und ergibt eine umfassende Kombination der Daten aus den Eingabesammlungen.
{
"resourceType": "ViewDefinition",
"resource": "Patient",
"select": [
{
"column": [
{"path": "getResourceKey()", "name": "id"}
]
},
{
"forEach": "name",
"column": [
{"path": "given.join(' ')", "name": "first_name"},
{"path": "family", "name": "last_name"}
]
}
]
}
Die naive Implementierung lautet:
function select(rows1, rows2){
return rows1.flatMap((r1)=> {
return rows2.map((r2)=>{
// merge r1 and r2
return { ...r1, ...r2 }
})
})
}
select([{a: 1}, {a: 2}], [{b: 1}, {b: 2}])
//=>
[{a: 1, b: 1},
{a: 1, b: 2},
{a: 2, b: 1},
{a: 2, b: 2}]
unionAll
Die Funktion unionAll kombiniert Zeilen aus verschiedenen Zweigen eines Ressourcenbaums, indem mehrere Datensatzmengen verkettet werden. Diese Funktion verkettet im Wesentlichen mehrere Sammlungen von DatensÀtzen zu einer einzigen, einheitlichen Sammlung und bewahrt dabei alle Zeilen aus den Eingabemengen.
{
"resourceType": "ViewDefinition",
"resource": "Patient",
"select": [
{
"column": [
{"path": "getResourceKey()", "name": "id"}
]
},
{
"unionAll": [
{
"forEach": "telecom.where(system='phone')",
"column": [{"path": "value", "name": "phone"}]
},
{
"forEach": "contact.telecom.where(system='phone')",
"column": [{"path": "value", "name": "phone"}]
}
]}
]
}
Die Implementierung ist eine einfache Verkettung:
function unionAll(rowSets){
return rowSet.flatMap((rows)=> { return rows})
}
unionAll([1,2,3], [3,4,5])
//=>
[1,2,3,3,4,5]
In einer Ressource können verschiedene SchlĂŒsselwörter auf derselben Ebene erscheinen. So können beispielsweise select, forEach und unionAll alle im selben JSON-Knoten vorhanden sein. Um solche Knoten zu interpretieren, mĂŒssen die SchlĂŒsselwörter (Funktionen) entsprechend ihrer PrioritĂ€t neu geordnet werden, wobei Funktionen mit höherer PrioritĂ€t nach oben steigen:
- forEach(OrNull)
- select
- unionAll
- column
{
"forEach": FOREACH,
"column": [COLUMNS], // got into select
"unionALL": [UNIONS], // got into select
"select": [SELECTS]
}
//=>
{
"forEach": FOREACH
"select": [
{"column": [COLUMNS]},
{"unionAll": [UNIONS]},
SELECTS...
]
}
Sehen Sie sich die Referenzimplementierung an.
ViewDefinition-Engines
Die ViewDefinition kann von einer Engine ausgefĂŒhrt werden, um flache Ansichten aus FHIR-Ressourcen zu erzeugen. Es gibt zwei Kategorien von Engines:
-
In-Memory-Engines: Diese Engines verarbeiten Ressourcen, flachen sie ab und geben die Ergebnisse in einen Stream, eine Datei oder eine Tabelle aus. Man kann sich eine ETL-Pipeline vorstellen, die FHIR Bulk-Export-NDJSON-Dateien in Parquet-Dateien umwandelt.
-
In-Database-Engines: Diese Engines ĂŒbersetzen ViewDefinition in eine SQL-Abfrage ĂŒber eine FHIR-native Datenbank. In diesem Fall kann die Ansicht eine echte Datenbankansicht sein. In-Database-Engines können hinsichtlich Geschwindigkeit und Speicherressourcen wesentlich effizienter sein als In-Memory-Engines, sind jedoch fĂŒr Implementierer komplexer.
Eine offizielle Liste der Implementierungen ist unter https://fhir.github.io/sql-on-fhir-v2/#impls verfĂŒgbar. Die meisten Implementierungen sind In-Memory-Engines. Aidbox (PostgreSQL) und Pathling (Spark SQL) sind In-Database-Engines.
Aidbox (In-Database-Engine)
Aidbox ist ein FHIR-Server und eine Datenbank fĂŒr FHIR-native Systeme mit integrierter UnterstĂŒtzung fĂŒr SQL on FHIR. Aidbox transpiliert eine ViewDefinition in eine PostgreSQL-SQL-Abfrage, die âas is" ausgefĂŒhrt oder zur Erstellung einer Datenbankansicht verwendet werden kann.
Zum Beispiel wird diese ViewDefinition
{
"resource": "Patient",
"select": [
{
"column": [
{
"name": "id",
"path": "getResourceKey()"
}
]
},
{
"forEach": "name",
"select": [
{
"column": [
{
"name": "family",
"path": "family"
},
{
"name": "given",
"path": "given.join(' ')"
}
]
}
]
}
]
}
in folgendes transpiliert:
SELECT
cast(id AS text) as "id",
cast(
jsonb_path_query_first(q1_1, '$ . family') #>> '{}' AS text
) as "family",
coalesce(
array_to_string(
(
SELECT
array_agg(x)
FROM jsonb_array_elements_text(jsonb_path_query_array(q1_1, '$ . given [*]')) as x
),
' '
),
''
) as "given"
FROM
"patient" as r
JOIN LATERAL jsonb_path_query(r.resource, '$ . name [*]') q1_1
ON true
LIMIT 100
Sie können Aidbox in wenigen Minuten lokal oder in der Cloud-Sandbox ausfĂŒhren â https://www.health-samurai.io/aidbox#run.
Sie können ViewDefinition visuell erstellen und mit FHIRPath-AutovervollstĂ€ndigung debuggen â mit unserem ViewDefinition Builder.
ViewDefinition in FHIR ist ein leistungsstarkes Werkzeug, das eine flexible Verwaltung der Datendarstellung ermöglicht, indem anpassbare Ansichten auf der Grundlage verschiedener Bedingungen und Parameter erstellt werden. Dies ist besonders nĂŒtzlich, wenn Sie die Darstellung von Informationen auf spezifische Benutzer- oder Systemanforderungen abstimmen mĂŒssen. Durch den Einsatz von ViewDefinition können Sie den Datenintegrationsprozess erheblich vereinfachen und beschleunigen und gleichzeitig die DatenintegritĂ€t und -zugĂ€nglichkeit sicherstellen.
Um die Möglichkeiten von ViewDefinition praktisch zu erkunden, können Sie die kostenlose Version von Aidbox installieren. Diese ermöglicht es Ihnen, alle Funktionen ohne EinschrĂ€nkungen zu testen und bietet eine ideale Umgebung fĂŒr Entwicklung und Experimente.
Demo der ELT-Implementierung fĂŒr PostgreSQL mit Aidbox, dem Open-Source-ViewDefinition Builder und Grafana.
Der Arbeitsgruppe beitreten
Wenn Sie Fragen stellen oder zu SQL on FHIR beitragen möchten, treten Sie uns im Chat unter chat.fhir.org bei. FĂŒr persönliche Fragen können Sie mich gerne auf LinkedIn kontaktieren.



