Le problème de l'exécution des eCQM
Les mesures électroniques de qualité clinique (eCQM) sont le moyen utilisé par les programmes pour évaluer la qualité des soins — par exemple, si les patients admissibles ont reçu leur dépistage colorectal ou si leur hypertension est maîtrisée. Les mesures CMS et HEDIS que chaque programme de soins basé sur la valeur doit rapporter sont rédigées en CQL (Clinical Quality Language), et la méthode habituelle pour les exécuter est un moteur CQL dédié : un environnement d'exécution distinct qui analyse la mesure, effectue des appels vers votre serveur FHIR pour les terminologies 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 mettez à l'échelle en parallèle de 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 une préoccupation de mise à l'échelle pour de grandes populations.
- Difficile à expliquer. Lorsqu'un patient apparaît comme ayant une lacune dans ses soins, pouvez-vous montrer à un clinicien exactement pourquoi? Cela implique de retracer les mécanismes internes du moteur et les expansions d'ensembles de valeurs — pas de lire une requête.
Voici donc la question à laquelle ce billet répond : et si la logique des mesures 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 est plus qu'un tour de passe-passe pour les performances : avec SQL on FHIR, l'ensemble des connaissances nécessaires au calcul d'une mesure devient des artefacts FHIR portables et normalisés que vous pouvez regrouper dans un paquet FHIR et installer sur un autre serveur. Voici une présentation technique de la méthode, accompagnée d'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 une arithmétique d'ensembles sur une population de patients :
- Population initiale — qui est admissible (âge, consultations, un état de santé).
- Dénominateur / Exclusions — qui est comptabilisé, moins qui est exclu (soins palliatifs, soins de confort, fragilité…).
- Numérateur — qui a satisfait la mesure (un dépistage, une lecture contrôlée).
- Score —
numérateur / (dénominateur − exclusions).
Chacun de ces éléments est un ensemble de patients. SQL excelle dans la manipulation d'ensembles. La seule chose qui sépare les données FHIR d'une requête SQL est que les ressources FHIR sont du JSON profondément imbriqué, et que l'appartenance à une terminologie (« ce code fait-il partie de l'ensemble 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 plate. Pointez-en une vers Encounter, matérialisez-la, et vous obtenez une table sof.encounter_flat bien organisée avec patient_id, type_system, type_code, status, period_start, etc. — votre logique de mesure lit des colonnes simples au lieu de naviguer dans des structures FHIR imbriquées.
L'exemple fournit une ViewDefinition par type de ressource touché par les mesures — patient, consultation, condition, observation, procédure, 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 chaque mesure.
Couche 2 : la terminologie comme JOIN, pas comme $expand
L'autre partie complexe d'une mesure est l'appartenance à un ensemble de codes : « cette consultation est-elle l'un des sept types de visite admissible? » Dans un environnement avec moteur CQL, cela est typiquement résolu à l'exécution via un service de terminologie. Avec SQL on FHIR, vous aplatissez les expansions de ValueSet une seule fois dans une table concepts — une ligne par (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. L'ensemble de valeurs est expansé hors ligne et indexé, donc la vérification « le code de ce patient est-il pris en compte? » est basée sur des ensembles et rapide.
Couche 3 : la mesure est une seule requête SQL
Avec des tables plates et une jointure concepts disponibles, une mesure complète devient une seule requête composée de CTE — et elle se lit de façon remarquablement proche de 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, RSOS durant la période, etc.), les exclusions regroupent les soins palliatifs/de confort/de fragilité, et un SELECT final calcule le score. Le point essentiel : la mesure est lisible. Vous pouvez la lire, la comparer à la spécification, et faire SELECT * FROM initial_population pour voir exactement qui est admissible.
La logique partagée reste partagée
Les exclusions comme les soins palliatifs, les soins de confort et la maladie avancée avec fragilité reviennent dans de nombreuses mesures. Dans l'exemple, elles sont factorisées en composants 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 semble être un travail considérable et sujet aux erreurs — et il existe des dizaines d'eCQM. En pratique, c'est exactement le type de tâche que les assistants de codage par intelligence artificielle modernes 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 modèle CTE présenté ci-dessus (population initiale → exclusions → numérateur → score).
Ce n'est pas une espérance — c'est ainsi que l'exemple lui-même a été construit. Chaque mesure a été traduite de son CQL publié en SQL avec un assistant IA, puis vérifiée par rapport aux données de référence MeasureReport de CMS, patient par patient, jusqu'à ce que les chiffres correspondent exactement. L'IA effectue la traduction mécanique ; les données de référence maintiennent l'honnêteté.
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 aux côtés du 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 — pas acceptée sur la foi.
Rendre le tout conforme aux normes : SQLQuery + Measure/$evaluate-measure
Exécuter du SQL convient en interne, 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 en tant que ressource SQLQuery
Library(un profil SQL-on-FHIR surLibrary) et invoqué avec l'opération$sqlquery-run. La logique de calcul réside dans le serveur FHIR, en tant que ressources de première classe — pas dans le code applicatif. - Le service répond à l'opération FHIR R4 standard
Measure/$evaluate-measureet retourne unMeasureReportapproprié — 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 désormais exprimé en tant que ressources FHIR standard résidant dans le serveur FHIR. Ce qui prépare le vrai bénéfice.
L'application evaluate-measure au milieu ne contient aucune logique de mesure : elle résout la bonne bibliothèque SQLQuery, invoque $sqlquery-run, et façonne les lignes retournées en un MeasureReport. Le calcul lui-même s'exécute dans la base de données.
Le bénéfice : toute la suite de mesures est un paquet FHIR portable
Parce que chaque partie d'une mesure est désormais une ressource FHIR standard, la suite entière se regroupe 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 pour 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 NPM FHIR dans lequel les Guides d'implémentation (IG) sont distribués — de sorte que tout 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 là. Et voici la partie importante : 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 se calculent de la même façon. La manière dont chaque serveur les exécute est un détail d'implémentation — certains font fonctionner un moteur SQL-on-FHIR distinct, d'autres exécutent en base de données.
Aidbox emprunte la voie en base de données : les ViewDefinitions se matérialisent en tables ou en 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 plus à déployer pour le calcul lui-même : aucun moteur d'exécution à déployer, à mettre à l'échelle ou à maintenir synchronisé. La logique des mesures se distribue de la même façon qu'un Guide d'implémentation — en tant qu'artefacts FHIR standard et partageables.
Analyser un résultat
Vous vous souvenez du problème « difficile à expliquer »? Il reçoit une réponse directe ici. Demandez pour un seul patient, et le MeasureReport contient un tableau evaluatedResource : de vraies références de 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 Procédure. « Pourquoi est-il exclu? » — cette consultation de soins palliatifs. Chaque référence pointe vers une ressource qui existe dans le serveur, donc la prochaine étape de l'investigation est simplement un GET.
Pour l'investigation à l'échelle d'une population, chaque mesure est également accompagnée d'une bibliothèque SQLQuery -evidence : une ligne par patient avec la chaîne de décision complète — quelle voie a satisfait le numérateur (coloscopie, RSOS ou aucune), la ressource déclencheuse avec son code et sa date, et quelle exclusion a été déclenchée. C'est une liste de travail sur les lacunes de soins — qui manque un dépistage, et qu'est-ce qui manque exactement — sous forme d'une seule requête. L'application de démonstration de l'exemple rend ces requêtes cliquables : une liste de travail multi-mesures, une vue 360 par patient avec exploration des preuves, et des listes de sensibilisation exportables.

