¿Qué es un ViewDefinition y cómo funciona?
El grupo de trabajo «SQL on FHIR v2» está próximo a su primera publicación, prevista para finales del verano de 2024. La especificación tiene como objetivo construir un puente entre los datos FHIR y los ecosistemas modernos de bases de datos y analítica. La idea central es introducir una forma estandarizada de aplanar los recursos FHIR en tablas relacionales. Creemos que una representación plana de los datos de salud hará que los ingenieros de datos y las herramientas analíticas sean más eficientes.
Esta transformación de aplanamiento está definida por un tipo de recurso especial: ViewDefinition. Aunque no existen vistas planas universales para la mayoría de los recursos FHIR, creemos que podrían existir muchas vistas útiles específicas para cada caso de uso. Los ViewDefinitions son CanonicalResources y pueden publicarse como parte de Implementation Guides. Con consultas SQL ANSI estándar, pueden constituir la base para la analítica y los informes interoperables sobre FHIR. Esta entrada le ayudará a comprender cómo funciona ViewDefinition.
Un ViewDefinition es un algoritmo que describe la transformación de aplanamiento de los recursos FHIR, compuesto por combinaciones de unas pocas funciones.
-
column({name:column_name,path: fhirpath},...) - la función principal de la transformación; extrae elementos mediante expresiones FHIRPath y coloca el resultado en columnas
-
where(fhirpath) - esta función filtra recursos mediante una expresión FHIRPath. Por ejemplo, puede que desee transformar únicamente perfiles específicos, como la presión arterial, en una tabla sencilla
-
forEach(expr, transform) - esta función desanida los elementos de una colección en filas independientes
-
select(rows1, rows2) - esta función realiza un cruce de filas (cross-join) entre rows1 y rows2, y se utiliza principalmente para combinar los resultados de forEach con las columnas de nivel superior
-
union(rows, rows) - esta función concatena conjuntos de filas. El caso de uso principal es combinar filas de diferentes ramas de un recurso (por ejemplo, telecom y contact.telecom)
Utilice nuestro ViewDefinition Builder gratuito en línea para convertir datos FHIR almacenados en formato JSON en un formato tabular y plano que facilite el análisis de datos. Ir al ViewDefinition Builder
Un ViewDefinition se representa como un recurso FHIR (documento JSON) donde los elementos (palabras clave) corresponden a funciones:
{
"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"}]
}
]}
]
}
Esta vista produce una tabla de contactos de pacientes, donde cada fila representa una entrada de telecom.
-
«where» filtra únicamente los pacientes activos
-
«forEach» desanida Patient.telecom y selecciona los números de teléfono
-
«forEach» desanida Patient.contact.telecom y selecciona los números de teléfono
-
«unionAll» concatena los resultados de las dos operaciones «forEach»
-
«column» extrae el id y el ssn
-
«select» realiza un cruce entre id y ssn con los números de teléfono de telecom
A continuación se muestra un ejemplo de la entrada y la salida para este 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]
Resultado
| id | ssn | phone |
|---|---|---|
| pt1 | s1 | t11 |
| pt1 | s1 | t12 |
| pt1 | s1 | t13 |
| pt2 | s1 | t21 |
| pt2 | s1 | t22 |
| pt2 | s1 | t23 |
Subconjunto de FHIRPath
Los ViewDefinitions utilizan un subconjunto mínimo de FHIRPath para que la implementación sea lo más sencilla posible. Además, la especificación introduce algunas funciones especiales:
-
getResourceKey - obtiene indirectamente el ID del recurso. En ocasiones puede ser complejo, razón por la cual se utiliza esta capa de indirección
-
getReferenceKey(resourceType) - una función similar que obtiene el ID a partir de una referencia
Funciones / Palabras clave
Veamos cada función en detalle.
column
La función «column» extrae elementos en columnas mediante expresiones FHIRPath. El algoritmo comienza recibiendo una lista de pares {name, path}. Para cada registro del contexto dado, evalúa la expresión de ruta para extraer los elementos deseados. Los valores resultantes se añaden entonces como columnas a la fila de salida.
{
"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()"},
]
}
A continuación se muestra la implementación básica en JavaScript:
function column(cols, rows) {
return rows.map((row)=> {
return cols.reduce((res, col ) => {
res[col.name] = fhirpath(col.path, row)
return res
}, {})
})
}
where
La función «where» conserva únicamente aquellos registros para los que su expresión FHIRPath devuelve verdadero.
{
"resourceType": "ViewDefinition",
"resource": "Patient",
"where": [
{"filter": "meta.profile.where($this = 'myprofile').exists()"},
{"filter": "active = 'true'"}
]
}
Implementación básica en JavaScript:
function where(exprs, rows) {
return rows.filter((row)=> {
return exprs.every((expr)=>{
return fhirpath(expr, row) == true;
})
})
}
forEach y forEachOrNull
La función forEach está diseñada para aplanar colecciones anidadas aplicando una transformación a cada elemento. Consta de una expresión FHIRPath para la colección sobre la que iterar y una transformación que se aplica a cada elemento. Esta función es análoga a flatMap o mapcat en otros lenguajes de programación.
{
"resourceType": "ViewDefinition",
"resource": "Patient",
"select": [{
"forEach": "name",
"column": [
{"path": "given.join(' ')", "name": "first_name"},
{"path": "family", "name": "last_name"}
]
}]
}
Existen dos versiones de esta función: forEach y forEachOrNull. La diferencia principal es que forEach elimina los registros en los que la expresión FHIRPath no devuelve resultados, mientras que forEachOrNull conserva un registro vacío en tales casos.
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 función select se utiliza en combinación con forEach para realizar un cruce (cross-join) entre los elementos padre (como Patient.id) y los elementos de la colección desanidada (como Patient.name). Esta función fusiona las columnas de cada conjunto de filas, dando como resultado una combinación completa de los datos procedentes de las colecciones de entrada.
{
"resourceType": "ViewDefinition",
"resource": "Patient",
"select": [
{
"column": [
{"path": "getResourceKey()", "name": "id"}
]
},
{
"forEach": "name",
"column": [
{"path": "given.join(' ')", "name": "first_name"},
{"path": "family", "name": "last_name"}
]
}
]
}
La implementación básica es:
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 función unionAll combina filas de diferentes ramas de un árbol de recursos concatenando múltiples conjuntos de registros. Esta función esencialmente concatena varias colecciones de registros en una única colección unificada, preservando todas las filas de los conjuntos de entrada.
{
"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"}]
}
]}
]
}
La implementación es simplemente una concatenación:
function unionAll(rowSets){
return rowSet.flatMap((rows)=> { return rows})
}
unionAll([1,2,3], [3,4,5])
//=>
[1,2,3,3,4,5]
En un recurso, distintas palabras clave pueden aparecer al mismo nivel. Por ejemplo, select, forEach y unionAll pueden estar presentes en el mismo nodo JSON. Para interpretar dichos nodos, es necesario reordenar las palabras clave (funciones) según su precedencia, haciendo que las funciones con mayor precedencia suban:
- 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...
]
}
Consulte la implementación de referencia.
Motores de ViewDefinition
Un ViewDefinition puede ser ejecutado por un motor para producir vistas planas a partir de recursos FHIR. Existen dos categorías de motores:
-
Motores en memoria: Estos motores consumen recursos, los aplanan y envían los resultados a un flujo de datos, un archivo o una tabla. Puede imaginarse un pipeline ETL que transforma archivos NDJSON de exportación Bulk de FHIR en archivos Parquet.
-
Motores en base de datos: Estos motores traducen un ViewDefinition a una consulta SQL sobre una base de datos nativa de FHIR. En este caso, la vista puede ser una vista de base de datos real. Los motores en base de datos pueden ser mucho más eficientes que los motores en memoria en términos de velocidad y recursos de almacenamiento, pero son más complejos para los implementadores.
Existe una lista oficial de implementaciones disponible en https://fhir.github.io/sql-on-fhir-v2/#impls. La mayoría de las implementaciones son motores en memoria. Aidbox (PostgreSQL) y Pathling (Spark SQL) son motores en base de datos.
Aidbox (motor en base de datos)
Aidbox es un servidor FHIR y una base de datos para sistemas nativos de FHIR con soporte integrado para SQL on FHIR. Aidbox transpila un ViewDefinition a una consulta SQL de PostgreSQL, que puede ejecutarse «tal cual» o utilizarse para crear una vista de base de datos.
Por ejemplo, este ViewDefinition
{
"resource": "Patient",
"select": [
{
"column": [
{
"name": "id",
"path": "getResourceKey()"
}
]
},
{
"forEach": "name",
"select": [
{
"column": [
{
"name": "family",
"path": "family"
},
{
"name": "given",
"path": "given.join(' ')"
}
]
}
]
}
]
}
será transpilado a:
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
Puede ejecutar Aidbox localmente o en el Sandbox en la nube en cuestión de minutos: https://www.health-samurai.io/aidbox#run.
Puede construir y depurar ViewDefinition visualmente con autocompletado de FHIRPath utilizando nuestro ViewDefinition Builder.
ViewDefinition en FHIR es una herramienta potente que permite una gestión flexible de la representación de datos mediante la creación de vistas personalizables basadas en diversas condiciones y parámetros. Esto resulta especialmente útil cuando es necesario adaptar la visualización de la información para satisfacer requisitos específicos de usuarios o sistemas. Mediante el uso de ViewDefinition, puede simplificar y acelerar considerablemente el proceso de integración de datos, garantizando al mismo tiempo la integridad y la accesibilidad de los mismos.
Para explorar de forma práctica las capacidades de ViewDefinition, puede instalar la versión gratuita de Aidbox. Le permite probar todas las funcionalidades sin limitaciones, proporcionando un entorno ideal para el desarrollo y la experimentación.
Demostración de la implementación ELT para PostgreSQL utilizando Aidbox, el ViewDefinition Builder de código abierto y Grafana.
Únase al grupo de trabajo
Si desea formular preguntas o contribuir a SQL on FHIR, únase al chat en chat.fhir.org. Para consultas personales, no dude en contactar conmigo en LinkedIn.





