Le problème de l'exécution des eCQM
Les mesures cliniques électroniques de qualité (eCQM) permettent aux programmes de mesurer la qualité des soins — par exemple, si les patients admissibles ont subi leur dépistage colorectal ou si leur hypertension est contrôlée. Les mesures CMS et HEDIS que chaque programme de soins basé sur la valeur doit déclarer sont rédigées en CQL (Clinical Quality Language), et la méthode habituelle pour les exécuter est un moteur CQL dédié : une couche d'exécution distincte qui analyse la mesure, interroge votre serveur FHIR pour les données terminologiques et les données patients, puis retourne un MeasureReport.
Cette approche fonctionne bien, mais à un coût :
- Une couche de calcul distincte. Un moteur CQL est son propre environnement d'exécution — vous le déployez et le faites évoluer parallèlement à votre serveur FHIR, et il effectue le calcul des populations en dehors de la base de données. C'est une infrastructure réelle, et un enjeu de mise à l'échelle pour de grandes populations.
- Difficile à expliquer. Lorsqu'un patient apparaît comme un écart de soins, pouvez-vous montrer à un clinicien exactement pourquoi? Cela implique de retracer les mécanismes internes du moteur et les expansions de jeux de valeurs — pas de lire une requête.
Voici donc la question à laquelle ce billet répond : et si la logique de la mesure n'était que du SQL, s'exécutant là où vos données résident déjà?
Il s'avère que c'est possible — et le résultat va bien au-delà d'une simple optimisation des performances : avec SQL on FHIR, l'ensemble du savoir nécessaire au calcul d'une mesure devient des artefacts FHIR portables et standardisés que vous pouvez empaqueter dans un paquet FHIR et installer sur un autre serveur. Voici une présentation technique de la démarche, avec un exemple complet et fonctionnel que vous pouvez cloner et exécuter.
L'idée : les mesures sont une logique d'ensembles, et SQL est un langage d'ensembles
Une mesure de qualité est fondamentalement de l'arithmétique d'ensembles sur une population de patients :
- Population initiale — qui est admissible (âge, consultations, une condition).
- Dénominateur / Exclusions — qui est comptabilisé, moins qui est exclu (soins palliatifs, soins de fin de vie, fragilité…).
- Numérateur — qui a satisfait à la mesure (un dépistage, une valeur contrôlée).
- Score —
numérateur / (dénominateur − exclusions).
Chacun de ces éléments est un ensemble de patients. SQL est très efficace pour les ensembles. La seule chose qui sépare les données FHIR d'une requête SQL, c'est que les ressources FHIR sont du JSON profondément imbriqué, et que l'appartenance terminologique (« ce code fait-il partie du jeu de valeurs? ») n'est pas une colonne sur laquelle on peut filtrer. SQL on FHIR résout ces deux problèmes.
Couche 1 : aplatir FHIR en tables avec des ViewDefinitions
Une ViewDefinition est une ressource FHIR qui décrit comment aplatir un type de ressource en une table plane. Pointez-en une sur Encounter, matérialisez-la, et vous obtenez une table sof.encounter_flat bien structurée avec patient_id, type_system, type_code, status, period_start, etc. — votre logique de mesure lit des colonnes simples plutôt que d'explorer des structures FHIR imbriquées.
L'exemple fournit une ViewDefinition par type de ressource que les mesures utilisent — patient, encounter, condition, observation, procedure, et quelques autres — chacune matérialisée dans le schéma sof.* et exposée via une vue enveloppante légère. Cette couche plate partagée est réutilisée par toutes les mesures.
Couche 2 : la terminologie comme JOIN, non comme $expand
L'autre partie difficile d'une mesure est l'appartenance à un jeu de codes : « cette consultation est-elle l'un des sept types de visites admissibles? » Dans un environnement avec moteur CQL, cela est typiquement résolu à l'exécution contre un service de terminologie. Avec SQL on FHIR, vous aplatissez les expansions de ValueSet une seule fois dans une table concepts — une ligne par triplet (valueset_url, system, code) — et l'appartenance devient une jointure ordinaire :
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/…'
Aucun aller-retour réseau, aucune expansion par patient. Le jeu de valeurs est étendu hors ligne et indexé, de sorte que la vérification « le code de ce patient est-il comptabilisé? » est basée sur des ensembles et rapide.
Couche 3 : la mesure est une seule requête SQL
Avec des tables plates et un JOIN sur concepts disponibles, une mesure entière devient une seule requête composée de CTE — et elle ressemble remarquablement à la définition en langage courant de la mesure. Voici la Population initiale de CMS130 (Dépistage du cancer colorectal) — les patients âgés de 46 à 75 ans ayant eu une consultation admissible durant la période de mesure :
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
Le numérateur ajoute quelques CTE supplémentaires (coloscopie dans les 9 dernières années, FOBT durant la période, etc.), les exclusions regroupent les soins palliatifs et de fin de vie et la fragilité, et un SELECT final calcule le score. L'essentiel : la mesure est lisible. Vous pouvez la lire, la comparer à la spécification, et faire SELECT * FROM initial_population pour voir exactement qui s'est qualifié.
La logique partagée reste partagée
Des exclusions comme les soins palliatifs, les soins de fin de vie et la maladie avancée avec fragilité se retrouvent dans de nombreuses mesures. Dans l'exemple, elles sont factorisées en éléments réutilisables plutôt que copiées-collées, de sorte que corriger la logique des soins palliatifs la corrige partout à la fois.
Mais qui écrit tout ce SQL?
L'objection évidente : traduire manuellement le CQL d'une mesure en SQL ressemble à un travail long et sujet aux erreurs — et il existe des dizaines d'eCQM. En pratique, c'est exactement le genre de tâche que les assistants de codage basés sur l'IA gèrent bien. CQL et SQL sont tous deux des langages structurés et bien spécifiés, et la logique d'une mesure se mappe proprement sur le patron CTE présenté ci-dessus (population initiale → exclusions → numérateur → score).
Ce n'est pas un espoir — c'est ainsi que l'exemple lui-même a été construit. Chaque mesure a été traduite de son CQL publié vers SQL avec un assistant IA, puis vérifiée patient par patient contre les données de référence MeasureReport de CMS jusqu'à ce que les chiffres concordent exactement. L'IA effectue la traduction mécanique; les données de référence la maintiennent honnête.
Les données de référence sont ce qui rend la traduction assistée par IA digne de confiance : les résultats MeasureReport attendus pour les patients de test sont publiés avec le contenu des mesures dans le dépôt dqm-content-qicore-2025, de sorte que chaque mesure traduite est vérifiable par rapport à une vérité terrain — et non acceptée sur la foi.
Rendre cela conforme aux standards : SQLQuery + Measure/$evaluate-measure
Exécuter du SQL en interne est bien, mais l'objectif est un service FHIR conforme, pas un script de base de données. Deux éléments SQL on FHIR comblent cet écart :
- Le SQL de chaque mesure est stocké dans Aidbox comme ressource
LibrarySQLQuery (un profil SQL on FHIR surLibrary) et invoqué avec l'opération$sqlquery-run. La logique de calcul réside dans le serveur FHIR, comme des ressources de première classe — pas dans le code applicatif. - Le service répond à l'opération FHIR R4 standard
Measure/$evaluate-measureet retourne unMeasureReportconforme — de sorte que tout client FHIR le consomme de la même façon qu'un résultat d'un moteur CQL.
Chaque bibliothèque SQLQuery déclare également une lignée depends-on vers les ViewDefinitions qu'elle lit, vous donnant un graphe interrogeable : mesure → vues → ressources. Tout ce dont une mesure a besoin — les vues plates, la terminologie, le calcul — est maintenant exprimé sous forme de ressources FHIR standard résidant dans le serveur FHIR. Ce qui prépare le vrai avantage.
L'application evaluate-measure au centre ne contient aucune logique de mesure : elle résout la bonne bibliothèque SQLQuery, invoque $sqlquery-run, et structure les lignes retournées en un MeasureReport. Le calcul lui-même s'exécute dans la base de données.
L'avantage : toute la suite de mesures est un paquet FHIR portable
Parce que chaque partie d'une mesure est maintenant une ressource FHIR standard, l'ensemble de la suite s'emballe dans un seul paquet FHIR NPM : la terminologie (CodeSystems + ValueSets), les ViewDefinitions qui aplatissent les données, et les bibliothèques SQLQuery qui contiennent la logique de calcul. Dans l'exemple, ce paquet transporte 170 ressources sur une douzaine de mesures — 10 CodeSystems, 107 ValueSets, 10 ViewDefinitions, et 43 bibliothèques SQLQuery.
Le format du paquet est standard — le même format FHIR NPM que les Implementation Guides utilisent — de sorte que n'importe quel serveur FHIR peut le charger avec son propre mécanisme d'installation de paquet; dans Aidbox, c'est un appel $fhir-package-install au démarrage du serveur, et les définitions sont simplement disponibles. Et voici ce qui est important : ce paquet n'est pas lié à Aidbox. Ce sont des blocs de construction SQL on FHIR standard — des ViewDefinitions et des bibliothèques SQLQuery définies par la spécification SQL on FHIR. Déplacez le paquet vers n'importe quel serveur FHIR qui implémente ces blocs de construction, installez-le, et les mêmes mesures calculent de la même façon. La façon dont chaque serveur les exécute est un détail d'implémentation — certains font tourner un moteur SQL on FHIR distinct, d'autres exécutent en base de données.
Aidbox emprunte le chemin en base de données : les ViewDefinitions se matérialisent en tables ou vues et les bibliothèques SQLQuery s'exécutent en SQL natif, directement dans son PostgreSQL — là où les données résident déjà. Ainsi, le paquet s'installe et fonctionne sans rien de supplémentaire à déployer pour le calcul lui-même : aucun moteur d'exécution à déployer, à faire évoluer ou à maintenir synchronisé. La logique de mesure se distribue de la même façon qu'un Implementation Guide — sous forme d'artefacts FHIR standard et partageables.
Analyser un résultat
Vous souvenez-vous du problème « difficile à expliquer »? Il trouve ici une réponse directe. Demandez pour un seul patient, et le MeasureReport contient un tableau evaluatedResource : des références réelles à des ressources FHIR, chacune étiquetée avec la population qu'elle satisfait via l'extension standard 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" }
]
}
]
« Pourquoi ce patient est-il dans le numérateur? » — cette Procedure. « Pourquoi est-il exclu? » — cette consultation de soins palliatifs. Chaque référence pointe vers une ressource qui existe dans le serveur, de sorte que l'étape suivante de l'enquête est simplement un GET.
Pour l'analyse à l'échelle d'une population, chaque mesure inclut également une bibliothèque SQLQuery -evidence : une ligne par patient avec la chaîne de décision complète — quel chemin a satisfait au numérateur (coloscopie ou FOBT ou aucun), la ressource déclencheuse avec son code et sa date, et quelle exclusion s'est appliquée. C'est une liste de travail sur les écarts de soins — qui manque un dépistage, et exactement ce qui manque — comme une seule requête. L'application de démonstration de l'exemple est ces requêtes rendues cliquables : une liste de travail multi-mesures, une vue à 360° par patient avec exploration des preuves, et des listes d'actions exportables.

