|
6 min de lecture
|

SQL on FHIR : ValueSets de première classe

Résumé de l'article

Une requête SQL a besoin à la fois des données des patients et de la terminologie. Nous explorons comment déclarer un ValueSet comme dépendance aux côtés d'une ViewDefinition, puis exposer son appartenance sous forme de relation ou de fonction member_of. Le groupe de travail penche actuellement vers la relation, mais la prochaine étape consiste à implémenter, expérimenter et décider.

Résumer cet article avec :
ChatGPTPerplexityClaudeGrok

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 Diabète.

Nous avons besoin de deux entrées : les codes de diagnostic des patients et les codes inclus dans le ValueSet. Un tableau 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 Diabète :

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 n'est 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 la 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 vois deux phases ici. Lorsque nous aplatissons les ressources FHIR, nous pouvons normaliser la terminologie — par exemple, traduire les codes de diagnostic à l'aide de fonctions FHIRPath. Ensuite, lorsque nous construisons une cohorte, nous devons utiliser la terminologie dans une requête SQL. Ce sont des tâches connexes, mais elles nécessitent des interfaces différentes.

Pour certaines mesures, nous prenons déjà un ValueSet développé, le traitons comme une table et écrivons du SQL contre lui. La question est de savoir comment en faire une interface commune plutôt que quelque chose que chaque implémentation invente.

John Grimes a fait le lien avec le 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 en vue d'une requête à l'exécution. Nous essayons effectivement de faire la même chose pour les cas d'utilisation analytiques. »

— John Grimes, 15 septembre (légèrement édité)

D'abord, transformer les Conditions en lignes

Une ViewDefinition 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 de la Condition avec chaque élément de code.coding. Pour ces exemples, supposons que les références patients sont Patient/p1, Patient/p2 et Patient/p3, et que le moteur d'exécution représente leurs clés comme p1, p2 et p3. Les représentations réelles des clés sont définies par le moteur d'exécution; 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 avons maintenant des données patient que nous pouvons interroger. Il nous reste à savoir quels codes sont considérés comme du diabète.

Déclarer les deux entrées de la vue SQL

Une vue ou 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 SQL.

Le 1er septembre, j'ai proposé d'appliquer le même modèle aux ValueSets :

« Nous pouvons référencer une ViewDefinition dans la vue SQL via relatedArtifact, lui donner un nom de table et l'utiliser dans une jointure. Nous pouvons utiliser 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 condition. 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 pièce jointe avec contentType: application/sql. Ici, url pointe vers un fichier SQL illustratif; alternativement, 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: |, et 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. Le moteur d'exécution — le logiciel qui exécute la requête — résout les dépendances. 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é du moteur d'exécution :

« Vous dites simplement que j'ai une dépendance envers ce value set. […] 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 seconde 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 compte : une relation n'a pas à être une table physique. Ce peut être une vue, des données en cache ou quelque chose évalué au besoin. Comme je l'ai dit le 8 septembre :

« Nous pouvons traiter le ValueSet pré-développé comme une relation. Peu importe comment nous y sommes arrivés — 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 effectuer des jointures 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 mise à la disposition de SQL.

Option A : joindre le ValueSet comme une relation

Le moteur d'exécution expose diabetes_codes comme une relation nommée. Notre requête effectue une jointure contre 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 change 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 importe, vous en faites une colonne lors de votre extraction et vous y avez alors 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. L'ébauche nécessite également une règle de unicité claire pour (system, version, code) : des lignes d'appartenance dupliquées peuvent multiplier les résultats de la requête. 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. Laquelle est plus performante dépend du moteur.

Le compromis est 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 moteur d'exécution prend en charge les deux interfaces, elles devraient s'entendre 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 moteur d'exécution 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 en soi 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 du CodeSystem. Fixer la version du ValueSet seul ne fixe pas nécessairement chaque dépendance de terminologie utilisée pour le développer.

Cela me préoccupe parce que nous avons un jour cassé la validation dans Aidbox en utilisant « latest ». Le validateur a capté des statuts de rencontre modifiés provenant de R5, et la validation a échoué sur des serveurs R4 en production. Depuis, nous fixons les versions autant que possible et mettons à jour les épingles de façon délibérée.

Pour notre requête, le risque est une liste de patients modifiée même si le SQL n'a pas changé. Si deux moteurs d'exécution 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.

L'ébauche 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, et non la deviner silencieusement.

Implémenter, expérimenter, puis décider

Le groupe de travail penche actuellement vers la relation comme base de référence, 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 pratique :

« Je pense que nous devrions créer une branche et commencer à construire des éléments dans la spec 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 à quel point 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 ce que chaque moteur d'exécution devrait prendre en charge.

L'objectif immédiat est simple : une requête devrait pouvoir dire je dépends de ce ValueSet. L'expérience devrait nous aider à décider comment les moteurs d'exécution l'exposent à SQL et ce que chaque implémentation doit prendre en charge.

Rejoignez la discussion sur Terminology 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 à prendre une décision.

Essayez SQL on FHIR et la terminologie par vous-même

Chez Health Samurai, nous construisons les deux côtés de cette équation : 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 discutée 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 mon ébauche d'abstraction ValueSet. Les extraits de réunion ont été légèrement édités pour la lisibilité.

Voir aussi : réunions du groupe de travail SQL on FHIR et analytique interopérable avec SQL on FHIR pour le tableau complet ViewDefinition / SQLView / SQLQuery dans lequel ce travail sur les ValueSets s'inscrit.

Partager cet article
Comments
Comments
Sign in
Loading comments...
Subscribe to our blog

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