|
11 min de lectura
|

Medidas de Calidad CMS como SQL on FHIR, sin motor CQL

Resumen del artículo

Las medidas de calidad 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. Hay un ejemplo completo y funcional (una docena de medidas CMS) en GitHub.

Resumir este artículo con:
ChatGPTPerplexityClaudeGrok

El problema de ejecutar eCQMs

Las Medidas de Calidad Clínica Electró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 CMS y HEDIS que reporta todo programa de atención basada en valor se elaboran en CQL (Clinical Quality Language), y la forma habitual de ejecutarlas es mediante un motor CQL dedicado: un entorno de ejecución independiente que analiza la medida, consulta al servidor FHIR para obtener terminología y datos del paciente, y devuelve un MeasureReport.

Esto funciona bien, pero tiene un coste:

  • Un nivel de cómputo separado. Un motor CQL es su propio entorno de ejecución — se despliega y escala junto al servidor FHIR, y realiza el cálculo de población fuera de la base de datos. Esto supone infraestructura real y un problema de escalado para poblaciones grandes.
  • Difícil de explicar. Cuando un paciente aparece como una brecha asistencial, ¿puede mostrar 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 este artículo: ¿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 algo 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 estándar y portables 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 completo y 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, cuidados de hospicio, fragilidad…).
  • Numerador — quién cumplió la medida (un cribado, una lectura controlada).
  • Puntuación — numerador / (denominador − exclusiones).

Cada uno de estos elementos es un conjunto de pacientes. SQL es muy bueno con conjuntos. Lo único que separa los datos FHIR de 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 sobre la que se pueda filtrar. SQL on FHIR resuelve ambos problemas.

ViewDefinition +$materialize flatten Aidbox — FHIR JSONB sof.* flat tablespatient, encounter, condition, observation… ValueSet expansions concepts table(valueset_url, system, code) Measure SQL (CTEs)IP → exclusions → numerator MeasureReport

Capa 1: aplanar FHIR en tablas con ViewDefinitions

Una 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 con columnas claras: patient_id, type_system, type_code, status, period_start, etc. — la lógica de la medida lee columnas sencillas en lugar de navegar por estructuras FHIR anidadas.

El ejemplo incluye una ViewDefinition por cada tipo de recurso que tocan 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. Esa capa plana compartida es reutilizada por todas las medidas.

Capa 2: terminología como un JOIN, no un $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 cualificada?». En una configuración con motor CQL, esto se resuelve habitualmente en tiempo de ejecución contra un servicio de terminología. Con SQL on FHIR se aplanan las expansiones de ValueSet una sola vez en una tabla concepts — una fila por (valueset_url, system, code) — y la pertenencia se convierte en un join ordinario:

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 ida y vuelta a la red, sin expansión por paciente. El value set se expande fuera de línea y se indexa, de modo que la comprobación «¿cuenta el código de este paciente?» es basada en conjuntos y rápida.

Capa 3: la medida es una sola consulta SQL

Con tablas planas y un join concepts disponible, toda una medida se convierte en una única consulta compuesta de CTEs — y se lee de forma notablemente parecida 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 46 a 75 años con un encuentro cualificado durante el período de medición:

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 agrupan hospicio/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 cualificó.

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 han factorizado 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 evidente: traducir manualmente el CQL de una medida a SQL parece un trabajo laborioso 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 modernos manejan bien. CQL y SQL son 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 así como se construyó el propio ejemplo. Cada medida se tradujo de su CQL publicado a SQL con un asistente de IA, y luego se verificó contra los fixtures de MeasureReport de referencia de CMS paciente por paciente hasta que los números coincidieron exactamente. La IA realiza la traducción mecánica; los fixtures la mantienen honesta.

Los fixtures de referencia son lo que hace que la traducción asistida por IA sea fiable: los resultados esperados de MeasureReport para los pacientes de prueba se publican junto con el contenido de la medida en el repositorio dqm-content-qicore-2025, por lo que cada medida traducida es comprobable contra la verdad de referencia — no se acepta por fe.

Haciéndolo conforme a los estándares: SQLQuery + Measure/$evaluate-measure