Et lorsqu'un chiffre semble encore incorrect, chaque population est un CTE nommé : SELECT * FROM initial_population et affinez pas à pas — sans avoir à retracer les mécanismes internes d'un moteur.
Essayez-le vous-même
Tout ce qui précède est un exemple fonctionnel et à source ouverte dans le dépôt d'exemples Aidbox — une douzaine de mesures CMS (CMS130, CMS165, CMS125, CMS131, et plus) avec des données patients d'exemple et une application de démonstration interactive.
→ github.com/Aidbox/examples · aidbox-custom-operations/measure-evaluate
Suivez les instructions dans le README pour faire tourner l'ensemble de la pile localement : démarrez Aidbox avec le paquet de mesures installé au démarrage, chargez le jeu de données d'exemple, calculez les mesures via l'opération standard Measure/$evaluate-measure, et explorez les preuves de chaque patient dans l'interface de l'application de démonstration. Ce qui est retourné est un MeasureReport FHIR ordinaire avec les décomptes de population et le score — calculé par SQL, à l'intérieur de Aidbox, sans moteur CQL nulle part dans la pile.

À retenir
CQL est un bon langage de rédaction pour les mesures de qualité. Mais il n'a pas à être votre moteur d'exécution. Lorsque vous aplatissez FHIR avec des ViewDefinitions, transformez la terminologie en jointure, et exprimez chaque mesure comme une bibliothèque SQLQuery derrière Measure/$evaluate-measure, la logique de mesure cesse d'être enfermée dans un moteur et devient ce que SQL on FHIR promet : des artefacts FHIR standard que vous pouvez empaqueter une fois et exécuter n'importe où. Distribuez toute la suite comme un paquet FHIR, installez-la sur n'importe quel serveur SQL on FHIR, et les mêmes mesures calculent de la même façon — explicables jusqu'à la ressource individuelle. Sur Aidbox, elles s'exécutent nativement dans PostgreSQL, il n'y a donc aucun moteur d'exécution distinct à opérer. Et y arriver est plus accessible que cela en a l'air : les assistants IA traduisent le CQL en SQL, et les données de référence propres à CMS vérifient chaque mesure par rapport aux résultats attendus.
Vous souhaitez essayer cette approche de calcul des mesures sur vos propres données? Communiquez avec nous — nous serions ravis de vous guider à travers le processus.






