---
{
  "title": "SQL on FHIR: Analíticas interoperables",
  "description": "Cómo SQL on FHIR hace que las analíticas de salud sean interoperables: ViewDefinition, los nuevos perfiles SQLQuery y SQLView, la API run/export/materialize, y un pipeline ELT al estilo dbt para FHIR a OMOP.",
  "date": "2026-06-11",
  "author": "Nikolai Ryzhikov",
  "reading-time": "22 min read",
  "tags": [
    "SQL on FHIR",
    "Analytics",
    "FHIR Standard"
  ],
  "tldr": "SQL on FHIR comenzó como una especificación comunitaria para aplanar recursos FHIR. Hoy es un DSL completo para describir ELT y analíticas interoperables: ViewDefinition para la capa de staging, SQLQuery y SQLView para transformaciones en capas, y una API estándar run/export/materialize — con cada capa separando la autoría de la implementación y el uso. Próximos pasos: el núcleo de FHIR R6, donde ViewDefinition se convierte en un recurso estándar, y una guía de implementación abierta de FHIR a OMOP construida sobre este DSL.",
  "utm-campaign": "analytics",
  "utm-content": "interoperable-analytics"
}
---

> For the complete documentation index, see [llms.txt](https://www.health-samurai.io/llms.txt).
> Use it to discover all available pages before guessing URLs.

---
Existen dos mundos maduros que apenas se comunican entre sí.

Por un lado, FHIR. Por fin disponemos de una gran cantidad de datos FHIR — [la gran mayoría de los hospitales de EE. UU. exponen APIs FHIR](https://healthit.gov/data/data-briefs/hospital-use-apis-enable-data-sharing-between-ehrs-and-apps/) y la proporción crece año tras año, mientras que los sistemas FHIR-first almacenan datos clínicos de forma nativa en recursos. Por otro lado, existe un ecosistema muy maduro de herramientas analíticas: bases de datos modernas, plataformas de BI, dataframes y todo el stack de datos moderno.

Ya es posible cargar FHIR en estas herramientas — las bases de datos modernas gestionan bien los datos anidados: Postgres con JSON binario, BigQuery, Spark, DuckDB. De hecho, la primera versión de SQL on FHIR era exactamente eso: consultar FHIR anidado directamente en la base de datos. Funcionaba — y no llegaba a estandarizarse. Cada motor tiene su propio dialecto para datos anidados; una consulta escrita para BigQuery no tiene nada en común con la misma consulta en Postgres. No había nada que estandarizar.

En el fondo subyace el viejo problema de la **impedancia objeto-relacional**: los recursos FHIR son documentos anidados, y el mundo analítico — SQL, herramientas de BI, dataframes — sigue necesitando tablas planas. La solución ingenua de generar una tabla para cada elemento FHIR simplemente no funciona: se obtienen mil tablas que nadie puede comprender. Así que todo el mundo acaba escribiendo ETL específico para cada caso de uso con el fin de aplanar los recursos — repitiendo el mismo trabajo, con ligeras variaciones, y con los mismos errores sutiles en torno a arrays, tipos de elección y referencias.

La versión dos de [SQL on FHIR](https://build.fhir.org/ig/HL7/sql-on-fhir/) es nuestro intento de construir un puente estándar entre estos dos mundos — esta vez estandarizando las *vistas planas* en lugar de las consultas anidadas. Comenzó como un borrador comunitario; hoy es una especificación completa desarrollada de forma abierta, adoptada en el proceso de votación de HL7, publicada bajo CC0 — con un artículo revisado por pares en [npj Digital Medicine](https://www.nature.com/articles/s41746-025-01708-w) que valida el enfoque en ~300.000 pacientes y replica un estudio clínico publicado en dos pilas de implementación independientes. Y en FHIR R6, ViewDefinition está en camino de convertirse en un recurso FHIR estándar — un *additional resource*, el mecanismo de R6 para hacer crecer la especificación núcleo en módulos.

Lo que el estándar realmente aporta es separación: **la autoría se separa de la implementación, y la implementación del uso**. Una persona describe las analíticas; el motor de cualquiera las ejecuta; cualquier herramienta consume el resultado.

Y ya no son solo definiciones de vistas lo que se puede crear. Usted describe sus *analíticas completas* como artefactos portables y estándar — vistas planas, las capas de transformación sobre ellas, consultas, data marts completos y pipelines de conversión. Ejecútelos en distintos motores, sobre diferentes conjuntos de datos, contra servidores de distintos fabricantes — e incluso distribúyalos como una guía de implementación, como cualquier otra pieza del ecosistema FHIR. Eso es lo que significa «analíticas interoperables», y la especificación ahora lo ofrece en tres niveles: vistas, consultas y la API.

## ViewDefinition: un lenguaje para aplanar

Una ViewDefinition es, en términos simples, un lenguaje para explicarle a un servidor cómo aplanar recursos en una tabla plana. Describe una vista tabular de exactamente un tipo de recurso — columnas, filtros y desanidamiento — utilizando un [subconjunto mínimo de FHIRPath](https://build.fhir.org/ig/HL7/sql-on-fhir/StructureDefinition-ViewDefinition.html#fhirpath-functionality):

```json
{
  "resourceType": "ViewDefinition",
  "resource": "Patient",
  "name": "patient_demographics",
  "select": [
    {
      "column": [
        {"name": "patient_id", "path": "getResourceKey()"},
        {"name": "gender", "path": "gender"},
        {"name": "dob", "path": "birthDate"},
        {"name": "active", "path": "active", "type": "boolean"}
      ]
    },
    {
      "forEach": "name.where(use = 'official').first()",
      "column": [
        {"path": "given.join(' ')", "name": "given_name"},
        {"path": "family", "name": "family_name"}
      ]
    }
  ]
}
```

Como la definición es declarativa y neutral respecto al motor, la misma vista funciona en un runner ETL de JavaScript, dentro de PostgreSQL, en Spark o sobre un conjunto de archivos ndjson procedentes de un Bulk Export. El modelo completo se reduce a cinco funciones componibles, y un runner completo es lo suficientemente pequeño como para leerlo en una tarde — la [implementación de referencia en JavaScript](https://github.com/FHIR/sql-on-fhir.js) tiene aproximadamente 400 líneas:

| Función | Qué hace | Analogía SQL |
|----------|-------------|-------------|
| `column` | Extrae elementos mediante FHIRPath en columnas | `SELECT` |
| `where` | Filtra recursos (p. ej., por perfil o estado) | `WHERE` |
| `forEach` / `forEachOrNull` | Desanida una colección en filas | `INNER JOIN` / `LEFT OUTER JOIN` contra una tabla anidada |
| `select` | Combina columnas padre con filas desanidadas mediante producto cartesiano | join de subselects |
| `unionAll` | Concatena filas de distintas ramas | `UNION ALL` |

Una aclaración honesta: este aplanamiento es **con pérdida, y somos conscientes de ello**. No existe una representación tabular universal de FHIR, por lo que las vistas son específicas para cada caso de uso por diseño — a veces bromo diciendo que una ViewDefinition es el modo CSV de FHIR. Eso no es una debilidad; es el contrato: un ingeniero define la vista una vez, y otros ingenieros y herramientas simplemente utilizan la tabla plana.

Una unidad natural para una vista es un perfil. Imagine un perfil de presión arterial — y sobre él una bonita tabla `blood_pressure` con columnas `systolic` y `diastolic`, una fila por medición. El perfil determina dónde viven los datos en el recurso; la vista transforma ese conocimiento en columnas. Todo perfil bien definido es una tabla plana esperando ser declarada.

Dos adiciones recientes merecen especial atención:

- **`repeat`** gestiona estructuras verdaderamente recursivas — elementos de QuestionnaireResponse, conceptos de CodeSystem — donde no se conoce la profundidad de antemano. Indíquele las rutas a recorrer y cada elemento anidado se convierte en una fila, independientemente de su nivel.
- **`%rowIndex`** captura la posición de un elemento durante la iteración. Los conjuntos de resultados SQL no tienen orden, pero los arrays de FHIR sí — el primer `name` no es igual al tercero. El índice permite preservar el orden de FHIR y construir claves sustitutas.

### Joins sin joins

Una única ViewDefinition nunca une recursos — por diseño. En su lugar, dos funciones emiten claves sobre las que la base de datos realiza joins: `getResourceKey()` para la clave primaria de la fila, y `subject.getReferenceKey(Patient)` para la clave foránea (con un filtro de tipo que devuelve una colección vacía — un null en la salida — si la referencia apunta a otro lugar). Una vista de Condition incluye `patient_id`; unir condiciones con pacientes es un simple join SQL en cualquier motor que utilice.

La forma en que se derivan las claves — `id` simple, identificador primario, hash — queda a criterio de la implementación. Eso es deliberado: es lo que hace que la misma vista sea portable entre sistemas con diferentes invariantes de datos.

Y las cosas que ViewDefinition deliberadamente *no* hace — sin joins entre recursos, sin agregación, sin ordenación, sin formatos de salida — no son lagunas. Cada una de ellas fue una decisión que debatimos: ¿debemos complicar cada runner de vistas, o delegamos en la base de datos? Las bases de datos están especializadas en joins; nosotros no cruzamos el límite del recurso. Esa moderación es lo que hace que un runner sea implementable en cualquier lugar, desde una biblioteca de 400 líneas hasta un motor SQL distribuido.

Los runners en sí mismos se presentan en dos variantes. Los **runners ETL** toman un flujo de recursos y producen filas — triviales de escribir si se dispone de un motor FHIRPath (algunos implementadores han reportado haber escrito uno en Rust en aproximadamente un mes). Los **runners ELT** se parecen más a transpiladores que a runners: compilan una ViewDefinition en una sofisticada consulta SQL sobre una base de datos nativa FHIR, de modo que la vista se convierte en una vista de base de datos real y nada se duplica — cientos de definiciones de vistas pueden ejecutarse sobre la misma tabla de recursos.

Y la especificación se construyó de abajo hacia arriba, no de arriba hacia abajo: [las implementaciones existían *antes* del primer lanzamiento](https://sql-on-fhir.org/extra/impls.html). Una suite de pruebas compartida forma parte de la especificación — conjunto de datos, definición de vista, resultado esperado — y la matriz de conformidad son los propios implementadores ejecutando las mismas pruebas e informando de los resultados. Así es como garantizamos que las implementaciones permanezcan compatibles entre sí.

## SQLQuery y SQLView: las consultas se vuelven compartibles

Las tablas planas son la mitad del puente. La otra mitad: ¿dónde viven las *consultas*? La definición de cohorte, la medida de calidad, la consulta del dashboard — estos son los artefactos menos portables en las analíticas de salud actuales, dispersos entre wikis y notebooks, vinculados al esquema de un único sitio.

Así que emparejamos la definición de vista con un recurso de consulta, y ahora es posible compartir el conjunto completo. **SQLQuery** es un perfil sobre el recurso Library de FHIR que empaqueta una consulta SQL lógica:

```json
{
  "resourceType": "Library",
  "meta": {"profile": ["https://sql-on-fhir.org/ig/StructureDefinition/SQLQuery"]},
  "type": {"coding": [{"system": "https://sql-on-fhir.org/ig/CodeSystem/LibraryTypesCodes", "code": "sql-query"}]},
  "name": "DiagnosisByAgeSummary",
  "status": "active",
  "relatedArtifact": [
    {"type": "depends-on", "resource": "https://example.org/ViewDefinition/patient_demographics", "label": "pt"},
    {"type": "depends-on", "resource": "https://example.org/ViewDefinition/diagnoses_view", "label": "dg"}
  ],
  "parameter": [
    {"name": "from_date", "type": "date", "use": "in"}
  ],
  "content": [{
    "contentType": "application/sql",
    "extension": [{
      "url": "https://sql-on-fhir.org/ig/StructureDefinition/sql-text",
      "valueString": "SELECT pt.gender, dg.code, count(*) FROM pt JOIN dg USING (patient_id) WHERE dg.onset >= :from_date GROUP BY 1, 2"
    }],
    "data": "..."
  }]
}
```

La estructura se descompone en tres ideas:

- **Dependencias como alias.** Se declaran las definiciones de vistas de las que depende la consulta, y el `label` se convierte en el nombre de la tabla en el SQL. La consulta nunca codifica nombres físicos de tablas — el entorno de ejecución resuelve `pt` y `dg` hacia lo que sea que las vistas estén materializadas.
- **Parámetros seguros.** Los parámetros se declaran en la Library y se referencian como marcadores de posición `:from_date`. La especificación es contundente: NO DEBE utilizarse interpolación de cadenas — solo binding real.
- **Variantes de dialecto.** Una Library puede incluir un valor por defecto `application/sql` portable junto con adjuntos `;dialect=postgresql` o `;dialect=spark`, siempre que sean funcionalmente equivalentes.

También existe una conveniencia de autoría que la especificación describe para las herramientas: escribir un archivo `.sql` plano con algunos comentarios de anotación (`@name`, `@param`, `@relatedDependency`) y dejar que un constructor lo transforme en el recurso Library. El SQL permanece como fuente de verdad; FHIR se convierte en el empaquetado.

**SQLView** es el perfil más reciente, y existe por una razón: para poder apilar consultas unas sobre otras. Es casi idéntico a SQLQuery pero sin parámetros — y la intención es diferente. Una consulta es algo que se ejecuta; una vista describe una tabla. Cuando se dice «necesito una vista en una base de datos», la gente entiende de qué se habla — por eso es un perfil separado en lugar de un indicador sobre SQLQuery.

### Cómo se apilan los SQLViews

Un SQLView es una Library con `type = sql-view`, una URL canónica, dependencias y SQL. He aquí uno que define «pacientes activos» sobre la ViewDefinition `patient_demographics`:

```json
{
  "resourceType": "Library",
  "meta": {"profile": ["https://sql-on-fhir.org/ig/StructureDefinition/SQLView"]},
  "type": {"coding": [{"system": "https://sql-on-fhir.org/ig/CodeSystem/LibraryTypesCodes", "code": "sql-view"}]},
  "url": "https://example.org/Library/ActivePatientsView",
  "name": "ActivePatientsView",
  "status": "active",
  "relatedArtifact": [
    {"type": "depends-on", "resource": "https://example.org/ViewDefinition/patient_demographics", "label": "pt"}
  ],
  "content": [{
    "contentType": "application/sql",
    "extension": [{
      "url": "https://sql-on-fhir.org/ig/StructureDefinition/sql-text",
      "valueString": "SELECT patient_id, gender, dob FROM pt WHERE active = true"
    }],
    "data": "..."
  }]
}
```

La parte crucial es la `url`. Dado que la vista tiene una URL canónica, *cualquier cosa* puede depender de ella de la misma manera que dependería de una ViewDefinition — basta con añadir una entrada `relatedArtifact` y usar el `label` como nombre de tabla. Una segunda vista se construye sobre la primera:

```sql
-- SQLView: DiabeticPatientsView
-- depends on: .../Library/ActivePatientsView as ap
-- depends on: .../ViewDefinition/diagnoses_view as dg
SELECT ap.patient_id, ap.gender, ap.dob, dg.onset
  FROM ap
  JOIN dg USING (patient_id)
 WHERE dg.code = '44054006'   -- diabetes tipo 2 (SNOMED)
```

Y una SQLQuery parametrizada se sitúa sobre ambas capas para el informe final. Las reglas de dependencia son sencillas:

- Un **SQLView** puede depender de ViewDefinitions y otros SQLViews — nunca de SQLQueries.
- Un **SQLQuery** puede depender de ViewDefinitions y SQLViews.
- Las referencias forman un grafo dirigido, que debe mantenerse acíclico — como las vistas en cualquier base de datos.

Apile suficientes de estos y habrá descrito un sistema analítico completo — en los mismos términos que un equipo de datos utilizaría para cualquier almacén. Las ViewDefinitions son la **capa de staging**: FHIR en bruto cargado como tablas planas. Los SQLViews son los **modelos intermedios**: bloques de construcción limpios, filtrados y unidos. La parte superior del grafo es el **data mart**: los registros de cohortes, tablas de hechos e informes parametrizados que los analistas utilizan directamente.

```mermaid
flowchart LR
    subgraph staging ["Capa de staging — ViewDefinitions"]
        VD1[patient_demographics]
        VD2[diagnoses_view]
        VD3[observations_view]
    end
    subgraph views ["Modelos intermedios — SQLViews"]
        SV1[ActivePatientsView]
        SV2[DiabeticPatientsView]
        SV3[LatestHbA1cView]
        SV4[DiabetesRegisterView]
    end
    subgraph queries ["Data mart — SQLQueries"]
        Q1["RegisterByAgeReport(:from_date)"]
        Q2["PatientDrilldown(:patient_id)"]
    end
    VD1 --> SV1
    SV1 --> SV2
    VD2 --> SV2
    VD3 --> SV3
    SV2 --> SV4
    SV3 --> SV4
    SV4 --> Q1
    SV4 --> Q2
```

Cada nodo es pequeño, legible y comprobable por sí mismo: las vistas de staging solo remodelan, cada modelo intermedio añade un paso de transformación, las consultas solo parametrizan el corte final. La complejidad reside en la *composición*, no en ningún artefacto individual — que es exactamente cómo se construyen los sistemas analíticos maduros dentro de los almacenes hoy en día. No existe un límite aquí: un registro de enfermedades, una suite de medidas de calidad, un modelo dimensional con hechos y dimensiones, una conversión de cien tablas — cualquier cosa que pueda expresarse como SQL en capas sobre vistas planas puede expresarse como este grafo.

Dos ventajas adicionales surgen de forma natural. El grafo de dependencias *es* su linaje de datos: para cualquier columna del data mart puede trazar, artefacto por artefacto, el camino de vuelta hasta la expresión FHIRPath que la produjo. Y el grafo completo se distribuye como recursos FHIR.

Lo que la especificación deliberadamente *no* exige es la ejecución: si `ActivePatientsView` se convierte en un CTE en línea, una vista de base de datos o una tabla materializada es decisión del motor — el grafo describe la lógica, el motor elige la física.

### Un DSL completo para ELT

Dígale a un ingeniero de datos «tenemos un DAG aquí» — y lo entenderá de inmediato. El grafo que acaba de ver *es* ELT, el patrón dominante del stack de datos moderno: cargar primero los datos en bruto, transformarlos en capas dentro del motor. Si conoce dbt, ya reconoce esta forma. En el momento en que añadimos la capacidad de construir consultas sobre consultas, SQL on FHIR se convirtió en un DSL completo para describir pipelines ELT: ViewDefinition proporciona el **EL** — extrae FHIR, carga tablas planas — y SQLView con SQLQuery añaden la **T**. El ciclo queda cerrado.

¿Qué hay realmente de nuevo aquí, en comparación con las herramientas ELT que los equipos de datos ya utilizan? El modelo de distribución. Un proyecto de transformación es normalmente código en *su* repositorio, asumiendo *su* almacén. Un pipeline SQL on FHIR es un conjunto de recursos con URLs canónicas que puede publicarse en una guía de implementación, versionarse y ejecutarse contra cualquier stack conforme. Y dado que los artefactos son tecnológicamente neutros, la gente escribirá traductores que los conviertan en activos específicos del stack — una vista de almacén, un paso en su orquestador, un modelo en el framework de transformación que use su equipo. Describimos el data mart una vez; la tecnología de destino es un detalle de compilación.

Esto también completa la historia de la autoría. Tome el ejemplo de la presión arterial de antes — el perfil y su vista `systolic`/`diastolic` — y añada ahora un conjunto de consultas útiles sobre él: informes de hipertensión, dashboards de tendencias. Perfil, vista, consultas — un paquete listo para distribuir. Creemos que SQL on FHIR pasa a formar parte de la autoría en sí: una IG que define datos debería incluir las vistas y consultas para analizarlos.

## La API: clientes portables, sin dependencia del proveedor

La tercera pieza es una API HTTP estándar, y es importante por la misma razón que los artefactos: sin ella, sus *herramientas* quedan vinculadas a un proveedor aunque sus vistas no lo estén. Con ella, un dashboard, un pipeline, un notebook — cualquier cosa que hable la API — funciona contra cualquier servidor conforme. Cambie el backend, conserve el flujo de trabajo. Los clientes descubren lo que soporta un servidor a través del CapabilityStatement estándar.

La API tiene tres verbos. Cada uno responde a una pregunta diferente:

| Verbo | Pregunta que responde | Operaciones | Modo |
|------|--------------------|------------|------|
| **run** | «Dame las filas, ahora» | `$viewdefinition-run`, `$sqlview-run`\*, `$sqlquery-run` | Síncrono, en streaming |
| **export** | «Construye los archivos, avísame cuando esté listo» | `$viewdefinition-export`, `$sqlview-export`\*, `$sqlquery-export` | Asíncrono, hacia almacenamiento de archivos |
| **materialize** | «Mantén una tabla actualizada para mí» | `$materialize`\* | Asíncrono, gestionado por el servidor |

\* `$sqlview-run`, `$sqlview-export` y `$materialize` se están incorporando a la especificación — el grupo de trabajo los está estandarizando ahora.

Nótese la simetría: cada artefacto en el grafo de dependencias — ViewDefinition, SQLView, SQLQuery — recibe los mismos verbos. Puede ejecutar o exportar cualquier nodo de su pipeline, ya sea una vista de staging en bruto, un modelo intermedio o una consulta final parametrizada. Y materialize se aplica tanto a ViewDefinitions como a SQLViews — cualquier nodo sin parámetros puede convertirse en una tabla gestionada.

### run — para autoría y tiempo real

`$viewdefinition-run` — invocado como `$run` sobre el recurso ViewDefinition — es una operación síncrona diseñada para la autoría y casos de uso con conjuntos de datos pequeños. La llamada más sencilla es un GET sobre una vista almacenada:

```http
GET /ViewDefinition/patient-demographics/$run?_format=csv&_limit=100
Accept: text/csv
```

```
id,birthDate,family,given
pt-1,1990-01-15,Smith,John
pt-2,1985-03-22,Johnson,Mary
```

Cuando la vista aún no está guardada, se hace un POST en línea como cuerpo `Parameters` — opcionalmente junto con los recursos a transformar. Ese es el ciclo de autoría: edite la definición, haga POST, vea las filas, repita. También es el camino en tiempo real: un widget de dashboard o un agente de IA que quiere las condiciones de un paciente como tabla plana en lugar de un grafo de recursos. Las consideraciones en tiempo de ejecución permanecen en el tiempo de ejecución: `patient`, `group`, `_since` y `_limit` son parámetros de operación, no propiedades de la vista — la misma definición de vista sirve para la exportación de población completa y la consulta de un único paciente.

`$sqlquery-run` es el mismo verbo una capa más arriba: ejecutar una SQLQuery almacenada o en línea contra las vistas materializadas, pasando los parámetros de consulta por nombre (un recurso `Parameters` anidado, vinculado de forma segura a las entradas `Library.parameter` declaradas), con `_format` y `_limit` para el control de la salida. Y `$sqlview-run` (próximamente en la especificación) ocupa el lugar intermedio: evalúa cualquier SQLView intermedio — con todo su subgrafo de dependencias resuelto por el servidor — y devuelve las filas en streaming. Muy útil cuando se está depurando un nodo de un pipeline profundo.

### export — exportación masiva, mejorada

Piense en `$viewdefinition-export` como un [FHIR Bulk Export](https://hl7.org/fhir/uv/bulkdata/) mejorado. Con el Bulk Export clásico se obtienen *todos* los recursos, como ndjson en bruto — y el aplanamiento es su problema: hay que montar un pipeline ETL solo para hacer los datos consultables. Con la exportación SQL on FHIR se solicitan las vistas que realmente se necesitan, y lo que llega a su bucket ya está plano — csv, ndjson o parquet, listo para ser leído por Spark, DuckDB, Athena o cargado en un almacén.

El flujo tiene cuatro pasos:

1. **Inicio.** Se hace un POST con una lista de vistas con la cabecera asíncrona:

```http
POST /ViewDefinition/$viewdefinition-export HTTP/1.1
Prefer: respond-async
Content-Type: application/fhir+json

{
  "resourceType": "Parameters",
  "parameter": [
    {"name": "view", "part": [{"name": "viewReference",
      "valueReference": {"reference": "ViewDefinition/patient-demographics"}}]},
    {"name": "view", "part": [{"name": "viewReference",
      "valueReference": {"reference": "ViewDefinition/diagnoses"}}]},
    {"name": "_format", "valueCode": "parquet"}
  ]
}
```

2. **Obtención del ticket.** El servidor responde con `202 Accepted` y una cabecera `Content-Location` — su URL de estado.

3. **Polling.** La URL de estado devuelve `202` mientras el trabajo se ejecuta (con progreso opcional). Cuando el trabajo finaliza, responde con `303 See Other` con una cabecera `Location` que apunta al resultado.

4. **Recopilación de archivos.** Se hace GET a la URL de resultado — la respuesta lista una URL de salida por vista:

```json
{
  "resourceType": "Parameters",
  "parameter": [
    {"name": "exportId", "valueString": "job-42"},
    {"name": "status", "valueCode": "completed"},
    {"name": "output", "part": [
      {"name": "name", "valueString": "patient_demographics"},
      {"name": "location", "valueUri": "https://storage.example.org/exports/patient_demographics.parquet"}
    ]},
    {"name": "output", "part": [
      {"name": "name", "valueString": "diagnoses"},
      {"name": "location", "valueUri": "https://storage.example.org/exports/diagnoses.parquet"}
    ]}
  ]
}
```

Si ha implementado Bulk Data Export, ya conoce esta coreografía — la misma orquestación asíncrona, pero el resultado son tablas listas para análisis en lugar de recursos en bruto. Hay menos datos que exportar, se omite por completo el paso ETL, y un servidor que soporta la exportación de forma nativa puede optimizar considerablemente internamente.

Los mismos filtros se aplican que para run — `patient`, `group`, `_since` — de modo que un pagador puede exportar las vistas de un único miembro con la misma facilidad que las de toda una población. Y el verbo se extiende hacia arriba en el grafo: `$sqlview-export` (próximamente en la especificación) materializa un modelo intermedio en archivos, `$sqlquery-export` hace lo mismo para resultados de consultas demasiado grandes para una respuesta síncrona. Exporte la capa de staging para su lake, o exporte el data mart terminado — usted elige el punto de corte.

### materialize — «servidor, mantenlo actualizado»

`$materialize` es la operación mediante la cual se le indica al servidor: aquí está mi definición de vista — construye una vista gestionada a partir de ella y mantenla actualizada conforme cambien los datos. Es una operación asíncrona: se le proporciona un nombre de destino y una política de actualización (`manual`, o `scheduled` con una expresión cron), el servidor construye la vista en segundo plano, y cuando el trabajo finaliza se obtiene una referencia a la vista materializada que puede consultarse desde ese momento:

```http
POST /ViewDefinition/patient-demographics/$materialize HTTP/1.1
Prefer: respond-async
Content-Type: application/fhir+json

{
  "resourceType": "Parameters",
  "parameter": [
    {"name": "targetName", "valueString": "patient_demographics"},
    {"name": "updatePolicy", "valueCode": "scheduled"},
    {"name": "schedule", "valueString": "0 0 * * *"}
  ]
}
```

Cuando el trabajo finaliza se obtiene una referencia a la vista materializada; cómo se expone para su consulta — esquema, nomenclatura, acceso — queda a criterio de la implementación, con `targetName` como identificador solicitado. A partir de ese punto el servidor es responsable de la actualización (nocturna en este ejemplo), y cualquier herramienta SQL simplemente consulta el resultado:

```sql
SELECT * FROM patient_demographics WHERE dob > '1990-01-01';
```

Su herramienta de BI se conecta a una tabla y nunca sabe que FHIR estaba implicado.

El mismo verbo se aplica a los SQLViews. Materializar un modelo intermedio es exactamente lo que se hace en un almacén cuando una vista se vuelve muy demandada: el servidor resuelve el subgrafo de dependencias, construye la tabla y se ocupa de su refresco. Qué nodos de su DAG están materializados y cuáles permanecen virtuales se convierte en una decisión de ajuste en tiempo de ejecución — la definición del pipeline no cambia.

Este patrón ya está probado en producción. En Aidbox implementamos [$materialize sobre PostgreSQL](/articles/introducing-materialize-sql-interface-for-fhir-data?utm_source=blog&utm_medium=article&utm_campaign=analytics&utm_content=interoperable-analytics), más adaptadores para ClickHouse, BigQuery y Databricks: una carga inicial muy eficiente — millones de recursos en segundos — y luego la tabla se mantiene actualizada en tiempo casi real mediante suscripciones. La misma definición de vista, cuatro motores diferentes — que es precisamente la cuestión. La operación se está estandarizando ahora para que «mantener esta tabla actualizada» se convierta en una solicitud portable, no en una característica de un proveedor.

En conjunto: **run** para desarrollo y acceso en tiempo real, **export** para alimentar motores externos, **materialize** para analíticas dentro de la base de datos — todos endpoints estándar, todos neutrales respecto al proveedor.

Hacia dónde se dirige esto: desde la perspectiva del usuario, el servidor se convierte en una caja mágica. Se le envían definiciones de vistas y consultas; las vistas se mantienen actualizadas; simplemente se ejecutan los informes. Y dado que los LLMs ya son bastante buenos creando SQL y definiciones de vistas, la siguiente interfaz sobre esta caja es el lenguaje natural — con la API estándar como lo que el agente maneja por debajo.

## FHIR a OMOP: poniendo a prueba el DSL

Ese es el conjunto de herramientas completo: un DSL para staging (ViewDefinition), un DSL para transformaciones (SQLView/SQLQuery), y una API para ejecutarlo todo. La mejor manera de comprobar si un conjunto de herramientas es real es someterlo a la conversión más exigente que conocemos — y esa es FHIR a OMOP. Francamente, este proyecto ya *dictó* partes del diseño: necesitábamos vistas sobre vistas para expresarlo, y esa necesidad es en gran parte la razón por la que existe SQLView.

OMOP CDM es el estándar de OHDSI para la investigación observacional, y lo que me gusta de OMOP es que es extremadamente pragmático: un conjunto fijo de tablas, todos los códigos normalizados a conceptos estándar, toda la infraestructura — incluyendo la terminología — almacenada directamente en la base de datos. FHIR es el modelo transaccional; OMOP es el analítico. A medida que los sistemas FHIR-first se extienden, «FHIR para OLTP, OMOP para OLAP» se convierte en la arquitectura por defecto, y la capa de conversión entre ellos se convierte en infraestructura crítica.

La propia comunidad OMOP generalmente construye pipelines al estilo ELT. Nosotros también — la conversión es un DAG en capas de exactamente los artefactos descritos anteriormente:

```mermaid
flowchart LR
    A[Recursos FHIR] -->|"ViewDefinitions<br/>(capa de staging)"| B[condition_staging<br/>patient_staging<br/>observation_staging]
    B -->|"SQLViews<br/>(capa de mapeo<br/>+ vocabularios OMOP)"| C[condition_mapped<br/>person_mapped<br/>domain_routed]
    C -->|"SQLQueries<br/>(capa de carga)"| D[OMOP CDM<br/>condition_occurrence<br/>person, measurement]
    D --> E[Herramientas OHDSI<br/>Atlas, HADES]
```

Lo básico, capa por capa.

**Las ViewDefinitions de staging** aplanan cada recurso exactamente en lo que OMOP necesita — claves, fechas y codificaciones de origen, una fila por codificación:

```json
{
  "resourceType": "ViewDefinition",
  "name": "condition_staging",
  "resource": "Condition",
  "select": [
    {
      "column": [
        {"name": "condition_id", "path": "getResourceKey()", "type": "string"},
        {"name": "person_id", "path": "subject.getReferenceKey(Patient)", "type": "string"},
        {"name": "start_date", "path": "onset.ofType(dateTime)", "type": "dateTime"}
      ]
    },
    {
      "forEach": "code.coding",
      "column": [
        {"name": "source_system", "path": "system", "type": "uri"},
        {"name": "source_code", "path": "code", "type": "code"}
      ]
    }
  ]
}
```

**Los SQLViews de mapeo** contienen la semántica: los vocabularios Athena de OMOP se cargan directamente en la base de datos, y **la resolución de concept-id es un JOIN, no una llamada a un servidor de terminología**:

```sql
-- SQLView: ConditionMappedView
-- depends on: .../ViewDefinition/condition_staging as cs
SELECT cs.condition_id,
       cs.person_id,
       std.concept_id_2   AS condition_concept_id,
       cs.start_date,
       src.concept_id     AS condition_source_concept_id,
       cs.source_code     AS condition_source_value,
       std_c.domain_id    AS target_domain
  FROM cs
  JOIN concept src
    ON src.concept_code = cs.source_code AND src.vocabulary_id = 'ICD10CM'
  JOIN concept_relationship std
    ON std.concept_id_1 = src.concept_id AND std.relationship_id = 'Maps to'
  JOIN concept std_c
    ON std_c.concept_id = std.concept_id_2
```

Parece complicado, pero en realidad es bastante sencillo: realiza el mapeo, hace los joins y traduce al vuelo. Y los joins gestionan de forma natural las partes genuinamente difíciles — el fan-out de `Maps to` (un código ICD que se convierte en varias filas SNOMED), el enrutamiento de dominio (una Condition de FHIR que aterriza en `condition_occurrence`, `observation` o `measurement` según el dominio del concepto de destino), y las reglas de descarte para registros no mapeables.

**Las SQLQueries de carga** leen las vistas mapeadas y pueblan las tablas del CDM:

```sql
INSERT INTO condition_occurrence
SELECT condition_id, person_id, condition_concept_id,
       start_date, condition_source_concept_id, condition_source_value
  FROM cm
 WHERE target_domain = 'Condition'
```

¿Por qué SQL y no FHIRPath o el Lenguaje de Mapeo de FHIR? Porque para este trabajo se necesitan búsquedas en tablas de mapeo, división de un registro en muchos, lógica condicional entre vocabularios. En principio se podría enrutar cada código a través del `$translate` de un servidor de terminología — pero eso no es viable para la transformación masiva; la gente acabaría quemando CPUs procesándolo fila a fila. Y esto debe ser eficiente a escala poblacional — miles de millones de registros, no miles. Para nosotros esto es simplemente SQL, y los joins basados en conjuntos sobre miles de millones de filas es el problema en el que las bases de datos han pasado cincuenta años perfeccionándose.

Una aclaración completa: este proyecto — [fhir2omop](https://github.com/lampadephoros/fhir2omop) — se encuentra en una etapa muy temprana, un trabajo en curso desarrollado de forma abierta. Las ideas que exploramos: conversión con puerta de perfil (un recurso se convierte a una tabla OMOP *si y solo si* valida contra un perfil FHIR de control), casos de prueba dorados (un recurso FHIR como entrada, las filas OMOP exactas como salida — porque los casos extremos son exactamente lo que las muestras hacen visible), y módulos de jurisdicción como *US Core to OMOP* o *German ICD to OMOP* que la comunidad puede extender.

Pero el objetivo es más ambicioso que un solo convertidor. FHIR-a-OMOP es cómo **ponemos a prueba SQL on FHIR en condiciones reales**: es la conversión más exigente que existe, y si el DSL puede expresarla — el staging, los joins de vocabulario, el enrutamiento de dominio, todo ello como artefactos portables — entonces todo lo más sencillo viene de forma gratuita. Por eso invitamos a todos: personas del mundo OMOP, personas del mundo FHIR, ingenieros de datos. Traigan sus mapeos, sus casos extremos, su escepticismo — el grupo de trabajo está dedicando sesiones a OMOP, y la sala está abierta.

## Únase a construirlo con nosotros

Únase al [stream #analytics-on-FHIR](https://chat.fhir.org/#narrow/stream/179219-analytics-on-FHIR) en chat.fhir.org y a las llamadas semanales del grupo de trabajo. Pruebe el [playground](https://sql-on-fhir.org/extra/playground.html) para una experiencia de cinco minutos, o el [taller completo de DevDays](/articles/lets-build-sql-on-fhir-video-nikolai-ryzhikov-at-fhir-devdays-2025?utm_source=blog&utm_medium=article&utm_campaign=analytics&utm_content=interoperable-analytics) — PostgreSQL, Grafana, Jupyter, datos de Synthea — para un inicio práctico. Y anote en su calendario [FHIR Analytics](/events/fhir-analytics-2025?utm_source=blog&utm_medium=article&utm_campaign=analytics&utm_content=interoperable-analytics) — nuestra conferencia online gratuita dedicada exactamente a este ámbito.

Y si desea ejecutar todo esto hoy mismo: [Aidbox](/fhir-server?utm_source=blog&utm_medium=article&utm_campaign=analytics&utm_content=interoperable-analytics) es un servidor FHIR transaccional con analíticas en tiempo real integradas sobre SQL on FHIR — y el primer servidor FHIR en pasar todos los tests de SQL on FHIR. ViewDefinitions sobre PostgreSQL, un constructor visual de ViewDefinitions y gestores de vistas y consultas SQL en la interfaz de usuario, `$materialize`, y adaptadores que mantienen sus tablas actualizadas en ClickHouse, BigQuery y Databricks. El mundo transaccional y el analítico, finalmente sobre un mismo puente.

---

*Nada de esto es obra de una sola persona. Agradecimiento especial a John Grimes, Arjun Sanyal, Gino Canessa y Steve Munini — y a todo el grupo de trabajo de SQL on FHIR que acude semana tras semana a debatir estas ideas hasta convertirlas en un estándar.*