---
{
  "title": "Cálculo de medidas de calidad de CMS como SQL on FHIR — sin necesidad de un motor CQL",
  "description": "Ejecute eCQMs de CMS/HEDIS como SQL on FHIR — sin motor CQL. Las ViewDefinitions y las SQLQuery Libraries se empaquetan en un paquete FHIR que puede trasladarse a cualquier servidor SQL on FHIR, haciendo que el cálculo de medidas sea portátil y estándar.",
  "date": "2026-07-20",
  "author": "Aleksandr Kislitsyn",
  "reading-time": "11 min read",
  "tags": [
    "SQL on FHIR",
    "Analytics",
    "Compliance"
  ],
  "seo-tags": [
    "SQL on FHIR",
    "Quality Measures",
    "eCQM"
  ],
  "tldr": "Las medidas de calidad de CMS elaboradas en CQL pueden ejecutarse como SQL estándar en Aidbox/PostgreSQL mediante SQL on FHIR — sin motor CQL en tiempo de ejecución. Las ViewDefinitions aplanan FHIR en tablas, la pertenencia a un ValueSet se convierte en un JOIN, y cada medida es una consulta SQL empaquetada como una SQLQuery Library y servida a través de la API estándar Measure/$evaluate-measure. Un ejemplo completo y funcional (con una docena de medidas de CMS) está disponible en GitHub.",
  "utm-campaign": "analytics",
  "utm-content": "cms-measures-sql"
}
---

> 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.

---

## El problema de ejecutar eCQMs

