Une question : quels patients ont un diagnostic de diabète ?
Commençons par une tâche simple :
Retourner les patients dont le code de diagnostic appartient au ValueSet Diabetes.
Nous avons besoin de deux entrées : les codes de diagnostic des patients et les codes inclus dans le ValueSet. Une table de conditions nous fournit la première, mais pas la seconde.
Voici trois lignes de diagnostic et les deux membres de notre exemple de ValueSet Diabetes :
conditions:
- patient_id: p1
system: http://snomed.info/sct
version: "2026-02"
code: "73211009"
- patient_id: p2
system: http://snomed.info/sct
version: "2026-02"
code: "44054006"
- patient_id: p3
system: http://snomed.info/sct
version: "2026-02"
code: "22298006"
diabetes_codes:
- system: http://snomed.info/sct
version: "2026-02"
code: "73211009"
display: Diabetes mellitus
- system: http://snomed.info/sct
version: "2026-02"
code: "44054006"
display: Type 2 diabetes mellitus
La réponse devrait être p1 et p2. Le code de diagnostic de p3 ne figure pas dans ce ValueSet. Nous allons construire les deux entrées et essayer deux façons de poser la même question.
Les exemples utilisent YAML pour des raisons de lisibilité et des versions de terminologie illustratives. Ce tout petit ValueSet n'est pas une définition clinique complète. Les extraits de ressources ne constituent pas un paquet exécutable, et l'interface ValueSet est encore une proposition.
Je distingue deux phases. Lorsque nous aplatissons les ressources FHIR, nous pouvons normaliser la terminologie — par exemple, traduire les codes de diagnostic à l'aide de fonctions FHIRPath. Plus tard, lorsque nous construisons une cohorte, nous devons utiliser la terminologie dans une requête SQL. Ce sont des tâches apparentées, mais qui nécessitent des interfaces différentes.
Pour certaines mesures, nous prenons déjà un ValueSet développé, nous le traitons comme une table et nous écrivons du SQL à son encontre. La question est de savoir comment en faire une interface commune plutôt que quelque chose que chaque implémentation invente à sa façon.
John Grimes a relié cela au problème que les serveurs de terminologie résolvent déjà :
« C'est le problème que le serveur de terminologie résout pour préparer les données de terminologie aux fins des requêtes à l'exécution. Nous essayons effectivement de faire la même chose pour les cas d'usage analytiques. »
— John Grimes, 15 septembre (légèrement édité)
Première étape : transformer les Conditions en lignes
Une ViewDefinition de SQL on FHIR décrit comment extraire des lignes et des colonnes à partir de ressources FHIR en utilisant FHIRPath. Elle ne contient pas de SQL.
Voici une vue simplifiée sur Condition :
resourceType: ViewDefinition
url: http://example.org/ViewDefinition/conditions
version: 1.0.0
name: conditions
status: draft
resource: Condition
select:
- column:
- name: patient_id
path: subject.getReferenceKey(Patient)
select:
- forEach: code.coding
column:
- name: system
path: system
- name: version
path: version
- name: code
path: code
Le select imbriqué combine l'identifiant patient provenant de la Condition avec chaque élément de code.coding. Pour ces exemples, supposons que les références patient sont Patient/p1, Patient/p2 et Patient/p3, et que l'exécuteur représente leurs clés sous la forme p1, p2 et p3. Les représentations réelles des clés sont définies par l'exécuteur ; elles doivent être cohérentes avec la vue Patient correspondante. getReferenceKey(Patient) restreint le type de référence à Patient.
Nous utilisons des codages versionnés pour rendre la correspondance explicite ; les données réelles omettent souvent la version.
Nous disposons maintenant de données patient que nous pouvons interroger. Nous devons encore savoir quels codes comptent comme du diabète.
Déclarer les deux entrées de la vue SQL
Une vue ou une requête SQL opère sur les lignes produites par des ViewDefinitions. SQL on FHIR représente les requêtes SQL à l'aide d'une ressource FHIR Library. Sa liste relatedArtifact déclare les dépendances : type: depends-on indique qu'une entrée est nécessaire, resource l'identifie, et label lui donne un nom local pour le SQL.
Le 1er septembre, j'ai proposé d'appliquer le même modèle aux ValueSets :
« Nous pouvons faire référence à une ViewDefinition dans la vue SQL via relatedArtifact, lui donner un nom de table et l'utiliser dans une jointure. Nous pouvons adopter une approche similaire pour le ValueSet : ma vue SQL dépend d'un ValueSet, et je lui donne un nom. »
— Nikolai Ryzhikov, 1er septembre (légèrement édité)
Pour notre requête sur le diabète, la déclaration de dépendance proposée ressemble à cet extrait de Library :
resourceType: Library
url: http://example.org/Library/patients-with-diabetes
status: draft
type:
coding:
- system: http://hl7.org/fhir/uv/sql-on-fhir/CodeSystem/LibraryTypesCodes
code: sql-query
relatedArtifact:
- type: depends-on
label: conditions
resource: http://example.org/ViewDefinition/conditions|1.0.0
- type: depends-on
label: diabetes_codes
resource: http://example.org/ValueSet/diabetes|2026
content:
- contentType: application/sql
url: https://example.org/queries/patients-with-diabetes.sql
La première dépendance fournit les lignes de conditions. La seconde fournit l'appartenance au ValueSet. La partie après | demande une version particulière de cet artefact.
Le SQL lui-même appartient à Library.content : une Attachment avec contentType: application/sql. Ici, url pointe vers un fichier SQL illustratif ; en variante, data peut contenir le SQL encodé en base64. Les deux requêtes ci-dessous sont des contenus alternatifs de ce fichier. Nous les présentons sous forme de blocs YAML lisibles sql: |, non comme des champs Library supplémentaires.
C'est ce que j'entends par ValueSets de première classe : la requête déclare explicitement la terminologie dont elle a besoin, aux côtés de ses vues de données. L'exécuteur — le logiciel exécutant la requête — résout les dépendances. Le SQL utilise leurs noms locaux au lieu de résoudre lui-même les URL.
John a décrit la même séparation du côté de l'exécuteur :
« Vous dites simplement que vous avez une dépendance envers ce ValueSet. […] Pour l'abstraction SQL, nous voulons garder les choses très simples, surtout pour les cas simples. »
— John Grimes, 15 septembre (légèrement édité)
L'appartenance, pas un algorithme d'expansion
La deuxième dépendance fournit l'appartenance diabetes_codes présentée au début.
Owen Loveluck a demandé si l'abstraction devrait être une vue plutôt qu'une table. Cette distinction est importante : une relation n'a pas à être une table physique. Elle peut être une vue, des données en cache ou quelque chose évalué au besoin. Comme je l'ai formulé le 8 septembre :
« Nous pouvons traiter le ValueSet pré-développé comme une relation. Peu importe comment nous y sommes parvenus — s'agit-il d'une requête d'expansion dynamique ou de données pré-développées chargées dans la base de données ? Je peux faire une jointure et construire ma mesure. »
— Nikolai Ryzhikov, 8 septembre (légèrement édité)
La proposition ne normalise pas la façon dont ValueSet.compose, ECL ou VCL sont évalués. Elle décrit comment l'appartenance résultante est rendue disponible au SQL.
Option A : joindre le ValueSet comme une relation
L'exécuteur expose diabetes_codes comme une relation nommée. Notre requête se joint à elle :
# Query text for illustration; `sql` is not a FHIR Library field.
sql: |
SELECT DISTINCT c.patient_id
FROM conditions c
JOIN diabetes_codes vs
ON vs.system = c.system
AND vs.version = c.version
AND vs.code = c.code
expected_patient_ids: [p1, p2]
Option B : tester l'appartenance avec une fonction
La même dépendance pourrait plutôt être disponible via member_of :
# Alternative query text, using the proposed function.
sql: |
SELECT DISTINCT c.patient_id
FROM conditions c
WHERE member_of(
c.system, c.version, c.code, 'diabetes_codes'
)
expected_patient_ids: [p1, p2]
Qu'est-ce qui diffère entre les deux options ?
Les deux requêtes retournent p1 et p2. Ce qui diffère, c'est la façon dont nous écrivons la requête et ce que l'interface nous permet de faire d'autre.
Il s'agit de SQL ordinaire. L'appartenance est visible : nous pouvons inspecter les codes, les compter ou inclure display dans notre sortie. Si la cohorte est incorrecte, nous pouvons examiner les deux entrées.
Des propriétés supplémentaires peuvent aussi être des colonnes. Gino Canessa l'a souligné en discutant des codes non sélectionnables :
« Si "non sélectionnable" est une propriété qui vous intéresse, vous en faites une colonne lors de votre extraction et vous y avez ensuite accès directement en SQL. Les fonctions ne permettent pas cela aussi facilement. »
— Gino Canessa, 15 septembre (légèrement édité)
Le coût est une jointure multi-colonnes répétée. Le brouillon a également besoin d'une règle d'unicité claire pour (system, version, code) : les lignes d'appartenance en double peuvent multiplier les résultats des requêtes. DISTINCT protège cette liste de patients, mais pas tous les agrégats que quelqu'un pourrait écrire ultérieurement.
La fonction est concise, s'intègre dans des expressions booléennes et ne multiplie pas les lignes d'entrée. Son implémentation pourrait utiliser une recherche efficace plutôt qu'une jointure. Les performances dépendent du moteur.
Le compromis concerne la portabilité : member_of est une fonction SQL on FHIR proposée, pas une fonction SQL standard. Les moteurs doivent la prendre en charge ou la traduire. Elle répond à une question d'appartenance, mais n'expose pas les membres ni ne retourne leur texte d'affichage.
Si un exécuteur prend en charge les deux interfaces, elles devraient s'accorder sur l'appartenance pour les mêmes entrées et le même instantané de terminologie. Nos deux requêtes devraient retourner les mêmes patients.
Un exécuteur doit également déclarer s'il prend en charge la relation, la fonction ou les deux. La concordance des résultats d'appartenance ne rend pas à elle seule une requête portable.
Les deux options nécessitent des règles de version claires
Il y a deux versions différentes dans l'exemple : diabetes|2026 identifie la définition du ValueSet, tandis que chaque ligne d'appartenance porte une version de CodeSystem. Épingler le ValueSet seul n'épingle pas nécessairement toutes les dépendances terminologiques utilisées pour le développer.
Ce point me tient à cœur, car nous avons un jour cassé la validation dans Aidbox en utilisant « latest ». Le validateur a récupéré des statuts de rencontre modifiés de R5, et la validation a échoué sur les serveurs R4 en production. Depuis lors, nous épinglons autant que possible et mettons à jour les épingles délibérément.
Pour notre requête, le risque est une liste de patients modifiée même si le SQL n'a pas changé. Si deux exécuteurs résolvent différemment une dépendance non versionnée, ils peuvent retourner des réponses différentes. Owen a soulevé exactement cette préoccupation lors de la réunion.
Le brouillon propose une appartenance cohérente pour chaque tâche, en enregistrant les versions résolues et en rejetant les résolutions ambiguës. Mais la façon dont les dépendances non versionnées devraient fonctionner reste ouverte. Notre exemple évite délibérément cette ambiguïté ; une implémentation doit la gérer explicitement, sans la deviner silencieusement.
Implémenter, expérimenter, puis décider
Le groupe de travail penche actuellement vers la relation comme référence de base, avec la fonction comme commodité optionnelle. C'est une orientation, pas une décision. Les deux options ont des propriétés utiles, et nous devons les essayer sur de vraies requêtes.
John Grimes a proposé une prochaine étape concrète :
« Je pense que nous devrions créer une branche et commencer à construire des éléments dans la spécification et l'implémentation de référence, juste pour avoir une idée de ce que ça donne. »
— John Grimes, 15 septembre (mots de remplissage supprimés)
Nous commencerons par des requêtes comme celle-ci et vérifierons trois choses : les interfaces retournent-elles les mêmes patients, comment gèrent-elles les versions, et dans quelle mesure sont-elles faciles à implémenter et à utiliser ? Nous devons également tester les propriétés supplémentaires et mesurer les performances. Nous pourrons alors décider de ce que chaque exécuteur doit prendre en charge.
L'objectif immédiat est simple : une requête doit pouvoir déclarer je dépends de ce ValueSet. L'expérience devrait nous aider à décider comment les exécuteurs l'exposent au SQL et ce que chaque implémentation doit obligatoirement prendre en charge.
Joignez-vous à la discussion sur la terminologie dans SQL on FHIR sur Zulip, et suivez les réunions du groupe de travail SQL on FHIR. Apportez une requête que vous devez exécuter, un ValueSet difficile à gérer, ou votre expérience dans l'implémentation de l'une ou l'autre approche — ces exemples nous aideront à tester les options et à décider.
Essayez SQL on FHIR et la terminologie par vous-même
Chez Health Samurai, nous construisons les deux côtés de ceci : Aidbox pour travailler avec les données FHIR et SQL on FHIR, et Termbox pour la terminologie FHIR. Ce sont des technologies que nous développons et utilisons nous-mêmes — et que nous rapportons au groupe de travail comme expérience pratique, pas seulement comme idées de conception.
Vous pouvez essayer les deux gratuitement. Explorez SQL on FHIR dans Aidbox, et utilisez Termbox pour travailler avec la terminologie et les expansions de ValueSet. Commencez avec vos propres données et un ValueSet que vous utilisez réellement ; c'est un meilleur test que n'importe quelle démonstration.
L'interface ValueSet de première classe dont il est question ici est encore une proposition. Les essais vous permettent d'explorer les capacités existantes des deux produits, pas une version déjà livrée de cette proposition.
Basé sur les discussions du groupe de travail SQL on FHIR des 1er, 8 et 15 septembre 2026, et sur mon brouillon d'abstraction ValueSet. Les extraits de réunion sont légèrement édités pour des raisons de lisibilité.
Voir aussi : Réunions du groupe de travail SQL on FHIR et Analytique FHIR avec SQL on FHIR : interopérable par conception pour la vue d'ensemble complète ViewDefinition / SQLView / SQLQuery dans laquelle s'inscrit ce travail sur les ValueSets.


![Terminology, c'est amusant : CodeableConcept.coding[]](/assets/images/articles/horizontal-5.avif?v=2ef8.muw)

