Una pregunta: ¿qué pacientes tienen un diagnóstico de diabetes?
Comenzamos con una tarea sencilla:
Devolver los pacientes cuyo código de diagnóstico pertenezca al ValueSet de Diabetes.
Necesitamos dos entradas: los códigos de diagnóstico de los pacientes y los códigos incluidos en el ValueSet. Una tabla de condiciones nos proporciona los primeros, pero no los segundos.
A continuación se muestran tres filas de diagnóstico y los dos miembros de nuestro ValueSet de Diabetes de ejemplo:
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 respuesta debe ser p1 y p2. El código de diagnóstico de p3 no figura en este ValueSet. Construiremos ambas entradas y probaremos dos formas de formular la misma pregunta.
Los ejemplos utilizan YAML por legibilidad y versiones de terminología ilustrativas. Este pequeño ValueSet no constituye una definición clínica completa. Los fragmentos de recursos no forman un paquete ejecutable, y la interfaz de ValueSet es todavía una propuesta.
Veo dos fases en este proceso. Cuando aplanamos recursos FHIR, podemos normalizar la terminología — por ejemplo, traducir códigos de diagnóstico mediante funciones FHIRPath. Más adelante, al construir una cohorte, necesitamos usar terminología dentro de una consulta SQL. Son tareas relacionadas, pero requieren interfaces distintas.
Para algunas medidas, ya tomamos un ValueSet expandido, lo tratamos como una tabla y escribimos SQL contra él. La pregunta es cómo convertir eso en una interfaz común, en lugar de algo que cada implementación inventa por su cuenta.
John Grimes vinculó esto al problema que los servidores de terminología ya resuelven:
«Este es el problema que el servidor de terminología resuelve para preparar datos de terminología en tiempo de ejecución. Estamos intentando hacer efectivamente lo mismo para casos de uso analíticos.»
— John Grimes, 15 de septiembre (ligeramente editado)
Primero, convertir Conditions en filas
Una ViewDefinition de SQL on FHIR describe cómo extraer filas y columnas de recursos FHIR mediante FHIRPath. No contiene SQL.
A continuación se muestra una vista simplificada sobre 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
El select anidado combina el identificador del paciente procedente de la Condition con cada elemento de code.coding. Para estos ejemplos, se asume que las referencias de los pacientes son Patient/p1, Patient/p2 y Patient/p3, y que el runner representa sus claves como p1, p2 y p3. Las representaciones reales de las claves las define el runner; deben ser coherentes con la vista de Patient correspondiente. getReferenceKey(Patient) restringe el tipo de referencia a Patient.
Usamos codings versionados para que la correspondencia sea explícita; los datos reales a menudo omiten la versión.
Ya disponemos de datos de pacientes que podemos consultar. Aún necesitamos saber qué códigos se consideran diabetes.
Declarar ambas entradas en la vista SQL
Una vista o consulta SQL opera sobre las filas producidas por las ViewDefinitions. SQL on FHIR representa las consultas SQL mediante un recurso Library de FHIR. Su lista relatedArtifact declara dependencias: type: depends-on indica que se necesita una entrada, resource la identifica, y label le asigna un nombre local para SQL.
El 1 de septiembre propuse aplicar el mismo patrón a los ValueSets:
«Podemos hacer referencia a una ViewDefinition en la vista SQL a través de relatedArtifact, asignarle un nombre de tabla y utilizarla en un join. Podemos usar un enfoque similar para el ValueSet: mi vista SQL depende de un ValueSet, y le estoy dando un nombre.»
— Nikolai Ryzhikov, 1 de septiembre (ligeramente editado)
Para nuestra consulta de diabetes, la declaración de dependencias propuesta tiene el siguiente aspecto en este fragmento 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 primera dependencia suministra las filas de condiciones. La segunda suministra la pertenencia al ValueSet. La parte tras | solicita una versión concreta de ese artefacto.
El propio SQL pertenece a Library.content: un Attachment con contentType: application/sql. Aquí, url apunta a un archivo SQL ilustrativo; alternativamente, data puede contener el SQL codificado en base64. Las dos consultas siguientes son contenidos alternativos de ese archivo. Las mostramos como bloques YAML legibles sql: |, no como campos adicionales de Library.
Esto es lo que entiendo por ValueSets de primera clase: la consulta declara explícitamente la terminología que necesita, junto a sus vistas de datos. El runner — el software que ejecuta la consulta — resuelve las dependencias. SQL utiliza sus nombres locales en lugar de resolver las URLs por sí mismo.
John describió la misma separación desde el lado del runner:
«Solo se está diciendo que tengo una dependencia de este value set. […] Para la abstracción SQL, queremos mantenerlo muy simple, especialmente para los casos sencillos.»
— John Grimes, 15 de septiembre (ligeramente editado)
Pertenencia, no un algoritmo de expansión
La segunda dependencia suministra la pertenencia a diabetes_codes mostrada al principio.
Owen Loveluck preguntó si la abstracción debería ser una vista en lugar de una tabla. Esta distinción importa: una relación no tiene por qué ser una tabla física. Puede ser una vista, datos en caché o algo que se evalúa cuando se necesita. Como indiqué el 8 de septiembre:
«Podemos tratar el ValueSet pre-expandido como una relación. No nos importa cómo llegamos hasta ahí — ¿es una consulta de expansión dinámica o datos pre-expandidos cargados en la base de datos? Pero puedo hacer un join y construir mi medida.»
— Nikolai Ryzhikov, 8 de septiembre (ligeramente editado)
La propuesta no estandariza cómo se evalúan ValueSet.compose, ECL o VCL. Describe cómo se pone a disposición de SQL la pertenencia resultante.
Opción A: unir el ValueSet como una relación
El runner expone diabetes_codes como una relación con nombre. Nuestra consulta hace un join contra ella:
# 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]
Opción B: comprobar la pertenencia con una función
La misma dependencia podría estar disponible a través de 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é cambia entre las dos opciones?
Ambas consultas devuelven p1 y p2. Lo que difiere es cómo escribimos la consulta y qué más permite hacer la interfaz.
Se trata de SQL ordinario. La pertenencia es visible: podemos inspeccionar los códigos, contarlos o incluir display en nuestra salida. Si la cohorte es incorrecta, podemos examinar ambas entradas.
Las propiedades adicionales también pueden ser columnas. Gino Canessa señaló esto al hablar de códigos que no son seleccionables:
«Si "no seleccionable" es una propiedad que le importa, la convierte en una columna durante la extracción y entonces tiene acceso a ella directamente en SQL. Las funciones no permiten eso tan fácilmente.»
— Gino Canessa, 15 de septiembre (ligeramente editado)
El coste es un join multi-columna repetido. El borrador también necesita una regla clara de unicidad para (system, version, code): las filas de pertenencia duplicadas pueden multiplicar los resultados de la consulta. DISTINCT protege esta lista de pacientes, pero no todos los agregados que alguien podría escribir más adelante.
La función es concisa, encaja en expresiones booleanas y no multiplica las filas de entrada. Su implementación podría usar una búsqueda eficiente en lugar de un join. Cuál rinde mejor depende del motor.
La contrapartida es la portabilidad: member_of es una función SQL on FHIR propuesta, no una función SQL estándar. Los motores necesitan soportarla o traducirla. Responde a una pregunta de pertenencia, pero no expone los miembros ni devuelve su texto de visualización.
Si un runner soporta ambas interfaces, deberían coincidir en la pertenencia para las mismas entradas y el mismo snapshot de terminología. Nuestras dos consultas deberían devolver los mismos pacientes.
Un runner también necesita declarar si soporta la relación, la función o ambas. Que los resultados de pertenencia coincidan no hace por sí solo que una consulta sea portable.
Ambas opciones necesitan reglas claras de versión
En el ejemplo hay dos versiones distintas: diabetes|2026 identifica la definición del ValueSet, mientras que cada fila de pertenencia lleva una versión del CodeSystem. Fijar solo el ValueSet no fija necesariamente todas las dependencias de terminología utilizadas para expandirlo.
Esto me importa porque en su momento rompimos la validación en Aidbox usando «latest». El validador tomó los estados de encounter modificados de R5, y la validación falló en servidores R4 en producción. Desde entonces fijamos todo lo que podemos y actualizamos las versiones de forma deliberada.
Para nuestra consulta, el riesgo es que la lista de pacientes cambie aunque el SQL no haya cambiado. Si dos runners resuelven una dependencia sin versionar de forma diferente, pueden devolver respuestas distintas. Owen planteó exactamente esa preocupación en la reunión.
El borrador propone pertenencia consistente para cada trabajo, registrando las versiones resueltas y rechazando la resolución ambigua. Sin embargo, cómo deben funcionar las dependencias sin versionar sigue siendo una cuestión abierta. Nuestro ejemplo evita deliberadamente esa ambigüedad; una implementación necesita tratarla de forma explícita, no suponerla en silencio.
Implementar, experimentar y después decidir
El grupo de trabajo se inclina actualmente por la relación como línea base, con la función como conveniencia opcional. Es una orientación, no una decisión. Ambas opciones tienen propiedades útiles, y necesitamos probarlas con consultas reales.
John Grimes ofreció un próximo paso práctico:
«Creo que deberíamos crear una rama y empezar a desarrollar cosas en la especificación y en la implementación de referencia, solo para hacernos una idea.»
— John Grimes, 15 de septiembre (muletillas eliminadas)
Comenzaremos con consultas como esta y comprobaremos tres aspectos: si las interfaces devuelven los mismos pacientes, cómo gestionan las versiones, y qué facilidad tienen de implementar y usar. También necesitamos probar propiedades adicionales y medir el rendimiento. Entonces podremos decidir qué debe soportar todo runner.
El objetivo inmediato es sencillo: una consulta debe poder decir dependo de este ValueSet. El experimento debe ayudarnos a decidir cómo los runners lo exponen a SQL y qué debe soportar toda implementación.
Únase a la discusión sobre Terminology en SQL on FHIR en Zulip, y siga las reuniones del grupo de trabajo de SQL on FHIR. Traiga una consulta que necesite ejecutar, un ValueSet que sea difícil de gestionar, o experiencia implementando alguno de los dos enfoques — esos ejemplos nos ayudarán a probar las opciones y decidir.
Pruebe SQL on FHIR y la terminología usted mismo
En Health Samurai, desarrollamos ambos lados de esto: Aidbox para trabajar con datos FHIR y SQL on FHIR, y Termbox para terminología FHIR. Son tecnologías que desarrollamos y utilizamos nosotros mismos — y que llevamos de vuelta al grupo de trabajo como experiencia práctica, no solo como ideas de diseño.
Puede probar ambas de forma gratuita. Explore SQL on FHIR en Aidbox, y utilice Termbox para trabajar con terminología y expansiones de ValueSet. Comience con sus propios datos y un ValueSet que use realmente; esa es una prueba mejor que cualquier demostración.
La interfaz de ValueSet de primera clase que se discute aquí es todavía una propuesta. Las pruebas le permiten explorar las capacidades actuales de los dos productos, no una versión ya publicada de esta propuesta.
Basado en las discusiones del grupo de trabajo de SQL on FHIR del 1, 8 y 15 de septiembre de 2026, y en mi borrador de abstracción de ValueSet. Los fragmentos de las reuniones han sido ligeramente editados para mejorar la legibilidad.
Véase también: Reuniones del WG de SQL on FHIR.


![Terminology is fun: CodeableConcept.coding[]](/assets/images/articles/horizontal-3.avif?v=2nw7.muc)

