For AI agents: the documentation index is at /docs/aidbox/llms.txt. A Markdown version of this page is available at /docs/aidbox/notebooks.md or by requesting it with the Accept: text/markdown header.
Aidbox Docs
All notebooks

Getting Started: SQL on FHIR (preview)

Description

29 cells · updated May 11, 2025

This is a read-only view. Responses shown were saved when the notebook was published — open it in your own Aidbox to run the cells against live data.

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.

REST Send Cells run only inside Aidbox. Use Run in Aidbox above to open this notebook in your own instance.
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

REST Send Cells run only inside Aidbox. Use Run in Aidbox above to open this notebook in your own instance.
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"
Response: Body
Status: 201
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

SQL Send Cells run only inside Aidbox. Use Run in Aidbox above to open this notebook in your own instance.
select * from sof.patient_demo_view
limit 10
Result
patient_idgender
pt-1
pt-2
03cb8799-bfbd-40fa-9ea8-96114cf1fec1male
08b9a2bf-7492-4d15-b97f-00b54a3b35eefemale
0be40874-5ae1-46e0-b549-1b5e17c58518female
179adc51-6875-4191-ad16-160339ed45ccfemale
0e57e58d-8721-44ab-834d-b09cefd6a76bmale
28572f59-c778-4d3e-91ad-c5b2488ebc1dmale
0ec5c51b-56d4-433e-a320-297faf061237female
102e001e-df1b-4fa8-adfe-6e2acb69aae8male

Compare with the equivalent SQL without using SQL on FHIR

SQL Send Cells run only inside Aidbox. Use Run in Aidbox above to open this notebook in your own instance.
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
Result
patient_idgender
pt-1
pt-2
03cb8799-bfbd-40fa-9ea8-96114cf1fec1male
08b9a2bf-7492-4d15-b97f-00b54a3b35eefemale
0be40874-5ae1-46e0-b549-1b5e17c58518female
179adc51-6875-4191-ad16-160339ed45ccfemale
0e57e58d-8721-44ab-834d-b09cefd6a76bmale
28572f59-c778-4d3e-91ad-c5b2488ebc1dmale
0ec5c51b-56d4-433e-a320-297faf061237female
102e001e-df1b-4fa8-adfe-6e2acb69aae8male

And create a somewhat more complex view, which extracts condition data and patient_id foreign key.

REST Send Cells run only inside Aidbox. Use Run in Aidbox above to open this notebook in your own instance.
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
Response: Body
Status: 201
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.

SQL Send Cells run only inside Aidbox. Use Run in Aidbox above to open this notebook in your own instance.
select * from sof.condition_demo_view
limit 10;
Result
codeonsetstatusabatementpatient_idcondition_id
8405390062022-10-26remission2022-11-15pt-1cond-1
8405390062021-10-26remission2021-11-15pt-2cond-2
4241320001999-12-10T23:48:18+03:00activeb262c1ba-d4da-4791-877c-c69a128f0c2b00633c33-a296-4eaa-a169-3b4b1e31f7a5
888050092014-06-26T12:50:13+04:00active38ad2730-be38-4e9a-97a0-2b97c857481400bb715a-d112-4843-befd-837fc5445e27
1628640051987-03-16T12:35:17+03:00active83991f61-aff5-4ae6-8559-3ef100c931a70119106a-365f-49a2-8bf5-b5ce8df6f2f3
4448140092016-08-06T02:13:33+03:00resolved2016-08-13T02:13:33+03:005e23837b-8ef5-46cd-9046-82cd4033bdb3011b6e34-80c1-473e-9c0e-38ac003a404f
1956620092016-05-27T13:44:17+03:00resolved2016-06-03T13:44:17+03:0093f09189-7ec2-4d0e-9310-f14efce8e4b6014774ea-8dac-46c0-a9b7-1554613d8625
1956620092003-02-14T18:25:00+03:00resolved2003-02-24T18:25:00+03:0044d86263-d939-4dff-8ce7-a5a86d70c34802116b05-4f06-4eb8-bc02-63380e64ebc9
105090022019-03-30T08:53:34+03:00resolved2019-04-06T08:53:34+03:0041907da4-2926-4f7a-9326-bb18585b07bf0287a9bc-ae77-4763-9282-39d4799e2b85
438780082013-07-04T14:17:52+04:00resolved2013-07-16T14:17:52+04:005bad6369-e2f1-41d3-aae2-daf1d603cb5002a79009-e719-43b5-9aad-ae68243066c8

Compare with equivalent SQL without using SQL on FHIR

SQL Send Cells run only inside Aidbox. Use Run in Aidbox above to open this notebook in your own instance.
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
Result
codeonsetstatusabatementpatient_idcondition_id
8405390062022-10-26remission2022-11-15pt-1cond-1
8405390062021-10-26remission2021-11-15pt-2cond-2
4241320001999-12-10T23:48:18+03:00activeb262c1ba-d4da-4791-877c-c69a128f0c2b00633c33-a296-4eaa-a169-3b4b1e31f7a5
888050092014-06-26T12:50:13+04:00active38ad2730-be38-4e9a-97a0-2b97c857481400bb715a-d112-4843-befd-837fc5445e27
1628640051987-03-16T12:35:17+03:00active83991f61-aff5-4ae6-8559-3ef100c931a70119106a-365f-49a2-8bf5-b5ce8df6f2f3
4448140092016-08-06T02:13:33+03:00resolved2016-08-13T02:13:33+03:005e23837b-8ef5-46cd-9046-82cd4033bdb3011b6e34-80c1-473e-9c0e-38ac003a404f
1956620092016-05-27T13:44:17+03:00resolved2016-06-03T13:44:17+03:0093f09189-7ec2-4d0e-9310-f14efce8e4b6014774ea-8dac-46c0-a9b7-1554613d8625
1956620092003-02-14T18:25:00+03:00resolved2003-02-24T18:25:00+03:0044d86263-d939-4dff-8ce7-a5a86d70c34802116b05-4f06-4eb8-bc02-63380e64ebc9
105090022019-03-30T08:53:34+03:00resolved2019-04-06T08:53:34+03:0041907da4-2926-4f7a-9326-bb18585b07bf0287a9bc-ae77-4763-9282-39d4799e2b85
438780082013-07-04T14:17:52+04:00resolved2013-07-16T14:17:52+04:005bad6369-e2f1-41d3-aae2-daf1d603cb5002a79009-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.

