Some medical queries are too complex for relational databases and data warehouses. A graph database is the right tool for mapping data coded to different libraries and canonical models. Take as a query: “Find every patient with a diabetes subtype who is on an ACE inhibitor and has stage 3 or worse kidney disease, where the kidney disease developed after the diabetes.”
A graph database can answer that question as a single query. First it traverses the SNOMED concept hierarchy to find every diabetes subtype, matches the prescribing relationship to ACE inhibitors, filters by CKD stage using the same hierarchy traversal, and applies a date ordering constraint on the relationship. The query reads like the clinical question it came from, and adding or changing a condition in the logic means editing one line, not restructuring a pipeline. Something that is not possible in Palantir.
Storing patient data as a graph also unlocks Graph RAG, a step change over standard AI retrieval. Conventional RAG finds context for an LLM by vector similarity: it embeds documents and fetches the closest matches. That works for unstructured text but breaks down for clinical questions, where the right answer depends on traversing connected facts: a patient’s conditions, their position in the SNOMED hierarchy, their medications, applicable interaction rules, and the order events occurred.
Graph RAG replaces vector lookup with graph traversal: it assembles the relevant subgraph for a question and passes that structured, verifiable context to the LLM. The model then reasons over precise clinical relationships rather than probabilistically similar text. A clinician asking “why is this prescription flagged?” gets back an answer with the full evidence chain made explicit and auditable. That is only possible if the data was stored as a graph in the first place.
Why Relational Databases Struggle Here
Snowflake is excellent for aggregations, throughput analytics, and wide flat tables. It is the wrong engine for three specific problems that recur constantly in primary care data.
SNOMED hierarchy traversal. SNOMED CT has 350,000+ concepts linked by 1.3 million IS_A relationships. GPs code at different specificity levels — one practice records “Type 2 diabetes mellitus”, another records “Insulin-treated Type 2 diabetes mellitus”. A query for any diabetic patient must traverse that hierarchy. In Snowflake, this means a recursive CTE with an arbitrary depth cap (silently missing results if the hierarchy is deeper than expected), or a pre-materialised ancestor table that must be rebuilt every time SNOMED releases twice-yearly updates. In Neo4j, it is [:IS_A*0..] — one expression, no cap, always current.
-- Snowflake: hierarchy traversal requires a recursive CTE with a depth capWITH RECURSIVE snomed_ancestors AS ( SELECT child_code, parent_code, 1 AS depth FROM snomed_hierarchy WHERE child_code = '73211009' UNION ALL SELECT h.child_code, h.parent_code, a.depth + 1 FROM snomed_hierarchy h JOIN snomed_ancestors a ON h.parent_code = a.child_code WHERE a.depth < 10 -- miss results if hierarchy is deeper than this)SELECT DISTINCT p.patient_id FROM patients pJOIN patient_conditions pc ON p.patient_id = pc.patient_idJOIN snomed_ancestors sa ON pc.snomed_code = sa.child_code
Multi-hop pattern matching. Comorbidity and prescribing safety queries match patterns across a patient’s record simultaneously: conditions, medications, contraindication rules, drug interactions. Each relationship hop is a JOIN in Snowflake. By the time you have checked a condition, its SNOMED parent, the medications prescribed for it, and interactions with other current medications, you have a query that is long, hard to verify clinically, and fragile when rules change. In Cypher, the same question is a graph pattern that reads like the clinical specification it came from.
Temporal changes. The sequence and timing of clinical events matter. “Kidney disease developed after diabetes” is a temporal path query. In Snowflake it means SCD Type 2 tables, effective_from/effective_to columns, and QUALIFY ROW_NUMBER() window functions just to reconstruct point-in-time state — before you can even compare dates. In Neo4j, onset dates are properties on relationships, and “condition A preceded condition B by more than 12 months” is a path query with simple date arithmetic.
Graph RAG: Why This Matters for AI on Clinical Data: Connected Facts not Probabilistic Text Chunks
There is a second reason to build a graph layer over primary care data, and it is becoming more important quickly: Graph RAG.
Standard Retrieval-Augmented Generation (RAG) finds relevant context for an LLM by converting documents into vector embeddings and retrieving the closest matches by cosine similarity. For unstructured text, this works well. For structured clinical data it requires traversing conditions, SNOMED hierarchies, medications, interactions, and temporal history simultaneously. Vector similarity is the wrong retrieval mechanism. You cannot embed your way to a multi-hop answer.
Graph RAG replaces or augments the vector retrieval step with graph traversal. When a clinician or AI assistant asks a question about a patient, the graph is traversed to assemble the relevant subgraph including the patient’s conditions, their positions in the SNOMED hierarchy, their current medications, applicable contra-indications, and temporal context. That subgraph is passed as structured context to the LLM. The LLM then reasons over precise, connected facts rather than probabilistically similar text chunks.
The practical difference is significant. A vector-based RAG system retrieving from patient records might surface the most “similar” passages about diabetes and kidney disease. A Graph RAG system asks the graph: “Give me this patient’s complete condition graph, all medications, any contraindicated combinations, and the onset dates in clinical order” — and gets back a structured, verifiable answer. The LLM’s role becomes interpretation and communication, not fact retrieval.
For primary care data specifically, Graph RAG enables things that are otherwise difficult to build reliably:
- A clinical decision support assistant that explains why a prescription is flagged, with the full chain of evidence (patient condition → SNOMED parent → contraindication rule → medication) made explicit
- A population health query interface where a clinician asks in natural language and the system translates to Cypher, executes against the patient graph, and returns an auditable answer
- A pharmacovigilance tool that identifies emerging drug-condition signals by traversing interaction and comorbidity patterns across the full patient graph
Neo4j has native LangChain and LlamaIndex integrations that expose the graph directly to LLM pipelines. The Cypher queries in this article are also the retrieval queries you would use in a Graph RAG architecture — the graph structure that makes complex clinical queries tractable is exactly the structure that makes LLM reasoning over patient data reliable.
The Data Model
Before writing queries, we need a graph schema. Here is a minimal but realistic model for primary care data:
(Patient)-[:HAS_CONDITION]->(SnomedConcept)(Patient)-[:PRESCRIBED]->(Medication)(SnomedConcept)-[:IS_A]->(SnomedConcept) // SNOMED hierarchy(Medication)-[:INDICATED_FOR]->(SnomedConcept)(Medication)-[:CONTRAINDICATED_WITH]->(SnomedConcept)(Medication)-[:INTERACTS_WITH]->(Medication)(Patient)-[:REGISTERED_AT]->(Practice)
Each SnomedConcept node holds a SNOMED CT concept code and preferred term. The IS_A relationships encode the SNOMED hierarchy — crucially, they allow variable-depth traversal, which is where graph databases earn their keep.
Setting Up: Seeding Example Data
The following Cypher creates a small but representative dataset you can run directly in Neo4j Browser or Aura.
// ── SNOMED hierarchy (illustrative subset) ──────────────────────────────────CREATE (endo:SnomedConcept {code: '362969004', term: 'Disorder of endocrine system'})CREATE (dm:SnomedConcept {code: '73211009', term: 'Diabetes mellitus'})CREATE (t2dm:SnomedConcept {code: '44054006', term: 'Type 2 diabetes mellitus'})CREATE (t1dm:SnomedConcept {code: '46635009', term: 'Type 1 diabetes mellitus'})CREATE (dmckd:SnomedConcept{code: '445276001', term: 'Diabetes mellitus with chronic kidney disease'})CREATE (ckd:SnomedConcept {code: '709044004', term: 'Chronic kidney disease'})CREATE (ckd3:SnomedConcept {code: '433144002', term: 'Chronic kidney disease stage 3'})CREATE (htn:SnomedConcept {code: '38341003', term: 'Hypertension'})CREATE (hf:SnomedConcept {code: '84114007', term: 'Heart failure'})CREATE (af:SnomedConcept {code: '49436004', term: 'Atrial fibrillation'})// SNOMED IS-A hierarchyCREATE (dm)-[:IS_A]->(endo)CREATE (t2dm)-[:IS_A]->(dm)CREATE (t1dm)-[:IS_A]->(dm)CREATE (dmckd)-[:IS_A]->(t2dm)CREATE (dmckd)-[:IS_A]->(ckd)CREATE (ckd3)-[:IS_A]->(ckd)// ── Medications ──────────────────────────────────────────────────────────────CREATE (metformin:Medication {code: '372567009', name: 'Metformin', class: 'Biguanide'})CREATE (ramipril:Medication {code: '386872004', name: 'Ramipril', class: 'ACE inhibitor'})CREATE (atorvastatin:Medication{code:'373444002', name: 'Atorvastatin', class: 'Statin'})CREATE (warfarin:Medication {code: '372756006', name: 'Warfarin', class: 'Anticoagulant'})CREATE (ibuprofen:Medication {code: '387207008', name: 'Ibuprofen', class: 'NSAID'})CREATE (lisinopril:Medication {code: '386858003', name: 'Lisinopril', class: 'ACE inhibitor'})// IndicationsCREATE (metformin)-[:INDICATED_FOR]->(t2dm)CREATE (ramipril)-[:INDICATED_FOR]->(htn)CREATE (ramipril)-[:INDICATED_FOR]->(hf)CREATE (atorvastatin)-[:INDICATED_FOR]->(htn)CREATE (warfarin)-[:INDICATED_FOR]->(af)// ContraindicationsCREATE (ibuprofen)-[:CONTRAINDICATED_WITH]->(ckd)CREATE (metformin)-[:CONTRAINDICATED_WITH]->(ckd3)// Drug-drug interactionsCREATE (ibuprofen)-[:INTERACTS_WITH]->(warfarin)CREATE (ibuprofen)-[:INTERACTS_WITH]->(ramipril)// ── Patients ─────────────────────────────────────────────────────────────────CREATE (p1:Patient {id: 'P001', name: 'Alice', dob: '1958-03-12', gender: 'F'})CREATE (p2:Patient {id: 'P002', name: 'Brian', dob: '1962-07-04', gender: 'M'})CREATE (p3:Patient {id: 'P003', name: 'Carol', dob: '1971-11-22', gender: 'F'})CREATE (p4:Patient {id: 'P004', name: 'David', dob: '1955-01-30', gender: 'M'})CREATE (p5:Patient {id: 'P005', name: 'Evelyn', dob: '1948-09-15', gender: 'F'})// Diagnoses (patients coded with specific SNOMED concepts)CREATE (p1)-[:HAS_CONDITION {onset: '2015-06-01'}]->(t2dm)CREATE (p1)-[:HAS_CONDITION {onset: '2018-02-14'}]->(ckd3)CREATE (p1)-[:HAS_CONDITION {onset: '2013-09-10'}]->(htn)CREATE (p2)-[:HAS_CONDITION {onset: '2019-03-22'}]->(dmckd)CREATE (p2)-[:HAS_CONDITION {onset: '2019-03-22'}]->(htn)CREATE (p3)-[:HAS_CONDITION {onset: '2020-08-05'}]->(t1dm)CREATE (p3)-[:HAS_CONDITION {onset: '2021-01-17'}]->(af)CREATE (p4)-[:HAS_CONDITION {onset: '2010-04-11'}]->(htn)CREATE (p4)-[:HAS_CONDITION {onset: '2016-12-03'}]->(hf)CREATE (p4)-[:HAS_CONDITION {onset: '2016-12-03'}]->(af)CREATE (p5)-[:HAS_CONDITION {onset: '2012-07-19'}]->(t2dm)CREATE (p5)-[:HAS_CONDITION {onset: '2017-05-28'}]->(ckd3)// PrescriptionsCREATE (p1)-[:PRESCRIBED {date: '2016-01-10', dose: '500mg BD'}]->(metformin)CREATE (p1)-[:PRESCRIBED {date: '2013-10-01', dose: '5mg OD'}]->(ramipril)CREATE (p1)-[:PRESCRIBED {date: '2021-03-15', dose: '400mg TDS'}]->(ibuprofen)CREATE (p2)-[:PRESCRIBED {date: '2019-04-01', dose: '500mg BD'}]->(metformin)CREATE (p2)-[:PRESCRIBED {date: '2019-04-01', dose: '10mg OD'}]->(lisinopril)CREATE (p3)-[:PRESCRIBED {date: '2021-02-01', dose: '5mg OD'}]->(warfarin)CREATE (p3)-[:PRESCRIBED {date: '2021-06-12', dose: '400mg TDS'}]->(ibuprofen)CREATE (p4)-[:PRESCRIBED {date: '2017-01-10', dose: '5mg OD'}]->(ramipril)CREATE (p4)-[:PRESCRIBED {date: '2016-12-10', dose: '5mg OD'}]->(warfarin)CREATE (p5)-[:PRESCRIBED {date: '2012-08-01', dose: '500mg BD'}]->(metformin)CREATE (p5)-[:PRESCRIBED {date: '2018-06-01', dose: '40mg OD'}]->(atorvastatin)
Use Case 1: Comorbidity Mapping via SNOMED Hierarchy
The clinical question
“Find all patients who have any form of diabetes AND any form of chronic kidney disease.”
This is a common clinical query — for QOF, for research cohorts, for prescribing safety reviews. The trap is that patients are coded at specific SNOMED concept levels. Alice has Type 2 diabetes mellitus (44054006) and CKD stage 3 (433144002). Brian has Diabetes mellitus with chronic kidney disease (445276001), which is simultaneously a subtype of both. A query that only matches on exact codes misses the clinical picture entirely.
Why SQL struggles
In a relational system, you would need either a pre-materialised SNOMED ancestor table (expensive to maintain) or a recursive CTE traversing a snomed_hierarchy table. Either way, the query is long, fragile, and slow on large datasets.
-- SQL equivalent: find patients with *any* diabetes AND *any* CKD-- Requires pre-built ancestor table or recursive CTEWITH RECURSIVE ancestors AS ( SELECT child_code, parent_code FROM snomed_hierarchy UNION ALL SELECT a.child_code, h.parent_code FROM ancestors a JOIN snomed_hierarchy h ON a.parent_code = h.child_code)SELECT DISTINCT p.id, p.nameFROM patients pJOIN patient_conditions pc_dm ON p.id = pc_dm.patient_idJOIN ancestors a_dm ON pc_dm.snomed_code = a_dm.child_code AND a_dm.parent_code = '73211009' -- Diabetes mellitus rootJOIN patient_conditions pc_ckd ON p.id = pc_ckd.patient_idJOIN ancestors a_ckd ON pc_ckd.snomed_code = a_ckd.child_code AND a_ckd.parent_code = '709044004' -- CKD root
That query requires three joins, a recursive CTE, and two separate hierarchy traversals. And it still doesn’t handle the case where a single concept (like Diabetes mellitus with chronic kidney disease) satisfies both criteria simultaneously.
The Cypher version
// Find patients who have ANY condition that is a subtype of Diabetes mellitus// AND ANY condition that is a subtype of Chronic kidney disease// Variable-length IS_A traversal handles the full SNOMED hierarchyMATCH (dm_root:SnomedConcept {code: '73211009'})MATCH (ckd_root:SnomedConcept {code: '709044004'})MATCH (p:Patient)-[:HAS_CONDITION]->(dm_concept:SnomedConcept)WHERE (dm_concept)-[:IS_A*0..]->(dm_root)MATCH (p)-[:HAS_CONDITION]->(ckd_concept:SnomedConcept)WHERE (ckd_concept)-[:IS_A*0..]->(ckd_root)RETURN DISTINCT p.id AS patient_id, p.name AS patient, collect(DISTINCT dm_concept.term) AS diabetes_diagnoses, collect(DISTINCT ckd_concept.term) AS ckd_diagnosesORDER BY p.name
Result:
| patient_id | patient | diabetes_diagnoses | ckd_diagnoses |
|---|---|---|---|
| P001 | Alice | [“Type 2 diabetes mellitus”] | [“Chronic kidney disease stage 3”] |
| P002 | Brian | [“Diabetes mellitus with chronic kidney disease”] | [“Diabetes mellitus with chronic kidney disease”] |
| P005 | Evelyn | [“Type 2 diabetes mellitus”] | [“Chronic kidney disease stage 3”] |
The [:IS_A*0..] syntax means “zero or more IS_A hops” — it matches the concept itself or any ancestor. Brian, who has a single composite concept that satisfies both criteria simultaneously, is correctly identified. The SQL version above would miss him unless you added a UNION branch.
Enriching with medication context
Now extend the same query to also return what each patient is currently prescribed for those conditions:
MATCH (dm_root:SnomedConcept {code: '73211009'})MATCH (ckd_root:SnomedConcept {code: '709044004'})MATCH (p:Patient)-[:HAS_CONDITION]->(dm_concept:SnomedConcept)WHERE (dm_concept)-[:IS_A*0..]->(dm_root)MATCH (p)-[:HAS_CONDITION]->(ckd_concept:SnomedConcept)WHERE (ckd_concept)-[:IS_A*0..]->(ckd_root)OPTIONAL MATCH (p)-[:PRESCRIBED]->(med:Medication)RETURN DISTINCT p.name AS patient, collect(DISTINCT dm_concept.term) AS diabetes_type, collect(DISTINCT ckd_concept.term) AS ckd_stage, collect(DISTINCT med.name) AS current_medicationsORDER BY p.name
In a single query, without any additional joins, you now have the full clinical picture: comorbidities traversed through the SNOMED hierarchy and current prescriptions, returned together.
Use Case 2: Prescribing Safety — Contraindications and Drug Interactions
This is where the graph model pays for itself most clearly. Prescribing safety requires joining patient conditions, current medications, contraindication rules, and drug interaction rules simultaneously. In SQL, this is four-to-six table joins with complex OR logic. In Cypher, it is pattern matching.
2a: Patients prescribed a drug contraindicated by an existing condition
// Find patients who have been prescribed a medication// that is contraindicated with a condition they have (or a parent condition)MATCH (p:Patient)-[:PRESCRIBED]->(med:Medication)-[:CONTRAINDICATED_WITH]->(contra_concept:SnomedConcept)MATCH (p)-[:HAS_CONDITION]->(patient_concept:SnomedConcept)WHERE (patient_concept)-[:IS_A*0..]->(contra_concept) OR (contra_concept)-[:IS_A*0..]->(patient_concept)RETURN p.name AS patient, med.name AS medication, med.class AS drug_class, contra_concept.term AS contraindicated_condition, patient_concept.term AS patient_actual_diagnosisORDER BY p.name
Result:
| patient | medication | drug_class | contraindicated_condition | patient_actual_diagnosis |
|---|---|---|---|---|
| Alice | Ibuprofen | NSAID | Chronic kidney disease | Chronic kidney disease stage 3 |
| Alice | Metformin | Biguanide | Chronic kidney disease stage 3 | Chronic kidney disease stage 3 |
| Evelyn | Metformin | Biguanide | Chronic kidney disease stage 3 | Chronic kidney disease stage 3 |
Alice has two contraindicated prescriptions. Evelyn has one. The bidirectional hierarchy check (IS_A*0.. in both directions) ensures we catch contraindications regardless of which level the rule is encoded at versus which level the patient’s condition is coded at — a common real-world mismatch in coded clinical data.
2b: Patients with potentially dangerous drug-drug interactions
// Find patients prescribed two drugs that interact with each other,// and surface the interaction pairMATCH (p:Patient)-[:PRESCRIBED]->(med1:Medication)-[:INTERACTS_WITH]->(med2:Medication)MATCH (p)-[:PRESCRIBED]->(med2)// Avoid returning both (A,B) and (B,A)WHERE med1.name < med2.nameRETURN p.name AS patient, med1.name AS drug_1, med2.name AS drug_2, med1.class AS class_1, med2.class AS class_2ORDER BY p.name
Result:
| patient | drug_1 | drug_2 | class_1 | class_2 |
|---|---|---|---|---|
| Alice | Ibuprofen | Ramipril | NSAID | ACE inhibitor |
| Carol | Ibuprofen | Warfarin | NSAID | Anticoagulant |
Alice is on an NSAID and an ACE inhibitor — a clinically significant interaction (NSAIDs reduce the antihypertensive effect and increase renal risk). Carol is on an NSAID and warfarin — a bleeding risk interaction. Both are surfaced in one query.
2c: Full polypharmacy risk profile per patient
Combine both safety checks into a single profile query:
// Build a complete prescribing safety profile for every patient// showing: medications, any contraindications, any drug interactionsMATCH (p:Patient)OPTIONAL MATCH (p)-[:PRESCRIBED]->(med:Medication)OPTIONAL MATCH (p)-[:PRESCRIBED]->(risky_med:Medication)-[:CONTRAINDICATED_WITH]->(contra:SnomedConcept)OPTIONAL MATCH (p)-[:HAS_CONDITION]->(pt_cond:SnomedConcept) WHERE (pt_cond)-[:IS_A*0..]->(contra)OPTIONAL MATCH (p)-[:PRESCRIBED]->(med_a:Medication)-[:INTERACTS_WITH]->(med_b:Medication)OPTIONAL MATCH (p)-[:PRESCRIBED]->(med_b) WHERE med_a.name < med_b.nameRETURN p.name AS patient, count(DISTINCT med) AS total_medications, collect(DISTINCT risky_med.name) AS contraindicated_meds, collect(DISTINCT [med_a.name, med_b.name]) AS interaction_pairsORDER BY total_medications DESC
This single query returns the complete prescribing risk surface for your entire patient list — something that would require multiple queries or a complex stored procedure in a relational system.
The Comparison in Numbers
To make the relational vs graph difference concrete, here is a rough comparison for a practice with 10,000 patients, 80,000 coded conditions, and 60,000 active prescriptions:
| Query | SQL approach | Cypher approach |
|---|---|---|
| Diabetes + CKD comorbidity (any subtype) | Recursive CTE + 3 joins, ~8–15 lines | 6 lines, single pattern match |
| Contraindicated prescriptions | 4-table join with OR logic | 3-line MATCH with hierarchy traversal |
| Drug interaction pairs | Self-join on interaction table | 2-line MATCH, deduplication in WHERE |
| Full polypharmacy risk profile | Multiple queries or complex stored proc | Single OPTIONAL MATCH chain |
| Adding a new SNOMED hierarchy level | Rebuild/update ancestor materialised view | No change — traversal is dynamic |
The last row matters most in practice. SNOMED CT releases updates twice a year. In a relational system, hierarchy changes require re-materialising ancestor tables. In Neo4j, new IS_A edges are added and existing queries automatically traverse them correctly — the graph is the hierarchy.
Why This Matters for Primary Care Data Specifically
Primary care data has several properties that make the graph model particularly well-suited:
Longitudinal depth. A patient’s record may span 30+ years with hundreds of coded events. Querying disease progression — “patients where the sequence was hypertension → diabetes → CKD, in that order” — is a temporal path query. Cypher handles this naturally with ordered relationship properties; SQL requires window functions and ranked subqueries.
SNOMED is already a graph. SNOMED CT contains over 350,000 concepts connected by 1.3 million relationships. It was designed as a graph. Storing it in tables and then querying with recursive CTEs is a category error. Neo4j can import the full SNOMED CT release and traverse it natively.
Coded data is imprecise. GPs code at different levels of specificity. One clinician codes “Type 2 diabetes mellitus”; another codes “Insulin-treated Type 2 diabetes mellitus”. Queries need to handle this by traversing the hierarchy in both directions — a pattern that graph traversal makes trivial.
Clinical rules are relationships. Contraindications, drug interactions, and NICE guidance are all relationship-shaped. Encoding them as edges in a graph means clinical rule updates are data changes, not schema changes.
Getting Started
Neo4j is free to run locally via Neo4j Desktop, or via Aura Free Tier (cloud). The seed data in this article will run as-is in Neo4j Browser.
For NHS/primary care data integration, the recommended pipeline is:
- Export your EMIS/SystmOne data via GPES or local SQL extract
- Import SNOMED CT using Neo4j’s CSV import tool (SNOMED releases include Relationship and Concept files in tab-separated format)
- Load patient conditions and prescriptions as edges pointing to SNOMED concept nodes
- Use Neo4j’s APOC library for batch imports and periodic graph refresh
// Example: bulk-load patient conditions from a CSV extractLOAD CSV WITH HEADERS FROM 'file:///patient_conditions.csv' AS rowMATCH (p:Patient {id: row.patient_id})MATCH (c:SnomedConcept {code: row.snomed_code})MERGE (p)-[:HAS_CONDITION {onset: date(row.onset_date)}]->(c)
Conclusion
Relational databases built primary care IT, and they will continue to run operational systems for the foreseeable future. But the queries that matter most for clinical safety, population health, and research are graph queries — they traverse hierarchies, match patterns, and follow relationship chains in ways that SQL was not designed for.
SNOMED CT is a graph. Patient journeys are paths. Drug interactions are edges. The data is already telling you what model to use.
Neo4j and Cypher don’t require you to redesign your entire data infrastructure. A graph layer alongside your existing systems — holding SNOMED, prescribing rules, and patient condition graphs — can answer questions that your relational system simply cannot, without the brittle recursive SQL that most teams quietly avoid writing.
The queries above are a starting point. The same patterns extend to disease progression analysis, QOF cohort identification, referral pathway optimisation, and genomics data integration. The shape of the problem is always the same: relationships all the way down.
All SNOMED CT codes used in this article are from the SNOMED CT International Edition. SNOMED CT® is the registered trademark of SNOMED International. Clinical examples are illustrative and do not constitute medical advice.