Las medidas electrónicas de calidad clínica (eCQMs) son el mecanismo con el que los programas miden la calidad asistencial: si los pacientes elegibles recibieron su cribado colorrectal, si su hipertensión está controlada. Las medidas de CMS y HEDIS que reporta cada programa de atención basada en valor se elaboran en [CQL](https://cql.hl7.org/) (Clinical Quality Language), y la forma habitual de ejecutarlas es un **motor CQL** dedicado: un runtime independiente que analiza la medida, consulta su servidor FHIR para obtener terminología y datos de pacientes, y devuelve un `MeasureReport`.

Esto funciona bien, pero tiene un coste:

- **Un nivel de cómputo separado.** Un motor CQL es su propio runtime — hay que desplegarlo y escalarlo junto al servidor FHIR, y realiza el cálculo poblacional fuera de la base de datos. Se trata de una infraestructura real y un problema de escalado para poblaciones grandes.
- **Difícil de explicar.** Cuando un paciente aparece con una brecha asistencial, ¿puede mostrarle a un clínico exactamente *por qué*? Eso implica rastrear los internos del motor y las expansiones de value sets — no leer una consulta.

Esta es la pregunta que responde esta entrada: **¿y si la lógica de la medida fuera simplemente SQL, ejecutándose donde ya viven los datos?**

Resulta que puede serlo — y el resultado es más que un truco de rendimiento: con SQL on FHIR, todo el conocimiento sobre cómo calcular una medida se convierte en **artefactos FHIR portátiles y estándar** que pueden empaquetarse en un paquete FHIR e instalarse en otro servidor. Este es un recorrido técnico de cómo lograrlo, con un ejemplo completamente funcional que puede clonar y ejecutar.

## La idea: las medidas son lógica de conjuntos, y SQL es un lenguaje de conjuntos

Una medida de calidad es fundamentalmente aritmética de conjuntos sobre una población de pacientes:

- **Población inicial** — quién es elegible (edad, encuentros, una condición).
- **Denominador / Exclusiones** — quién se cuenta, menos quién queda excluido (cuidados paliativos, hospicio, fragilidad…).
- **Numerador** — quién cumplió la medida (un cribado, una lectura controlada).
- **Puntuación** — `numerador / (denominador − exclusiones)`.

Cada uno de esos elementos es un conjunto de pacientes. SQL trabaja muy bien con conjuntos. Lo único que se interpone entre los datos FHIR y una consulta SQL es que los recursos FHIR son JSON profundamente anidado, y la pertenencia a una terminología («¿está este código en el value set?») no es una columna por la que filtrar. [SQL on FHIR](https://www.health-samurai.io/docs/aidbox/modules/sql-on-fhir) resuelve ambos problemas.

```mermaid
flowchart TD
    A["Aidbox — FHIR JSONB"] -->|ViewDefinition +<br/>$materialize| B["sof.* flat tables<br/>patient, encounter, condition, observation…"]
    C["ValueSet expansions"] -->|flatten| D["concepts table<br/>(valueset_url, system, code)"]
    B --> E["Measure SQL (CTEs)<br/>IP → exclusions → numerator"]
    D --> E
    E --> F["MeasureReport"]
```

## Capa 1: aplanar FHIR en tablas con ViewDefinitions

Una [ViewDefinition](/blog/what-is-a-viewdefinition) es un recurso FHIR que describe cómo aplanar un tipo de recurso en una tabla plana. Apúntela a `Encounter`, materialícela, y obtendrá una tabla `sof.encounter_flat` bien organizada con `patient_id`, `type_system`, `type_code`, `status`, `period_start`, etc. — la lógica de la medida lee columnas simples en lugar de explorar estructuras FHIR anidadas.

El ejemplo incluye una ViewDefinition por tipo de recurso que utilizan las medidas — patient, encounter, condition, observation, procedure y algunos más — cada una materializada en el esquema `sof.*` y expuesta a través de una vista envolvente ligera. Esa capa plana compartida es reutilizada por cada medida.

## Capa 2: terminología como JOIN, no como $expand

La otra parte difícil de una medida es la pertenencia a conjuntos de códigos: «¿es este encuentro uno de los siete tipos de visita cualificante?». En un motor CQL esto se resuelve típicamente en tiempo de ejecución contra un servicio de terminología. Con SQL on FHIR se aplanan las expansiones de [ValueSet](https://www.health-samurai.io/docs/aidbox/terminology-module/fhir-terminology/valueset) **una sola vez** en una tabla `concepts` — una fila por `(valueset_url, system, code)` — y la pertenencia se convierte en un join ordinario:

```sql
JOIN concepts c
  ON  c.system = e.type_system
  AND c.code   = e.type_code
  AND c.valueset_url = 'http://cts.nlm.nih.gov/fhir/ValueSet/…'
```

Sin llamadas de red, sin expansión por paciente. El value set se expande de forma offline y se indexa, de modo que la comprobación «¿cuenta el código de este paciente?» está basada en conjuntos y es rápida.

## Capa 3: la medida es una sola consulta SQL

Con tablas planas y un join a `concepts` disponibles, una medida completa se convierte en una única consulta compuesta de CTEs — y su lectura se aproxima notablemente a la definición en lenguaje natural de la medida. Aquí está la Población Inicial de **CMS130 (Cribado de Cáncer Colorrectal)** — pacientes de entre 46 y 75 años con un encuentro cualificante durante el período de medición:

```sql
WITH mp AS (
  SELECT '2026-01-01'::timestamptz AS mp_start,
         '2026-12-31'::timestamptz AS mp_end
),

qualifying_encounters AS (
  SELECT DISTINCT e.patient_id
  FROM encounter_flat e
  JOIN concepts c
    ON  c.system = e.type_system
    AND c.code   = e.type_code
    AND c.valueset_url IN (
      -- ValueSet URLs shortened for readability; full URLs in the example repo
      'http://cts.nlm.nih.gov/fhir/ValueSet/…',  -- Office Visit
      'http://cts.nlm.nih.gov/fhir/ValueSet/…',  -- Annual Wellness Visit
      '…'                                        -- + 5 more
    )
  CROSS JOIN mp
  WHERE e.status = 'finished'
    AND e.period_start BETWEEN mp.mp_start AND mp.mp_end
),

initial_population AS (
  SELECT p.id AS patient_id
  FROM patient_flat p
  CROSS JOIN mp
  WHERE EXTRACT(YEAR FROM AGE(mp.mp_end, p.birth_date::date)) BETWEEN 46 AND 75
    AND p.id IN (SELECT patient_id FROM qualifying_encounters)
)
-- … denominator exclusions, numerator, and the final score follow as more CTEs
```

El numerador añade algunos CTEs más (colonoscopia en los últimos 9 años, FOBT durante el período, etc.), las exclusiones unen hospicio/cuidados paliativos/fragilidad, y un `SELECT` final calcula la puntuación. La clave es que **la medida es legible.** Puede leerla, compararla con la especificación y ejecutar `SELECT * FROM initial_population` para ver exactamente quién calificó.

### La lógica compartida permanece compartida

Exclusiones como hospicio, cuidados paliativos y enfermedad avanzada con fragilidad se repiten en muchas medidas. En el ejemplo se extraen en piezas reutilizables en lugar de copiarse y pegarse, de modo que corregir la lógica de hospicio la corrige en todos los lugares a la vez.

### ¿Pero quién escribe todo este SQL?

La objeción obvia: traducir manualmente el CQL de una medida a SQL suena como un trabajo minucioso y propenso a errores — y hay docenas de eCQMs. En la práctica, este es exactamente el tipo de tarea que los asistentes de codificación con IA manejan bien. CQL y SQL son ambos lenguajes estructurados y bien especificados, y la lógica de una medida se mapea limpiamente sobre el patrón de CTEs mostrado arriba (población inicial → exclusiones → numerador → puntuación).

Esto no es una esperanza — es cómo se construyó el propio ejemplo. Cada medida se tradujo de su CQL publicado a SQL con un asistente de IA, y después se **verificó contra los fixtures de `MeasureReport` de referencia de CMS paciente a paciente** hasta que los números coincidieron exactamente. La IA realiza la traducción mecánica; los fixtures la mantienen honesta.

{% hint style="info" %}
Los fixtures de referencia son los que hacen confiable la traducción asistida por IA: los resultados esperados de `MeasureReport` para los pacientes de prueba se publican junto al contenido de la medida en el repositorio [dqm-content-qicore-2025](https://github.com/cqframework/dqm-content-qicore-2025), por lo que cada medida traducida puede verificarse contra la verdad de referencia — no se acepta por fe.
{% endhint %}

## Hacerlo conforme a los estándares: SQLQuery + Measure/$evaluate-measure

Ejecutar SQL es correcto internamente, pero el objetivo es un servicio FHIR conforme, no un script de base de datos. Dos elementos de SQL on FHIR cierran esa brecha:

- El SQL de cada medida se almacena en Aidbox como un recurso **SQLQuery `Library`** (un perfil SQL on FHIR sobre `Library`) y se invoca con la operación `$sqlquery-run`. La lógica de cálculo vive *en el servidor FHIR*, como recursos de primera clase — no en el código de la aplicación.
- El servicio responde a la operación estándar FHIR R4 [`Measure/$evaluate-measure`](https://hl7.org/fhir/R4/operation-measure-evaluate-measure.html) y devuelve un `MeasureReport` apropiado — de modo que cualquier cliente FHIR lo consume de la misma manera que consumiría el resultado de un motor CQL.

Cada SQLQuery Library también declara linaje `depends-on` hacia las ViewDefinitions que lee, lo que le proporciona un grafo consultable: **medida → vistas → recursos.** Todo lo que necesita una medida — las vistas planas, la terminología, el cálculo — está ahora expresado como recursos FHIR estándar que viven en el servidor FHIR. Lo que prepara el terreno para el verdadero beneficio.

```mermaid
flowchart LR
    Client -->|"POST /Measure/<br/>$evaluate-measure"| Aidbox
    Aidbox -->|routes to| App["evaluate-measure app"]
    App -->|"$sqlquery-run"| Lib["SQLQuery Library<br/>(the measure SQL)"]
    Lib -->|reads| SOF["sof.* views + concepts"]
    App -->|builds| MR["MeasureReport<br/>→ back to the client"]
```

La aplicación evaluate-measure intermedia no contiene lógica de medida alguna: resuelve la SQLQuery Library correcta, invoca `$sqlquery-run` y da forma a las filas devueltas en un `MeasureReport`. El cómputo se ejecuta en la base de datos.

## El beneficio: toda la suite de medidas es un paquete FHIR portátil

Dado que cada parte de una medida es ahora un recurso FHIR estándar, toda la suite se empaqueta en **un solo paquete FHIR NPM**: terminología (CodeSystems + ValueSets), las ViewDefinitions que aplanan los datos y las SQLQuery Libraries que contienen la lógica de cálculo. En el ejemplo, ese paquete alberga 170 recursos distribuidos en una docena de medidas — 10 CodeSystems, 107 ValueSets, 10 ViewDefinitions y 43 SQLQuery Libraries.

El formato del paquete es estándar — el mismo formato FHIR NPM con el que se distribuyen las Implementation Guides — por lo que cualquier servidor FHIR puede cargarlo con su propio mecanismo de instalación de paquetes; en Aidbox eso es una llamada [`$fhir-package-install`](https://www.health-samurai.io/docs/aidbox/reference/package-registry-api#fhir-package-install) al arrancar el servidor, y las definiciones simplemente *están ahí*. Y aquí está la parte que importa: **ese paquete no está ligado a Aidbox.** Son bloques de construcción SQL on FHIR estándar — ViewDefinitions y SQLQuery Libraries definidas por la especificación [SQL on FHIR](https://build.fhir.org/ig/HL7/sql-on-fhir/). Traslade el paquete a cualquier servidor FHIR que implemente estos bloques de construcción, instálelo, y las mismas medidas se calculan de la misma manera. Cómo las ejecuta cada servidor es un detalle de implementación — algunos ejecutan un motor SQL on FHIR separado, otros lo ejecutan dentro de la base de datos.

```mermaid
flowchart LR
    Pkg["FHIR package<br/>terminology + ViewDefinitions + SQLQuery Libraries"]
    Pkg -->|"package install"| S1["FHIR server A<br/>(SQL on FHIR)"]
    Pkg -->|"package install"| S2["FHIR server B<br/>(SQL on FHIR)"]
    Pkg -->|"package install"| S3["…any SQL-on-FHIR server"]
```

Aidbox toma la vía dentro de la base de datos: las ViewDefinitions se materializan en tablas o vistas y las SQLQuery Libraries se ejecutan como SQL nativo, **directamente en su PostgreSQL** — justo donde ya viven los datos. De este modo, el paquete se instala y ejecuta sin nada adicional que desplegar para el cómputo en sí: no hay motor de ejecución que desplegar, escalar o mantener sincronizado. La lógica de la medida se distribuye de la misma manera que una Implementation Guide — como artefactos FHIR estándar y compartibles.

## Investigar un resultado

¿Recuerda el problema de «difícil de explicar»? Aquí recibe una respuesta directa. Al solicitar un paciente individual, el `MeasureReport` incluye un array `evaluatedResource`: referencias reales a recursos FHIR, cada una etiquetada con la población que satisface mediante la extensión estándar `cqf-criteriaReference`.

```json
"evaluatedResource": [
  {
    "reference": "Encounter/abc",
    "extension": [
      { "url": ".../cqf-criteriaReference", "valueString": "initial-population" },
      { "url": ".../cqf-criteriaReference", "valueString": "denominator" }
    ]
  },
  {
    "reference": "Procedure/xyz",
    "extension": [
      { "url": ".../cqf-criteriaReference", "valueString": "numerator" }
    ]
  }
]
```

«¿Por qué está este paciente en el numerador?» — *este* Procedure. «¿Por qué está excluido?» — *este* encuentro de hospicio. Cada referencia se resuelve a un recurso que existe en el servidor, de modo que el siguiente paso de la investigación es simplemente un `GET`.

Para la investigación a escala poblacional, cada medida también incluye una SQLQuery Library `-evidence`: una fila por paciente con la cadena de decisión completa — qué vía satisfizo el numerador (colonoscopia vs. FOBT vs. ninguna), el recurso desencadenante con su código y fecha, y qué exclusión se activó. Eso es una lista de trabajo de brechas asistenciales — *quién tiene pendiente el cribado y qué exactamente le falta* — como una sola consulta. La aplicación de demostración del ejemplo convierte estas consultas en elementos clicables: una lista de trabajo de medidas cruzadas, una vista de 360° por paciente con desglose de evidencia, y listas de alcance exportables.

![Vista de 360° del paciente en la aplicación de demostración: la cadena de decisión de CMS130 con un veredicto por CTE para cada paso de población, y el encuentro cualificante mostrado como evidencia](image-1.png "Patient 360 in the demo app: an open colorectal-screening gap, the full CMS130 decision chain (one verdict per CTE), and the evidence resource behind the patient's Initial Population membership.")

Y cuando un número sigue pareciendo incorrecto, cada población es un CTE con nombre: `SELECT * FROM initial_population` y reduzca el ámbito paso a paso — sin rastrear internos del motor.

## Pruébelo usted mismo

Todo lo anterior es un ejemplo funcional de código abierto en el repositorio de ejemplos de Aidbox — una docena de medidas de CMS (CMS130, CMS165, CMS125, CMS131 y más) con datos de pacientes de muestra y una aplicación de demostración interactiva.

**→ [github.com/Aidbox/examples · aidbox-custom-operations/measure-evaluate](https://github.com/Aidbox/examples/tree/main/aidbox-custom-operations/measure-evaluate)**

Siga las instrucciones del README para ejecutar la pila completa en local: inicie Aidbox con el paquete de medidas instalado al arrancar, cargue el conjunto de datos de muestra, calcule las medidas mediante la operación estándar `Measure/$evaluate-measure` y explore la evidencia de cada paciente en la interfaz de la aplicación de demostración. Lo que se devuelve es un `MeasureReport` FHIR estándar con los recuentos de población y la puntuación — calculado por SQL, dentro de Aidbox, sin ningún motor CQL en ningún punto de la pila.

![Visión general de la aplicación de demostración: doce tarjetas de medidas de CMS con puntuaciones y una barra lateral que resume los pacientes con brechas y las brechas abiertas](image-2.png "The demo app's Overview: a dozen CMS measures computed by SQL, with scores and open-gap counts across 530 sample patients.")

## Conclusión

CQL es un buen lenguaje de autoría para medidas de calidad. Pero no tiene que ser su motor de *ejecución*. Cuando aplana FHIR con ViewDefinitions, convierte la terminología en un join y expresa cada medida como una SQLQuery Library detrás de `Measure/$evaluate-measure`, la lógica de la medida deja de estar encerrada en un motor y se convierte en lo que SQL on FHIR promete: **artefactos FHIR estándar que puede empaquetar una vez y ejecutar en cualquier lugar.** Distribuya toda la suite como un paquete FHIR, instálelo en cualquier servidor SQL on FHIR, y las mismas medidas se calculan de la misma manera — explicables hasta el recurso individual. En Aidbox se ejecutan de forma nativa en PostgreSQL, por lo que no hay ningún motor de ejecución separado que operar. Y llegar hasta aquí es más accesible de lo que parece: los asistentes de IA traducen el CQL a SQL, y los propios fixtures de referencia de CMS verifican cada medida contra los resultados esperados.

> ¿Le interesa probar este enfoque de cálculo de medidas con sus propios datos? [Contáctenos](https://www.health-samurai.io/contacts?utm_source=article&utm_medium=blog&utm_campaign=cms-measures-sql) — estaremos encantados de guiarle.