SQL Send Cells run only inside Aidbox. Use Run in Aidbox above to open this notebook in your own instance.
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

SQL Send Cells run only inside Aidbox. Use Run in Aidbox above to open this notebook in your own instance.
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')
Result
patient_idonsetgivencondition_idfamilyabatementstatuscodegender
pt-12022-10-26John Mcond-1Smith2022-11-15remission840539006male
pt-12022-10-26Robert Acond-1Doe2022-11-15remission840539006male

More complex examples

Let's find the most popular conditon for several specific patient cohorts.

REST Send Cells run only inside Aidbox. Use Run in Aidbox above to open this notebook in your own instance.
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
Response: Body
Status: 201
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'}
SQL Send Cells run only inside Aidbox. Use Run in Aidbox above to open this notebook in your own instance.
select * from sof.condition
limit 10;
Result
onsetabatementpatient_idcondition_id
2022-10-262022-11-15pt-1cond-1
2021-10-262021-11-15pt-2cond-2
1999-12-10T23:48:18+03:00b262c1ba-d4da-4791-877c-c69a128f0c2b00633c33-a296-4eaa-a169-3b4b1e31f7a5
2014-06-26T12:50:13+04:0038ad2730-be38-4e9a-97a0-2b97c857481400bb715a-d112-4843-befd-837fc5445e27
1987-03-16T12:35:17+03:0083991f61-aff5-4ae6-8559-3ef100c931a70119106a-365f-49a2-8bf5-b5ce8df6f2f3
2016-08-06T02:13:33+03:002016-08-13T02:13:33+03:005e23837b-8ef5-46cd-9046-82cd4033bdb3011b6e34-80c1-473e-9c0e-38ac003a404f
2016-05-27T13:44:17+03:002016-06-03T13:44:17+03:0093f09189-7ec2-4d0e-9310-f14efce8e4b6014774ea-8dac-46c0-a9b7-1554613d8625
2003-02-14T18:25:00+03:002003-02-24T18:25:00+03:0044d86263-d939-4dff-8ce7-a5a86d70c34802116b05-4f06-4eb8-bc02-63380e64ebc9
2019-03-30T08:53:34+03:002019-04-06T08:53:34+03:0041907da4-2926-4f7a-9326-bb18585b07bf0287a9bc-ae77-4763-9282-39d4799e2b85
2013-07-04T14:17:52+04:002013-07-16T14:17:52+04:005bad6369-e2f1-41d3-aae2-daf1d603cb5002a79009-e719-43b5-9aad-ae68243066c8
REST Send Cells run only inside Aidbox. Use Run in Aidbox above to open this notebook in your own instance.
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_ssn
Response: Body
Status: 201
name: 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'}
SQL Send Cells run only inside Aidbox. Use Run in Aidbox above to open this notebook in your own instance.
select * from sof.patient
limit 10
Result
givenfamilypatient_iddobpatient_ssn
John MDoept-1
Jane MDoept-2
Calvin845DuBuque21103cb8799-bfbd-40fa-9ea8-96114cf1fec12012-10-09999-94-5813
Esperanza675Hernandes72408b9a2bf-7492-4d15-b97f-00b54a3b35ee1962-06-22999-20-3208
Kaye394Wilkinson7960be40874-5ae1-46e0-b549-1b5e17c585181988-02-27999-37-9960
Kristina583Tromp100179adc51-6875-4191-ad16-160339ed45cc2011-12-12999-96-1646
John539Windler790e57e58d-8721-44ab-834d-b09cefd6a76b1956-09-06999-11-6992
Irvin970Gaylord33228572f59-c778-4d3e-91ad-c5b2488ebc1d2015-01-08999-72-5260
Somer38Nicolas7690ec5c51b-56d4-433e-a320-297faf0612371936-10-27999-24-8659
Hollis7Ortiz186102e001e-df1b-4fa8-adfe-6e2acb69aae81944-01-08999-56-2680
SQL Send Cells run only inside Aidbox. Use Run in Aidbox above to open this notebook in your own instance.
-- 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
Result
codeamountmin_age
44481400914100
4448140092280
4448140091170
4448140091260
4448140091450
4448140091140
728920021830
4448140091120
653630021410
6536300290

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.

SQL Send Cells run only inside Aidbox. Use Run in Aidbox above to open this notebook in your own instance.
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;
Result
dobgivenonset
1978-05-31Frank3782019-01-05T11:23:03+03:00
2012-10-09Calvin8452019-01-17T05:34:59+03:00
1997-06-15Emmett2002018-09-13T21:32:43+03:00
2016-02-24Leigh6892019-02-10T14:36:57+03:00