Getting Started: SQL on FHIR (preview)
Description
29 cells · updated May 11, 2025
SQL on FHIR is an upcoming standard that provides a generic way to convert FHIR resources into a flat tables.
Let's import some synthetic data for this demonstration.
POST /fhir/$load
Accept: text/yaml
Content-Type: text/yaml
source: 'https://storage.googleapis.com/aidbox-public/synthea/100/all.ndjson.gz'The most important part of the SQL on FHIR standard is the ViewDefinition resource. View Definitions allow users to define flattened views of FHIR data that are portable between systems.
name is the name of the column in the resulting table.
expr is the FhirPath expression defining the data for this column.
forEach expression convert arrays into a set of rows.
Let's create a view using a ViewDefinition resource
PUT /ViewDefinition/patient_demo_view
Content-Type: text/yaml
Accept: text/yaml
name: patient_demo_view
status: active
resource: Patient
select:
- column:
- name: patient_id
path: id
- name: gender
path: gender
select:
- forEach: name
column:
- name: given
path: "given.join(' ')"
- name: family
path: "family"name: patient_demo_view
select:
- column:
- {name: patient_id, path: id}
- {name: gender, path: gender}
select:
- column:
- {name: given, path: given.join(' ')}
- {name: family, path: family}
forEach: name
status: active
resource: patient
id: becb0608-4469-4c09-9990-01972fef5304
resourceType: ViewDefinition
meta: {lastUpdated: '2024-02-05T14:31:02.408994Z', createdAt: '2024-02-05T14:31:02.408994Z', versionId: '936'}
Now the View has been created. We can access it using the usual SQL queries
select * from sof.patient_demo_view
limit 10| patient_id | gender |
|---|---|
| pt-1 | |
| pt-2 | |
| 03cb8799-bfbd-40fa-9ea8-96114cf1fec1 | male |
| 08b9a2bf-7492-4d15-b97f-00b54a3b35ee | female |
| 0be40874-5ae1-46e0-b549-1b5e17c58518 | female |
| 179adc51-6875-4191-ad16-160339ed45cc | female |
| 0e57e58d-8721-44ab-834d-b09cefd6a76b | male |
| 28572f59-c778-4d3e-91ad-c5b2488ebc1d | male |
| 0ec5c51b-56d4-433e-a320-297faf061237 | female |
| 102e001e-df1b-4fa8-adfe-6e2acb69aae8 | male |
Compare with the equivalent SQL without using SQL on FHIR
SELECT jsonb_path_query_first(r.resource, '$ . id') #>> '{}' as "patient_id",
jsonb_path_query_first(r.resource, '$ . gender') #>> '{}' as "gender"
FROM (SELECT (resource || jsonb_build_object('id', id)) as resource,
ts,
cts,
status
FROM "patient") as r
LIMIT 10| patient_id | gender |
|---|---|
| pt-1 | |
| pt-2 | |
| 03cb8799-bfbd-40fa-9ea8-96114cf1fec1 | male |
| 08b9a2bf-7492-4d15-b97f-00b54a3b35ee | female |
| 0be40874-5ae1-46e0-b549-1b5e17c58518 | female |
| 179adc51-6875-4191-ad16-160339ed45cc | female |
| 0e57e58d-8721-44ab-834d-b09cefd6a76b | male |
| 28572f59-c778-4d3e-91ad-c5b2488ebc1d | male |
| 0ec5c51b-56d4-433e-a320-297faf061237 | female |
| 102e001e-df1b-4fa8-adfe-6e2acb69aae8 | male |
And create a somewhat more complex view, which extracts condition data and patient_id foreign key.
PUT /fhir/ViewDefinition/condition_demo_view
Content-Type: text/yaml
Accept: text/yaml
name: condition_demo_view
resource: Condition
status: active
select:
- column:
- name: condition_id
path: id
- name: onset
path: onset.dateTime
- name: abatement
path: abatement.dateTime
- name: status
path: clinicalStatus.coding.code.first()
- name: code
path: code.coding.where(system='http://snomed.info/sct').code.first()
- name: patient_id
path: subject.id
name: condition_demo_view
select:
- column:
- {name: condition_id, path: id}
- {name: onset, path: onset.dateTime}
- {name: abatement, path: abatement.dateTime}
- {name: status, path: clinicalStatus.coding.code.first()}
- {name: code, path: 'code.coding.where(system=''http://snomed.info/sct'').code.first()'}
- {name: patient_id, path: subject.id}
status: active
resource: condition
id: deb361b1-2f6d-488e-a80d-aed6c0f4f1b7
resourceType: ViewDefinition
meta: {lastUpdated: '2024-02-01T17:06:44.315877Z', createdAt: '2024-02-01T17:06:44.315877Z', versionId: '908'}
As with Patient view, we can use the Condition view in SQL queries.
select * from sof.condition_demo_view
limit 10;| code | onset | status | abatement | patient_id | condition_id |
|---|---|---|---|---|---|
| 840539006 | 2022-10-26 | remission | 2022-11-15 | pt-1 | cond-1 |
| 840539006 | 2021-10-26 | remission | 2021-11-15 | pt-2 | cond-2 |
| 424132000 | 1999-12-10T23:48:18+03:00 | active | b262c1ba-d4da-4791-877c-c69a128f0c2b | 00633c33-a296-4eaa-a169-3b4b1e31f7a5 | |
| 88805009 | 2014-06-26T12:50:13+04:00 | active | 38ad2730-be38-4e9a-97a0-2b97c8574814 | 00bb715a-d112-4843-befd-837fc5445e27 | |
| 162864005 | 1987-03-16T12:35:17+03:00 | active | 83991f61-aff5-4ae6-8559-3ef100c931a7 | 0119106a-365f-49a2-8bf5-b5ce8df6f2f3 | |
| 444814009 | 2016-08-06T02:13:33+03:00 | resolved | 2016-08-13T02:13:33+03:00 | 5e23837b-8ef5-46cd-9046-82cd4033bdb3 | 011b6e34-80c1-473e-9c0e-38ac003a404f |
| 195662009 | 2016-05-27T13:44:17+03:00 | resolved | 2016-06-03T13:44:17+03:00 | 93f09189-7ec2-4d0e-9310-f14efce8e4b6 | 014774ea-8dac-46c0-a9b7-1554613d8625 |
| 195662009 | 2003-02-14T18:25:00+03:00 | resolved | 2003-02-24T18:25:00+03:00 | 44d86263-d939-4dff-8ce7-a5a86d70c348 | 02116b05-4f06-4eb8-bc02-63380e64ebc9 |
| 10509002 | 2019-03-30T08:53:34+03:00 | resolved | 2019-04-06T08:53:34+03:00 | 41907da4-2926-4f7a-9326-bb18585b07bf | 0287a9bc-ae77-4763-9282-39d4799e2b85 |
| 43878008 | 2013-07-04T14:17:52+04:00 | resolved | 2013-07-16T14:17:52+04:00 | 5bad6369-e2f1-41d3-aae2-daf1d603cb50 | 02a79009-e719-43b5-9aad-ae68243066c8 |
Compare with equivalent SQL without using SQL on FHIR
SELECT
cond.id as condition_id,
jsonb_path_query_first(cond.resource, '$.onset.dateTime') #>> '{}' AS onset,
jsonb_path_query_first(cond.resource, '$.abatement.dateTime') #>> '{}' AS abatement,
jsonb_path_query_first(cond.resource, '$.clinicalStatus.coding.code[0]') #>> '{}' AS status,
jsonb_path_query_first(cond.resource, '$.code.coding ? (@.system == "http://snomed.info/sct").code[0]') #>> '{}' AS code,
jsonb_path_query_first(cond.resource, '$.subject.id') #>> '{}' AS patient_id
FROM Condition AS cond
limit 10| code | onset | status | abatement | patient_id | condition_id |
|---|---|---|---|---|---|
| 840539006 | 2022-10-26 | remission | 2022-11-15 | pt-1 | cond-1 |
| 840539006 | 2021-10-26 | remission | 2021-11-15 | pt-2 | cond-2 |
| 424132000 | 1999-12-10T23:48:18+03:00 | active | b262c1ba-d4da-4791-877c-c69a128f0c2b | 00633c33-a296-4eaa-a169-3b4b1e31f7a5 | |
| 88805009 | 2014-06-26T12:50:13+04:00 | active | 38ad2730-be38-4e9a-97a0-2b97c8574814 | 00bb715a-d112-4843-befd-837fc5445e27 | |
| 162864005 | 1987-03-16T12:35:17+03:00 | active | 83991f61-aff5-4ae6-8559-3ef100c931a7 | 0119106a-365f-49a2-8bf5-b5ce8df6f2f3 | |
| 444814009 | 2016-08-06T02:13:33+03:00 | resolved | 2016-08-13T02:13:33+03:00 | 5e23837b-8ef5-46cd-9046-82cd4033bdb3 | 011b6e34-80c1-473e-9c0e-38ac003a404f |
| 195662009 | 2016-05-27T13:44:17+03:00 | resolved | 2016-06-03T13:44:17+03:00 | 93f09189-7ec2-4d0e-9310-f14efce8e4b6 | 014774ea-8dac-46c0-a9b7-1554613d8625 |
| 195662009 | 2003-02-14T18:25:00+03:00 | resolved | 2003-02-24T18:25:00+03:00 | 44d86263-d939-4dff-8ce7-a5a86d70c348 | 02116b05-4f06-4eb8-bc02-63380e64ebc9 |
| 10509002 | 2019-03-30T08:53:34+03:00 | resolved | 2019-04-06T08:53:34+03:00 | 41907da4-2926-4f7a-9326-bb18585b07bf | 0287a9bc-ae77-4763-9282-39d4799e2b85 |
| 43878008 | 2013-07-04T14:17:52+04:00 | resolved | 2013-07-16T14:17:52+04:00 | 5bad6369-e2f1-41d3-aae2-daf1d603cb50 | 02a79009-e719-43b5-9aad-ae68243066c8 |
And use joins to find patients with specific conditions.
In this example we find patients which recovered from COVID-19 in the last year.
SELECT *
FROM sof.patient_demo_view pt
JOIN sof.condition_demo_view cond using (patient_id)
WHERE code = '840539006' and max_text_date_bound(cond.abatement) > (NOW() - interval '1 year')Compare with the equivalent SQL without using SQL on FHIR
SELECT
pt.id AS patient_id,
array_to_string(
(
SELECT array_agg(x)
FROM jsonb_array_elements_text(
jsonb_path_query_array(name.element, '$.given[*]')
) AS x
), ' ') AS given,
jsonb_path_query_first(name.element, '$.family') #>> '{}' AS family,
jsonb_path_query_first(pt.resource, '$.gender') #>> '{}' AS gender,
cond.id as condition_id,
jsonb_path_query_first(cond.resource, '$.onset.dateTime') #>> '{}' AS onset,
jsonb_path_query_first(cond.resource, '$.abatement.dateTime') #>> '{}' AS abatement,
jsonb_path_query_first(cond.resource, '$.clinicalStatus.coding.code[0]') #>> '{}' AS status,
jsonb_path_query_first(cond.resource, '$.code.coding ? (@.system == "http://snomed.info/sct").code[0]') #>> '{}' AS code
FROM Patient AS pt
JOIN Condition cond ON jsonb_path_query_first(cond.resource, '$.subject.id') #>> '{}' = pt.id
JOIN LATERAL jsonb_path_query(pt.resource, '$.name [*]') name(element) ON true
WHERE
jsonb_path_query_first(cond.resource, '$.code.coding ? (@.system == "http://snomed.info/sct").code[0]') #>> '{}' = '840539006'
AND max_text_date_bound(jsonb_path_query_first(cond.resource, '$.abatement.dateTime') #>> '{}') > (NOW() - interval '1 year')| patient_id | onset | given | condition_id | family | abatement | status | code | gender |
|---|---|---|---|---|---|---|---|---|
| pt-1 | 2022-10-26 | John M | cond-1 | Smith | 2022-11-15 | remission | 840539006 | male |
| pt-1 | 2022-10-26 | Robert A | cond-1 | Doe | 2022-11-15 | remission | 840539006 | male |
More complex examples
Let's find the most popular conditon for several specific patient cohorts.
PUT /ViewDefinition/condition
content-type: text/yaml
accept: text/yaml
name: condition
resource: Condition
status: active
select:
- column:
- name: condition_id
path: id
- name: patient_id
path: subject.getId()
- name: onset
path: onset.dateTime
- name: abatement
path: abatement.dateTime
- forEach: code.coding
column:
- name: code
path: code
- name: system
path: system
- name: display
path: display
name: condition
select:
- column:
- {name: condition_id, path: id}
- {name: patient_id, path: subject.getId()}
- {name: onset, path: onset.dateTime}
- {name: abatement, path: abatement.dateTime}
- column:
- {name: code, path: code}
- {name: system, path: system}
- {name: display, path: display}
forEach: code.coding
status: active
resource: Condition
id: 375f8fcf-ca4e-4e56-b161-fd795e1c2149
resourceType: ViewDefinition
meta: {lastUpdated: '2024-02-05T14:32:25.110123Z', createdAt: '2024-02-05T14:32:25.110123Z', versionId: '940'}
select * from sof.condition
limit 10;| onset | abatement | patient_id | condition_id |
|---|---|---|---|
| 2022-10-26 | 2022-11-15 | pt-1 | cond-1 |
| 2021-10-26 | 2021-11-15 | pt-2 | cond-2 |
| 1999-12-10T23:48:18+03:00 | b262c1ba-d4da-4791-877c-c69a128f0c2b | 00633c33-a296-4eaa-a169-3b4b1e31f7a5 | |
| 2014-06-26T12:50:13+04:00 | 38ad2730-be38-4e9a-97a0-2b97c8574814 | 00bb715a-d112-4843-befd-837fc5445e27 | |
| 1987-03-16T12:35:17+03:00 | 83991f61-aff5-4ae6-8559-3ef100c931a7 | 0119106a-365f-49a2-8bf5-b5ce8df6f2f3 | |
| 2016-08-06T02:13:33+03:00 | 2016-08-13T02:13:33+03:00 | 5e23837b-8ef5-46cd-9046-82cd4033bdb3 | 011b6e34-80c1-473e-9c0e-38ac003a404f |
| 2016-05-27T13:44:17+03:00 | 2016-06-03T13:44:17+03:00 | 93f09189-7ec2-4d0e-9310-f14efce8e4b6 | 014774ea-8dac-46c0-a9b7-1554613d8625 |
| 2003-02-14T18:25:00+03:00 | 2003-02-24T18:25:00+03:00 | 44d86263-d939-4dff-8ce7-a5a86d70c348 | 02116b05-4f06-4eb8-bc02-63380e64ebc9 |
| 2019-03-30T08:53:34+03:00 | 2019-04-06T08:53:34+03:00 | 41907da4-2926-4f7a-9326-bb18585b07bf | 0287a9bc-ae77-4763-9282-39d4799e2b85 |
| 2013-07-04T14:17:52+04:00 | 2013-07-16T14:17:52+04:00 | 5bad6369-e2f1-41d3-aae2-daf1d603cb50 | 02a79009-e719-43b5-9aad-ae68243066c8 |
PUT /ViewDefinition/patient
content-type: text/yaml
accept: text/yaml
name: patient
resource: Patient
status: active
select:
- column:
- name: patient_id
path: id
- path: name.first().family
name: family
- name: given
path: name.first().given.join(' ')
- name: dob
path: birthDate
- path: identifier.where(system = 'http://hl7.org/fhir/sid/us-ssn').value
name: patient_ssnname: patient
select:
- column:
- {name: patient_id, path: id}
- {name: family, path: name.first().family}
- {name: given, path: name.first().given.join(' ')}
- {name: dob, path: birthDate}
- {name: patient_ssn, path: 'identifier.where(system = ''http://hl7.org/fhir/sid/us-ssn'').value'}
status: active
resource: Patient
id: 99d8e7ce-c706-44f1-a551-f52044cd946b
resourceType: ViewDefinition
meta: {lastUpdated: '2024-02-05T14:32:32.074425Z', createdAt: '2024-02-05T14:32:32.074425Z', versionId: '942'}
select * from sof.patient
limit 10| given | family | patient_id | dob | patient_ssn |
|---|---|---|---|---|
| John M | Doe | pt-1 | ||
| Jane M | Doe | pt-2 | ||
| Calvin845 | DuBuque211 | 03cb8799-bfbd-40fa-9ea8-96114cf1fec1 | 2012-10-09 | 999-94-5813 |
| Esperanza675 | Hernandes724 | 08b9a2bf-7492-4d15-b97f-00b54a3b35ee | 1962-06-22 | 999-20-3208 |
| Kaye394 | Wilkinson796 | 0be40874-5ae1-46e0-b549-1b5e17c58518 | 1988-02-27 | 999-37-9960 |
| Kristina583 | Tromp100 | 179adc51-6875-4191-ad16-160339ed45cc | 2011-12-12 | 999-96-1646 |
| John539 | Windler79 | 0e57e58d-8721-44ab-834d-b09cefd6a76b | 1956-09-06 | 999-11-6992 |
| Irvin970 | Gaylord332 | 28572f59-c778-4d3e-91ad-c5b2488ebc1d | 2015-01-08 | 999-72-5260 |
| Somer38 | Nicolas769 | 0ec5c51b-56d4-433e-a320-297faf061237 | 1936-10-27 | 999-24-8659 |
| Hollis7 | Ortiz186 | 102e001e-df1b-4fa8-adfe-6e2acb69aae8 | 1944-01-08 | 999-56-2680 |
-- Find most popular conditon for specific patient cohort
with patient_cohort as (
select
pt.patient_id patient_id,
floor(DATE_PART('YEAR', AGE(NOW(), pt.dob::date))/10)*10 min_age
from sof.patient pt
),
patient_ages as (
select distinct min_age
from patient_cohort
),
patient_condition as (
select
pc.min_age min_age,
cond.code code,
count(cond.code) amount
from patient_cohort pc
join sof.condition cond using (patient_id)
group by pc.min_age, cond.code
)
select min_age,
(select pc2.amount from patient_condition pc2
where pc2.min_age = pc.min_age
order by pc2.amount desc
limit 1) amount,
(select pc2.code from patient_condition pc2
where pc2.min_age = pc.min_age
order by pc2.amount desc
limit 1) code
from patient_ages pc
order by pc.min_age desc| code | amount | min_age |
|---|---|---|
| 444814009 | 14 | 100 |
| 444814009 | 22 | 80 |
| 444814009 | 11 | 70 |
| 444814009 | 12 | 60 |
| 444814009 | 14 | 50 |
| 444814009 | 11 | 40 |
| 72892002 | 18 | 30 |
| 444814009 | 11 | 20 |
| 65363002 | 14 | 10 |
| 65363002 | 9 | 0 |
The same views allow us to find all patients who were born in or after 1970 and were diagnosed with Acute bronchitis in or after 2018.
select pt.given,
pt.dob,
cond.onset
from sof.patient pt
join sof.condition cond on cond.patient_id = pt.patient_id
where pt.dob > '1970-01-01'
and cond.code = '10509002'
and cond.system = 'http://snomed.info/sct'
and cond.onset > '2018-01-01'
limit 100;| dob | given | onset |
|---|---|---|
| 1978-05-31 | Frank378 | 2019-01-05T11:23:03+03:00 |
| 2012-10-09 | Calvin845 | 2019-01-17T05:34:59+03:00 |
| 1997-06-15 | Emmett200 | 2018-09-13T21:32:43+03:00 |
| 2016-02-24 | Leigh689 | 2019-02-10T14:36:57+03:00 |