|
6 min de lecture
|

SQL on FHIR : les ValueSets de première classe

Résumé de l'article

Une requête SQL a besoin à la fois de données patient et de 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 est d'implémenter, d'expérimenter et de 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. 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 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 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 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 forment pas un ensemble exécutable, et l'interface ValueSet demeure 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 à l'intérieur d'une requête SQL. Ce sont des tâches liées, 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 à son encontre. La question est de savoir comment en faire une interface commune plutôt que quelque chose que chaque implémentation invente.

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 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 révisé)

D'abord, transformer les Conditions en lignes

Une ViewDefinition SQL on FHIR décrit comment extraire des lignes et des colonnes de ressources FHIR à l'aide de 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 patient sont Patient/p1, Patient/p2 et Patient/p3, et que le moteur d'exécution représente leurs clés sous forme de 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 disposons maintenant de données patient que nous pouvons interroger. Il nous reste encore à savoir quels codes comptent comme 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 les 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 patron 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 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 révisé)

Pour notre requête 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 supplémentaires de Library.

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. Le SQL utilise leurs noms locaux plutôt que de résoudre les URL lui-même.

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 que ce soit très simple, surtout pour les cas simples. »

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

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 importe : 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 nous 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 ? Mais je peux faire une jointure et construire ma mesure. »

— Nikolai Ryzhikov, 8 septembre (légèrement révisé)

La proposition ne standardise 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

Le moteur d'exécution expose diabetes_codes comme une relation nommée. Notre requête y effectue une jointure :

# 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 accessible 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 résultat. 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 alors directement accès en SQL. Les fonctions ne permettent pas cela aussi facilement. »

— Gino Canessa, 15 septembre (légèrement révisé)

Le coût est une jointure répétée sur plusieurs colonnes. L'ébauche doit aussi comporter une règle claire d'unicité pour (system, version, code) : les lignes d'appartenance en double 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. Les performances dépendent 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'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 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. Figer le ValueSet seul ne fixe pas nécessairement toutes les dépendances de terminologie utilisées pour le développer.

Cela me tient à cœur parce que nous avons un jour brisé la validation dans Aidbox en utilisant « latest ». Le validateur a capté des statuts de rencontre modifiés de R5, et la validation a échoué sur les serveurs R4 en production. Depuis, nous figeons autant que possible et mettons à jour les versions 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 en 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 des requêtes réelles.

John Grimes a proposé une prochaine étape concrète :

« Je pense que nous devrions créer une branche et commencer à développer des éléments dans la spécification et l'implémentation de référence pour en avoir une idée. »

— John Grimes, 15 septembre (mots de remplissage retirés)

Nous commencerons avec 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 aussi tester des 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 au SQL et ce que chaque implémentation doit prendre en charge.

Rejoignez la discussion sur la 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 une expérience d'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 développons les deux côtés de ceci : Aidbox pour travailler avec des 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 les essayer gratuitement. Explorez SQL on FHIR dans Aidbox, et utilisez Termbox pour travailler avec la terminologie et les développements 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, et non 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 révisés pour la lisibilité.

Voir aussi : Réunions du groupe de travail SQL on FHIR.

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

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