Le groupe de travail « SQL on FHIR v2 » approche de sa premiÚre publication, prévue pour la fin de l'été 2024. La spécification vise à établir un pont entre les données FHIR et les écosystÚmes modernes de bases de données et d'analytique. L'idée centrale est d'introduire une façon normalisée d'aplatir les ressources FHIR en tables relationnelles. Nous croyons qu'une représentation aplatie des données de santé rendra les ingénieurs de données et les outils analytiques plus efficaces.
Cette transformation d'aplatissement est dĂ©finie par un type de ressource spĂ©cial : ViewDefinition. Bien qu'il n'existe pas de vues aplaties universelles pour la plupart des ressources FHIR, nous croyons que de nombreuses vues utiles et propres Ă des cas d'utilisation prĂ©cis pourraient exister. Les ViewDefinitions sont des CanonicalResources et peuvent ĂȘtre publiĂ©es dans le cadre de guides d'implĂ©mentation (IG). GrĂące Ă des requĂȘtes SQL ANSI standard, elles peuvent constituer la base d'une analytique interopĂ©rable et de rapports sur FHIR. Cet article vous aidera Ă comprendre le fonctionnement du ViewDefinition.
Un ViewDefinition est un algorithme qui décrit la transformation d'aplatissement des ressources FHIR, composé de combinaisons de quelques fonctions.
-
column({name:column_name,path: fhirpath},...) - le principal moteur de la transformation ; cette fonction extrait des éléments à l'aide d'expressions FHIRPath et place le résultat dans des colonnes
-
where(fhirpath) - cette fonction filtre les ressources par expression FHIRPath. Par exemple, vous pourriez vouloir transformer uniquement des profils spécifiques, comme la pression artérielle, en une table simple
-
forEach(expr, transform) - cette fonction déplie les éléments d'une collection en lignes distinctes
-
select(rows1, rows2) - cette fonction effectue une jointure croisée entre rows1 et rows2 ; elle sert principalement à joindre les résultats de forEach avec les colonnes de niveau supérieur
-
union(rows, rows) - cette fonction concatÚne des ensembles de lignes. Le principal cas d'utilisation est la combinaison de lignes provenant de différentes branches d'une ressource (par exemple, telecom et contact.telecom)
Utilisez notre Générateur de ViewDefinition en ligne gratuit pour convertir des données FHIR stockées en représentation JSON en un format tabulaire et aplati, pratique pour l'analyse de données. Accéder au Générateur de ViewDefinition
Un ViewDefinition est représenté comme une ressource FHIR (document JSON) dont les éléments (mots-clés) correspondent aux fonctions :
{
"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"}]
}
]}
]
}
Cette vue produit une table des contacts des patients, chaque ligne représentant une entrée de télécommunication.
-
« where » filtre uniquement les patients actifs
-
« forEach » déplie Patient.telecom et sélectionne les numéros de téléphone
-
« forEach » déplie Patient.contact.telecom et sélectionne les numéros de téléphone
-
« unionAll » concatÚne les résultats des deux opérations « forEach »
-
L'instruction « column » extrait l'identifiant et le NAS
-
L'instruction « select » effectue une jointure croisée entre l'identifiant et le NAS avec les numéros de téléphone des télécommunications
Voici un exemple d'entrée et de sortie pour ce 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]
Résultat
| id | ssn | phone |
|---|---|---|
| pt1 | s1 | t11 |
| pt1 | s1 | t12 |
| pt1 | s1 | t13 |
| pt2 | s1 | t21 |
| pt2 | s1 | t22 |
| pt2 | s1 | t23 |
Sous-ensemble FHIRPath
Les ViewDefinitions utilisent un sous-ensemble minimal de FHIRPath afin de simplifier au maximum l'implémentation. De plus, la spécification introduit quelques fonctions spéciales :
-
getResourceKey - obtient indirectement l'identifiant de la ressource. Cela peut parfois ĂȘtre complexe, d'oĂč l'utilisation de cette couche d'indirection
-
getReferenceKey(resourceType) - une fonction similaire qui obtient l'identifiant à partir d'une référence
Fonctions / Mots-clés
Passons en revue chaque fonction en détail.
column
La fonction « column » extrait des éléments dans des colonnes à l'aide d'expressions FHIRPath. L'algorithme commence par recevoir une liste de paires {name, path}. Pour chaque enregistrement dans le contexte donné, il évalue l'expression de chemin pour extraire les éléments souhaités. Les valeurs résultantes sont ensuite ajoutées sous forme de colonnes à la ligne de sortie.
{
"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()"},
]
}
Voici l'implémentation JavaScript naïve :
function column(cols, rows) {
return rows.map((row)=> {
return cols.reduce((res, col ) => {
res[col.name] = fhirpath(col.path, row)
return res
}, {})
})
}
where
La fonction « where » ne conserve que les enregistrements pour lesquels son expression FHIRPath retourne vrai.
{
"resourceType": "ViewDefinition",
"resource": "Patient",
"where": [
{"filter": "meta.profile.where($this = 'myprofile').exists()"},
{"filter": "active = 'true'"}
]
}
Implémentation JavaScript de base :
function where(exprs, rows) {
return rows.filter((row)=> {
return exprs.every((expr)=>{
return fhirpath(expr, row) == true;
})
})
}
forEach & forEachOrNull
La fonction forEach est destinée à aplatir les collections imbriquées en appliquant une transformation à chaque élément. Elle se compose d'une expression FHIRPath pour la collection à itérer et d'une transformation à appliquer à chaque élément. Cette fonction est analogue à flatMap ou mapcat dans d'autres langages de programmation.
{
"resourceType": "ViewDefinition",
"resource": "Patient",
"select": [{
"forEach": "name",
"column": [
{"path": "given.join(' ')", "name": "first_name"},
{"path": "family", "name": "last_name"}
]
}]
}
Il existe deux versions de cette fonction : forEach et forEachOrNull. La principale différence est que forEach supprime les enregistrements pour lesquels l'expression FHIRPath ne retourne aucun résultat, tandis que forEachOrNull conserve un enregistrement vide dans ces cas.
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
La fonction select est utilisée en combinaison avec forEach pour effectuer une jointure croisée entre des éléments parents (comme Patient.id) et des éléments de collection dépliés (comme Patient.name). Cette fonction fusionne les colonnes de chaque ensemble de lignes, produisant une combinaison complÚte des données des collections d'entrée.
{
"resourceType": "ViewDefinition",
"resource": "Patient",
"select": [
{
"column": [
{"path": "getResourceKey()", "name": "id"}
]
},
{
"forEach": "name",
"column": [
{"path": "given.join(' ')", "name": "first_name"},
{"path": "family", "name": "last_name"}
]
}
]
}
L'implémentation naïve est :
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
La fonction unionAll combine des lignes provenant de différentes branches d'un arbre de ressources en concaténant plusieurs ensembles d'enregistrements. Cette fonction concatÚne essentiellement plusieurs collections d'enregistrements en une seule collection unifiée, préservant toutes les lignes des ensembles d'entrée.
{
"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"}]
}
]}
]
}
L'implémentation est une simple concaténation :
function unionAll(rowSets){
return rowSet.flatMap((rows)=> { return rows})
}
unionAll([1,2,3], [3,4,5])
//=>
[1,2,3,3,4,5]
Dans une ressource, diffĂ©rents mots-clĂ©s peuvent apparaĂźtre au mĂȘme niveau. Par exemple, select, forEach et unionAll peuvent tous ĂȘtre prĂ©sents dans le mĂȘme nĆud JSON. Pour interprĂ©ter ces nĆuds, vous devez rĂ©ordonner les mots-clĂ©s (fonctions) selon leur prioritĂ©, les fonctions de prioritĂ© supĂ©rieure remontant en tĂȘte :
- 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...
]
}
Consultez l'implémentation de référence.
Moteurs ViewDefinition
Le ViewDefinition peut ĂȘtre exĂ©cutĂ© par un moteur pour produire des vues aplaties Ă partir de ressources FHIR. Il existe deux catĂ©gories de moteurs :
-
Moteurs en mémoire : Ces moteurs consomment des ressources, les aplatissent et produisent les résultats dans un flux, un fichier ou une table. On peut imaginer un pipeline ETL transformant des fichiers NDJSON exportés en vrac depuis FHIR (Bulk Data) en fichiers Parquet.
-
Moteurs en base de donnĂ©es : Ces moteurs traduisent le ViewDefinition en une requĂȘte SQL sur une base de donnĂ©es native FHIR. Dans ce cas, la vue peut ĂȘtre une vraie vue de base de donnĂ©es. Les moteurs en base de donnĂ©es peuvent ĂȘtre bien plus efficaces que les moteurs en mĂ©moire en termes de vitesse et de ressources de stockage, mais ils sont plus complexes Ă implĂ©menter.
Une liste officielle des implémentations est disponible à l'adresse https://fhir.github.io/sql-on-fhir-v2/#impls. La plupart des implémentations sont des moteurs en mémoire. Aidbox (PostgreSQL) et Pathling (Spark SQL) sont des moteurs en base de données.
Aidbox (moteur en base de données)
Aidbox est un serveur FHIR et une base de donnĂ©es pour les systĂšmes natifs FHIR, avec une prise en charge intĂ©grĂ©e de SQL on FHIR. Aidbox transpile un ViewDefinition en une requĂȘte SQL PostgreSQL, qui peut ĂȘtre exĂ©cutĂ©e « telle quelle » ou utilisĂ©e pour crĂ©er une vue de base de donnĂ©es.
Par exemple, ce ViewDefinition
{
"resource": "Patient",
"select": [
{
"column": [
{
"name": "id",
"path": "getResourceKey()"
}
]
},
{
"forEach": "name",
"select": [
{
"column": [
{
"name": "family",
"path": "family"
},
{
"name": "given",
"path": "given.join(' ')"
}
]
}
]
}
]
}
sera transpilé en :
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
Vous pouvez exécuter Aidbox localement ou dans un bac à sable infonuagique en quelques minutes - https://www.health-samurai.io/aidbox#run.
Vous pouvez créer et déboguer visuellement un ViewDefinition avec la complétion automatique FHIRPath grùce à notre Générateur de ViewDefinition.
Le ViewDefinition dans FHIR est un outil puissant qui permet une gestion flexible de la représentation des données en créant des vues personnalisables basées sur diverses conditions et paramÚtres. Cela est particuliÚrement utile lorsque vous devez adapter l'affichage des informations pour répondre à des exigences précises d'utilisateurs ou de systÚmes. En utilisant ViewDefinition, vous pouvez simplifier et accélérer considérablement le processus d'intégration des données tout en assurant l'intégrité et l'accessibilité des données.
Pour explorer concrÚtement les capacités de ViewDefinition, vous pouvez installer la version gratuite d'Aidbox. Elle vous permet de tester toutes les fonctionnalités sans limitations, offrant un environnement idéal pour le développement et l'expérimentation.
Démonstration de l'implémentation ELT pour PostgreSQL avec Aidbox, le Générateur de ViewDefinition libre et ouvert, et Grafana.
Rejoindre le groupe de travail
Si vous souhaitez poser des questions ou contribuer à SQL on FHIR, joignez-vous à nous dans la discussion sur chat.fhir.org. Pour des questions personnelles, n'hésitez pas à me contacter sur LinkedIn.