Et lorsqu'un chiffre semble encore erroné, chaque population est un CTE nommé : SELECT * FROM initial_population et réduisez 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 open-source fonctionnel dans le dépôt d'exemples Aidbox — une douzaine de mesures CMS (CMS130, CMS165, CMS125, CMS131, et plus) avec des données de patients types et une application de démonstration interactive.
→ github.com/Aidbox/examples · aidbox-custom-operations/measure-evaluate
Suivez les instructions dans le README pour exécuter toute la pile localement : démarrez Aidbox avec le paquet de mesures installé au démarrage, chargez le jeu de données types, 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énombrements de populations et le score — calculé par SQL, dans Aidbox, sans aucun moteur CQL nulle part dans la pile.

Conclusion
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 en tant que bibliothèque SQLQuery derrière Measure/$evaluate-measure, la logique des mesures cesse d'être enfermée dans un moteur et devient ce que SQL on FHIR promet : des artefacts FHIR standard que vous pouvez regrouper une fois et exécuter n'importe où. Distribuez toute la suite sous forme de paquet FHIR, installez-la sur n'importe quel serveur SQL-on-FHIR, et les mêmes mesures se calculent de la même façon — explicables jusqu'à la ressource individuelle. Sur Aidbox, elles s'exécutent nativement dans PostgreSQL, donc il n'y a aucun moteur d'exécution distinct à opérer. Et y parvenir est plus accessible que cela ne le paraît : les assistants IA traduisent le CQL en SQL, et les données de référence de 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? Contactez-nous — nous serions ravis de vous guider à travers le processus.