Ejecutar SQL está bien 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 y devuelve un MeasureReport adecuado — de modo que cualquier cliente FHIR lo consume de la misma manera que consumiría un resultado de un motor CQL.

Cada SQLQuery Library también declara una dependencia depends-on hacia las ViewDefinitions que lee, lo que 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 camino para el beneficio real.

POST /Measure/$evaluate-measure routes to $sqlquery-run reads builds Client Aidbox evaluate-measure app SQLQuery Library(the measure SQL) sof.* views + concepts MeasureReport→ back to the client

La aplicación evaluate-measure en el medio no contiene ninguna lógica de medida: resuelve la SQLQuery Library correcta, invoca $sqlquery-run y da forma a las filas devueltas en un MeasureReport. El cálculo en sí se ejecuta en la base de datos.

El beneficio: toda la suite de medidas es un paquete FHIR portable

Dado que cada parte de una medida es ahora un recurso FHIR estándar, la suite completa se empaqueta en un único 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 incluye 170 recursos a lo largo de 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 en el que se distribuyen las Guías de Implementación — 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 al arrancar el servidor, y las definiciones simplemente están ahí. Y aquí está la parte que importa: ese paquete no está vinculado a Aidbox. Son bloques de construcción SQL on FHIR estándar — ViewDefinitions y SQLQuery Libraries definidas por la especificación 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. La forma en que cada servidor las ejecuta es un detalle de implementación — algunos ejecutan un motor SQL on FHIR separado, otros ejecutan en la base de datos.

package install package install package install FHIR packageterminology + ViewDefinitions + SQLQuery Libraries FHIR server A(SQL on FHIR) FHIR server B(SQL on FHIR) …any SQL-on-FHIR server

Aidbox toma la ruta en 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. Así que el paquete se instala y ejecuta sin necesidad de levantar nada adicional para el cálculo en sí: ningún motor de ejecución que desplegar, escalar o mantener sincronizado. La lógica de la medida se distribuye de la misma manera que lo hace una Guía de Implementación — como artefactos FHIR estándar y compartibles.

Investigar un resultado

¿Recuerda el problema de «difícil de explicar»? Aquí obtiene una respuesta directa. Si solicita un único paciente, 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.

"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 resuelve a un recurso que existe en el servidor, por lo que el siguiente paso de la investigación es simplemente un GET.

Para la investigación a escala de población, 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, FOBT o 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 no tiene el cribado, y exactamente qué falta — como una sola consulta. La aplicación de demostración del ejemplo convierte estas consultas en elementos clicables: una lista de trabajo entre medidas, una vista 360 por paciente con detalle de evidencia, y listas de alcance exportables.

Patient 360 view in the demo app: the CMS130 decision chain with a per-CTE verdict for every population step, and the qualifying encounter shown as evidence
Vista 360 del paciente en la aplicación de demostración: una brecha de cribado colorrectal abierta, la cadena de decisión CMS130 completa (un veredicto por CTE), y el recurso de evidencia detrás de la pertenencia del paciente a la Población Inicial.

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

Ejecútelo usted mismo

Todo lo anterior es un ejemplo de código abierto y funcional en el repositorio de ejemplos de Aidbox — una docena de medidas 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

Siga las instrucciones del README para ejecutar toda la pila localmente: inicie Aidbox con el paquete de medidas instalado al arrancar, cargue el conjunto de datos de muestra, calcule las medidas a través de 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 cadena.

Demo app Overview: twelve CMS measure cards with scores, and a sidebar summarizing patients with gaps and open gaps
La pantalla de inicio de la aplicación de demostración: una docena de medidas CMS calculadas por SQL, con puntuaciones y recuentos de brechas abiertas en 530 pacientes de muestra.

Conclusión

CQL es un buen lenguaje de autoría para las medidas de calidad. Pero no tiene que ser su motor de ejecución. Cuando se aplana FHIR con ViewDefinitions, se convierte la terminología en un join y se expresa cada medida como una SQLQuery Library detrás de Measure/$evaluate-measure, la lógica de la medida deja de estar encerrada dentro de 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 — estaremos encantados de guiarle a través del proceso.

Compartir este artículo
Comments
Comments
Sign in
Loading comments...
Subscribe to our blog

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