|
6 Min. Lesezeit
|

FHIR ViewDefinition erklÀrt (SQL on FHIR Spec)

Diesen Artikel zusammenfassen mit:
ChatGPTPerplexityClaudeGrok

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

idssnphone
pt1s1t11
pt1s1t12
pt1s1t13
pt2s1t21
pt2s1t22
pt2s1t23

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.

Diesen Artikel teilen
Comments
Comments
Sign in
Loading comments...
Subscribe to our blog

Get the latest articles on FHIR, interoperability, and healthcare IT.