Was ist eine ViewDefinition, und wie funktioniert sie?
Die Arbeitsgruppe „SQL on FHIR v2" steht kurz vor ihrer ersten Veröffentlichung, die für Ende Sommer 2024 geplant ist. Die Spezifikation hat zum Ziel, eine Brücke zwischen FHIR-Daten und modernen Datenbank- sowie Analyse-Ökosystemen zu bauen. Die Kernidee besteht darin, einen standardisierten Weg einzuführen, um FHIR-Ressourcen in relationale Tabellen zu überführen. Wir sind überzeugt, dass eine flache Darstellung von Gesundheitsdaten Data Engineers und Analysetools effizienter macht.
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 möglich sind. ViewDefinitions sind CanonicalResources und können als Teil von Implementation Guides veröffentlicht werden. Zusammen mit Standard-ANSI-SQL-Abfragen können sie die Grundlage für interoperable Analysen und Berichte auf 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 einiger weniger Funktionen besteht.
-
column({name:column_name,path: fhirpath},...) – das Hauptwerkzeug der Transformation; diese Funktion extrahiert Elemente mithilfe von FHIRPath-Ausdrücken und speichert das Ergebnis in Spalten
-
where(fhirpath) – diese Funktion filtert Ressourcen anhand eines FHIRPath-Ausdrucks. Beispielsweise möchten Sie möglicherweise nur bestimmte Profile, wie Blutdruck, in eine einfache Tabelle überführen
-
forEach(expr, transform) – diese Funktion entschachtelt Kollektionselemente in separate Zeilen
-
select(rows1, rows2) – diese Funktion verbindet rows1 und rows2 per Cross-Join und wird hauptsächlich verwendet, um die Ergebnisse von forEach mit Spalten der obersten Ebene zu verknüpfen
-
union(rows, rows) – diese Funktion konkateniert Zeilenmengen. Der Hauptanwendungsfall ist das Zusammenführen von Zeilen aus verschiedenen Zweigen einer Ressource (zum Beispiel telecom und contact.telecom)
Nutzen Sie unseren kostenlosen Online-ViewDefinition Builder, um in JSON-Darstellung gespeicherte FHIR-Daten in ein tabellarisches, flaches Format für eine komfortable Datenanalyse zu konvertieren. 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 repräsentiert.
-
„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" konkateniert die Ergebnisse der beiden „forEach"-Operationen
-
Die „column"-Anweisung extrahiert id und ssn
-
Die „select"-Anweisung verbindet id und ssn per Cross-Join mit den Telecom-Telefonnummern
Hier ist ein Beispiel für Ein- 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 – ruft indirekt die Ressourcen-ID ab. Dies kann manchmal komplex sein, weshalb diese Indirektionsebene verwendet wird
-
getReferenceKey(resourceType) – eine ähnliche Funktion, die die ID aus einer Referenz abruft
Funktionen / Schlüsselwörter
Lassen Sie uns jede Funktion im Detail durchgehen.
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 bei, für die der FHIRPath-Ausdruck den Wert true zurückgibt.
{
"resourceType": "ViewDefinition",
"resource": "Patient",
"where": [
{"filter": "meta.profile.where($this = 'myprofile').exists()"},
{"filter": "active = 'true'"}
]
}
Grundlegende 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 Kollektionen, indem eine Transformation auf jedes Element angewendet wird. Sie besteht aus einem FHIRPath-Ausdruck für die zu iterierende Kollektion und einer Transformation, die auf jedes Element angewendet wird. Diese Funktion ist vergleichbar mit 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) per Cross-Join mit entschachtelten Kollektionselementen (wie Patient.name) zu verbinden. Diese Funktion führt Spalten aus jeder Zeilenmenge zusammen und erzeugt so eine umfassende Kombination der Daten aus den Eingabekollektionen.
{
"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 sie mehrere Datensatzmengen konkateniert. Diese Funktion fügt im Wesentlichen mehrere Kollektionen von Datensätzen zu einer einzigen, einheitlichen Kollektion zusammen 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 Konkatenation:
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. Beispielsweise können select, forEach und unionAll alle im selben JSON-Knoten vorhanden sein. Um solche Knoten zu interpretieren, müssen die Schlüsselwörter (Funktionen) gemäß ihrer Priorität neu geordnet werden, wobei Funktionen mit höherer Priorität nach oben wandern:
- 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 die ViewDefinition in eine SQL-Abfrage über eine FHIR-native Datenbank. In diesem Fall kann die Ansicht ein echter Datenbankview sein. In-Database-Engines können hinsichtlich Geschwindigkeit und Speicherressourcen deutlich effizienter als In-Memory-Engines sein, 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 eines Datenbankviews 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(' ')"
}
]
}
]
}
]
}
transpiliert zu:
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 einer Cloud-Sandbox ausführen – https://www.health-samurai.io/aidbox#run.
Sie können ViewDefinitions visuell erstellen und mit FHIRPath-Autovervollständigung debuggen, indem Sie unseren ViewDefinition Builder verwenden.
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 Anzeige von Informationen auf spezifische Benutzer- oder Systemanforderungen abstimmen müssen. Durch die Verwendung von ViewDefinition können Sie den Datenintegrationsprozess erheblich vereinfachen und beschleunigen und dabei 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. Sie ermöglicht es Ihnen, alle Funktionen ohne Einschränkungen zu testen und bietet damit 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 auf chat.fhir.org bei. Für persönliche Fragen können Sie sich gerne auf LinkedIn an mich wenden.





