SNOMED CT Clinical Terminology
A comprehensive, polyhierarchical clinical terminology — not a billing classification — that assigns numeric concept identifiers to clinical findings, disorders, procedures, and observable entities, enabling granular EHR phenotyping via descendant-hierarchy expansion and serving as the standard Condition vocabulary in the OMOP Common Data Model.
On this page
SNOMED CT is the world's largest clinical terminology — a giant dictionary that gives each clinical concept (a disease, a finding, a procedure) its own permanent numeric ID and connects those concepts in a hierarchy, so that searching for 'diabetes mellitus' automatically includes all the specific subtypes below it. Unlike ICD codes, which are designed for insurance billing and lump many things together, SNOMED CT is designed for clinical documentation and can describe what a clinician actually saw or decided with high precision. It is most commonly encountered in electronic health record (EHR) problem lists and is the vocabulary that the OMOP data model uses to store diagnoses — so understanding SNOMED CT is a prerequisite for understanding OMOP concept sets.
SNOMED CT
(Systematized Nomenclature of Medicine — Clinical Terms) is the world's most comprehensive clinical terminology and the vocabulary of record for capturing clinical meaning at the point of care. It is maintained by SNOMED International (a not-for-profit association of national member bodies) and distributed in the United States by the National Library of Medicine (NLM) through the US Edition.
Use requires a license; in member countries, including the United States, that license is free — US users obtain access through the NLM's UMLS Metathesaurus license, which covers SNOMED CT for US purposes. Small illustrative examples with attribution to SNOMED International are acceptable; bulk reproduction of SNOMED CT content is not.
Core conceptual distinction: terminology vs classification
The single most important thing to understand about SNOMED CT is what it is not: it is not a billing classification. ICD-10-CM (and its predecessors) was designed for statistical, administrative, and reimbursement purposes — its categories are mutually exclusive, its hierarchy is coarse, and a single code often collapses many clinically distinct entities for the practical convenience of counting and billing.
SNOMED CT was designed for the opposite purpose: to capture the full clinical meaning of what a clinician observed, recorded, or decided, with enough granularity and structure to support clinical decision support, research phenotyping, and inter-system exchange. This distinction is the conceptual spine of every RWE use case: ICD-10-CM tells you what was billed; SNOMED CT tells you (or tries to tell you) what actually happened clinically.
Three core components
Every concept in SNOMED CT has three mandatory elements. First, a concept — a unique, atomic clinical idea identified by a numeric concept identifier (SCTID) that is 6–18 digits long and encodes a partition (the type of component) and a Verhoeff check digit. The concept identifier is opaque and permanent: 44054006 represents |Diabetes mellitus type 2| in every edition, every country, every year it has existed.
Second, descriptions — one or more human-readable labels for the concept, split into a single Fully Specified Name (FSN, which is globally unique and unambiguous), one Preferred Term (the display label for a locale), and zero or more Synonyms and Acceptable Terms. A single concept may have many acceptable terms; 73211009 has both |Diabetes mellitus| and |DM| as accepted synonyms. Third, relationships — formal machine-readable links between concepts, stored in the RF2 Relationship table. The most important relationship type is |Is a| (SCTID 116680003), which builds the hierarchy.
The |is a| polyhierarchy
Unlike ICD-10-CM's tree — where each code has exactly one parent — SNOMED CT is a directed acyclic graph (DAG). A concept can have multiple parents, making it polyhierarchical. For example, 44054006 |Diabetes mellitus type 2| is a child of both 73211009 |Diabetes mellitus| (the metabolic disorder lineage) and of 8801005 |Secondary diabetes mellitus| if the concept is inherited from multiple classification facets.
This means descendant queries must traverse a graph, not a tree, typically via a transitive closure of |is a| relationships over the RF2 Relationship file — every concept that can reach a root concept through a chain of |is a| edges is a descendant.
The practical upshot for RWE is that a concept-set built by "all descendants of 73211009 |Diabetes mellitus|" will capture far more granular codes than any flat ICD list, but the membership of that concept-set changes between SNOMED CT releases (twice yearly for the US Edition) as new concepts are added and retired.
Defining attribute relationships
Beyond |is a|, SNOMED CT uses additional relationship types — called defining attributes — to formally specify what makes a concept what it is. Clinical findings carry attributes such as |finding site| (which body structure is affected) and |associated morphology| (what is structurally abnormal). Disorders carry |causative agent|, |pathological process|, and others. These attributes power description logic classifiers (such as EL++ reasoners) that can infer implied subsumption relationships and detect modeling errors.
For most RWE users, attribute relationships are background machinery; for advanced use cases (clinical decision support, automated phenotype generation), they are how SNOMED CT earns its designation as a "formal ontology" rather than a flat vocabulary.
Pre-coordination vs post-coordination
SNOMED CT concepts are either pre-coordinated — where a single SCTID expresses a complete clinical idea (e.g., 44054006 |Diabetes mellitus type 2|) — or post-coordinated, where a base concept is combined with additional expressions using SNOMED's Compositional Grammar to capture a nuanced clinical statement not represented by any single concept (e.g., a finding site applied to a generic finding concept at the point of documentation).
Post-coordination is theoretically powerful but is rarely supported at the EHR interface level; most RWE work operates exclusively on pre-coordinated concepts from problem lists and encounter diagnoses.
Pros, cons, and trade-offs — specific and comparative
- vs ICD-10-CM (the dominant alternative for condition coding in US claims): SNOMED CT's polyhierarchy enables descendant expansion, meaning a single query "find all concepts below 73211009 |Diabetes mellitus|" returns hundreds of granular concepts — type 1, type 2, maturity onset, gestational, drug-induced, and rare monogenic subtypes — without requiring the analyst to enumerate them individually. ICD-10-CM requires manual code list curation; adding a new ICD code requires updating every code list that should include it. SNOMED CT also preserves synonymy (many terms → one concept), so free-text and structured entry with different labels resolve to the same computable unit. Cons: SNOMED CT is not in US administrative claims — clinical encounter data must be coded in ICD-10-CM for billing — so SNOMED exists in the EHR, not in the payer file. The two vocabularies require an explicit crosswalk. Prefer SNOMED CT when working from EHR or registry source data with rich clinical terminology; prefer ICD-10-CM when your primary source is US insurance claims.
- vs flat proprietary code lists (e.g., hand-curated ICD code lists for a specific indication): A SNOMED CT-based concept set updates automatically as new descendant concepts are added to the terminology, whereas a flat list requires manual maintenance. Cons: SNOMED CT concept membership changes between releases, making reproducibility dependent on pinning the terminology version; a concept set built against the 2023-09 US Edition may have slightly different membership than one built against the 2024-03 US Edition. Prefer SNOMED CT descendant sets for maintainability in longitudinal or multi-database studies; use version-pinning and document the release date in your methods.
- vs OMOP Concept Sets (which use SNOMED under the hood): OMOP's CONDITION_OCCURRENCE table stores diagnoses using SNOMED CT as its standard vocabulary — source ICD-9/10-CM codes are mapped to SNOMED standard concepts via the OMOP vocabulary "Maps to" relationship. The OMOP Atlas tool's concept-set builder applies descendant expansion using the same |is a| hierarchy, so OMOP concept sets are SNOMED CT descendant queries packaged in a user interface. Understanding SNOMED CT structure is therefore a prerequisite for understanding OMOP concept-set logic. Cons: The OMOP mapping layer (ICD→SNOMED) introduces mapping errors and losses; a source ICD code may map to a SNOMED concept that is too broad or too narrow, and some source codes are explicitly excluded from mapping ("Maps to" nothing). Audit the mapping before relying on OMOP concept-set coverage.
- vs LOINC (laboratory/observation coding) and RxNorm (drug coding): SNOMED CT, LOINC, and RxNorm are complementary, not competing. SNOMED CT covers clinical findings, disorders, procedures, body structures, and organisms; LOINC covers the identity of a laboratory test or observation; RxNorm covers drugs and their ingredients. All three are required for a complete clinical representation. OMOP uses all three in their respective domains (Condition→SNOMED; Measurement→LOINC; Drug→RxNorm). Prefer the domain-appropriate vocabulary in each context; do not substitute SNOMED for LOINC in lab-based phenotyping or for RxNorm in drug exposure definitions.
When NOT to use — and when it is actively misleading or dangerous
- Do not use SNOMED CT as a drop-in replacement for ICD-10-CM in US claims analysis. US insurance claims are coded in ICD-10-CM for billing; SNOMED does not appear in the adjudicated claim record. Applying a SNOMED-based code list to a raw claims table without an explicit mapping step will return no rows and produce silently empty cohorts — a failure mode that is not always immediately obvious.
- Do not ignore the terminology version. SNOMED CT releases in the United States occur twice yearly (January and July). Concept content, descriptions, and relationship definitions change between releases. A cohort built without specifying the SNOMED CT release date is not reproducible. Always pin the terminology version in your methods and SAP.
- Do not conflate problem-list SNOMED entries with encounter diagnoses. In EHR systems that use SNOMED CT for problem list documentation, a concept entered on the problem list may persist for years without update, representing the patient's chronic conditions at time of entry — not a fresh clinical judgment. Encounter diagnoses, by contrast, are event-based. Mixing these provenance types in a descendant-expansion query can inflate prevalence estimates and misattribute timing.
- Do not assume the SNOMED→ICD-10-CM map is lossless. SNOMED International publishes a rule-based map from SNOMED CT concepts to ICD-10 for reimbursement derivation. This map is lossy in both directions: multiple SNOMED concepts map to the same ICD code (granularity is lost in the ICD→SNOMED direction), and some SNOMED concepts have no ICD equivalent or map to "unspecified" codes. Using a derived ICD code from a SNOMED source concept in a claims validation context can introduce systematic misclassification.
- Do not use SNOMED CT for medication coding. SNOMED CT includes a Substance hierarchy and some Clinical Drug concepts, but RxNorm is the designated vocabulary for drug exposure in OMOP and in most US EHR interoperability standards. Using SNOMED for drug coding duplicates capability that is better served by RxNorm and introduces a non-standard pattern that most ETL pipelines will not support.
Data-source operational depth
- EHR: SNOMED CT is most commonly present in EHR data as the vocabulary behind problem lists (the clinician's running list of the patient's active conditions) and, in some implementations, encounter diagnoses. Problem-list capture is clinician-driven: conditions are added at clinical discretion and may be carried forward indefinitely without update, meaning a resolved condition may still appear as "active" in a SNOMED problem-list query years after resolution. Encounter diagnoses are event-level and more analogous to claims diagnoses but may be coded at various levels of specificity depending on the EHR's interface and the clinician's coding habits. In OMOP ETLs from EHR source data, the `condition_type_concept_id` distinguishes problem-list entries from encounter diagnoses, and this provenance metadata is critical for RWE studies where the timing and recurrence of the condition are analytically relevant.
- Registry: Disease registries frequently use SNOMED CT for condition coding, often with tighter clinical curation (oncology registries, rare-disease registries). Registry SNOMED data tends to be highly precise for the index condition but may be absent for comorbidities and is rarely present for drug exposure, which still requires claims or pharmacy linkage.
- Linked EHR-claims: The ideal configuration for SNOMED-based RWE — EHR provides SNOMED-coded clinical granularity (problem-list subtype, lab values, vitals, pathology) while claims provide complete drug exposure and a defined enrollment denominator. The linkage must reconcile SNOMED problem-list dates with ICD claim dates; these will often differ because the problem-list date reflects when the condition was first documented in this EHR, not when the patient first had the condition or when the claim was generated.
- OMOP CDM: In OMOP, the CONDITION_OCCURRENCE table stores SNOMED CT concept IDs as the standard vocabulary for conditions. The ETL maps source ICD-9-CM or ICD-10-CM codes to SNOMED using the "Maps to" relationship in the OMOP vocabulary tables. Descendant queries are executed using the CONCEPT_ANCESTOR table, which pre-computes the transitive closure of the |is a| hierarchy — exactly the operation you would perform manually over the RF2 Relationship file. Always verify the ETL mapping completeness: not all source codes map to SNOMED concepts (some are excluded), and mapping quality varies across CDM vintages and ETL implementations.
Licensing note
SNOMED CT is owned by SNOMED International. Use requires a license. In member countries — which include the United States — access is free for most users. US users should obtain access through the NLM via a UMLS license (https://www.nlm.nih.gov/healthit/snomedct/index.html). Do not bulk-reproduce SNOMED CT content; small illustrative examples with attribution to SNOMED International are acceptable.
Decision diagram
flowchart TD ROOT["73211009<br/>|Diabetes mellitus|<br/>(root concept)"] ROOT --> T2["44054006<br/>|Diabetes mellitus type 2|"] ROOT --> T1["46635009<br/>|Diabetes mellitus type 1|"] ROOT --> GDM["11687002<br/>|Gestational diabetes|"] T2 --> MOD["609568004<br/>|Maturity onset diabetes of the young|"] T2 --> INS["44054006-INS<br/>|T2DM with insulin|<br/>(illustrative subtype)"] T2 --> COMP["73211009-COMP<br/>|T2DM with complication|<br/>(illustrative subtype)"] style ROOT fill:#4a90d9,color:#fff style T2 fill:#7bb3e8,color:#000 style T1 fill:#a8d0f0,color:#000 style GDM fill:#a8d0f0,color:#000 style MOD fill:#d0e8f8,color:#000 style INS fill:#d0e8f8,color:#000 style COMP fill:#d0e8f8,color:#000
Worked example
Scenario
A pharmacoepidemiologist wants to build a type 2 diabetes cohort from an EHR dataset that has been converted to the OMOP CDM. She needs to understand why a SNOMED CT-based descendant expansion finds more patients than a flat ICD-10-CM code list, and whether the extra patients represent true type 2 diabetes or noise from the broader hierarchy. The example walks through the two approaches on a hypothetical patient database of 100,000 adults, shows the code counts each strategy produces, and computes the incremental capture rate.
Dataset
Comparison of two phenotyping strategies for type 2 diabetes: flat ICD-10-CM list vs SNOMED CT descendant expansion. Row counts are from a hypothetical 100,000-patient EHR-OMOP dataset.
| Strategy | Root or seed concept | Number of codes / concepts in set | Patients identified | Notes |
|---|---|---|---|---|
| Flat ICD-10-CM list | E11 (Type 2 diabetes mellitus) and E11.* subcodes | 37 | 8200 | Manual enumeration of E11 and its subcategories; must be updated when new ICD codes are added |
| SNOMED CT descendant expansion | 44054006 (Diabetes mellitus type 2) and all is-a descendants | 112 | 8960 | Automated hierarchy traversal in CONCEPT_ANCESTOR; includes granular clinical subtypes not represented in ICD-10-CM |
| Incremental patients (SNOMED only) | Concepts without ICD-10-CM equivalent or mapping gap | 75 | 760 | Patients coded with SNOMED-specific subtypes (e.g., maturity onset diabetes of the young in problem list) that did not have a corresponding E11 claim in the observation window |
Steps
Result
SNOMED CT descendant expansion identifies 8960 patients vs 8200 from a flat ICD-10-CM code list, capturing 760 / 8200 = 0.0927 more patients (9.3% incremental capture). The 112-concept SNOMED set covers granular clinical subtypes absent from the 37-code ICD list. Version-pinning the SNOMED release and documenting provenance (problem-list vs encounter diagnosis) are required for reproducibility.
Trade-offs
Runnable example
Two operations: (1) build a transitive closure of the |is a| relationship over the RF2 Relationship file to find all descendants of a given SNOMED CT concept (here: 44054006 Diabetes mellitus type 2); (2) run the equivalent query over an OMOP CONCEPT_ANCESTOR table using SQLite.
import pandas as pd
import sqlite3
# ── Part 1: RF2-based transitive closure (native SNOMED CT) ──────────────────
# Download the US Edition RF2 from NLM (UMLS licence required).
# The Relationship snapshot file is typically named:
# sct2_Relationship_Snapshot_US1000124_<YYYYMMDD>.txt
def load_isa_edges(rf2_relationship_file: str) -> dict[str, set[str]]:
"""Return a dict: child_sctid -> set of immediate parent SCTIDs (Is a only)."""
IS_A = "116680003" # SCTID for the |Is a| relationship type
df = pd.read_csv(rf2_relationship_file, sep="\t", dtype=str, usecols=[
"active", "sourceId", "destinationId", "typeId"
])
df = df[(df["active"] == "1") & (df["typeId"] == IS_A)]
parents: dict[str, set[str]] = {}
for _, row in df.iterrows():
parents.setdefault(row["sourceId"], set()).add(row["destinationId"])
return parents
def descendants(root_sctid: str, parents: dict[str, set[str]]) -> set[str]:
"""Return all concepts that are a descendant of root_sctid (transitive Is a).
Uses BFS over the child→parent graph traversed in reverse (child→parent → parent is ancestor).
"""
# Build child map (parent → set of children) from the parent map
children: dict[str, set[str]] = {}
for child, pset in parents.items():
for p in pset:
children.setdefault(p, set()).add(child)
visited: set[str] = set()
queue = [root_sctid]
while queue:
node = queue.pop()
for child in children.get(node, set()):
if child not in visited:
visited.add(child)
queue.append(child)
return visited # does NOT include root_sctid itself
# Example usage (comment out if no RF2 files are available):
# parents = load_isa_edges("sct2_Relationship_Snapshot_US1000124_20240301.txt")
# dm_t2_descendants = descendants("44054006", parents)
# print(f"Found {len(dm_t2_descendants)} descendants of 44054006 |Diabetes mellitus type 2|")
# ── Part 2: OMOP CONCEPT_ANCESTOR query (SQLite example) ─────────────────────
# In a real OMOP environment, replace sqlite3 with your database driver.
# CONCEPT_ANCESTOR stores the pre-computed transitive closure of the Is a hierarchy.
DM_T2_OMOP_CONCEPT_ID = 201826 # OMOP standard concept_id for Diabetes mellitus type 2 (SNOMED 44054006)
def get_omop_descendants(con: sqlite3.Connection, ancestor_concept_id: int) -> pd.DataFrame:
"""Return all OMOP standard concept_ids that are descendants of a given concept.
min_levels_of_separation = 0 includes the root; >= 1 for descendants only.
"""
query = """
SELECT
ca.descendant_concept_id,
c.concept_name,
c.vocabulary_id,
ca.min_levels_of_separation
FROM concept_ancestor ca
JOIN concept c ON c.concept_id = ca.descendant_concept_id
WHERE ca.ancestor_concept_id = ?
AND ca.min_levels_of_separation >= 1 -- descendants only, not the root itself
AND c.invalid_reason IS NULL -- active concepts only
ORDER BY ca.min_levels_of_separation, c.concept_name
"""
return pd.read_sql(query, con, params=(ancestor_concept_id,))
# Example usage (replace ':memory:' with path to OMOP SQLite or use your DB connection):
# con = sqlite3.connect(":memory:") # placeholder
# df = get_omop_descendants(con, DM_T2_OMOP_CONCEPT_ID)
# print(f"OMOP descendant concepts: {len(df)}")
# print(df.head(10).to_string(index=False))Equivalent SNOMED CT descendant expansion in R: (1) BFS over the RF2 Relationship file to build descendants from native SNOMED CT data; (2) SQL query against OMOP CONCEPT_ANCESTOR using DBI/RSQLite for the OMOP-native approach. Both are minimal, commented implementations.
library(dplyr)
library(DBI)
library(RSQLite)
# ── Part 1: RF2-based transitive closure (native SNOMED CT) ──────────────────
IS_A_TYPE <- "116680003" # SCTID for Is a relationship type
load_isa_edges <- function(rf2_relationship_file) {
# Read only the columns we need; typeId and active are character for SCTID fidelity
df <- read.delim(rf2_relationship_file, sep = "\t", colClasses = "character",
stringsAsFactors = FALSE)
df <- df[df$active == "1" & df$typeId == IS_A_TYPE,
c("sourceId", "destinationId")]
# Return named list: parent → vector of children (reverse of sourceId → destinationId)
child_map <- split(df$sourceId, df$destinationId)
child_map
}
descendants_bfs <- function(root_sctid, child_map) {
# BFS from root down through child_map
visited <- character(0)
queue <- root_sctid
while (length(queue) > 0) {
node <- queue[1]
queue <- queue[-1]
kids <- child_map[[node]]
new <- setdiff(kids, visited)
visited <- c(visited, new)
queue <- c(queue, new)
}
visited # excludes root itself
}
# Example usage (comment out if RF2 files not available):
# child_map <- load_isa_edges("sct2_Relationship_Snapshot_US1000124_20240301.txt")
# dm_t2_desc <- descendants_bfs("44054006", child_map)
# cat("Descendants of 44054006:", length(dm_t2_desc), "\n")
# ── Part 2: OMOP CONCEPT_ANCESTOR query (RSQLite / DBI) ─────────────────────
DM_T2_OMOP_ID <- 201826L # OMOP concept_id for Diabetes mellitus type 2
get_omop_descendants <- function(con, ancestor_concept_id) {
query <- "
SELECT
ca.descendant_concept_id,
c.concept_name,
c.vocabulary_id,
ca.min_levels_of_separation
FROM concept_ancestor ca
JOIN concept c ON c.concept_id = ca.descendant_concept_id
WHERE ca.ancestor_concept_id = ?
AND ca.min_levels_of_separation >= 1
AND c.invalid_reason IS NULL
ORDER BY ca.min_levels_of_separation, c.concept_name
"
DBI::dbGetQuery(con, query, params = list(ancestor_concept_id))
}
# Example usage:
# con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") # replace with real OMOP connection
# df <- get_omop_descendants(con, DM_T2_OMOP_ID)
# cat("OMOP descendants:", nrow(df), "\n")
# print(head(df, 10))Citations
- [1]Cornet R, de Keizer N. Forty years of SNOMED: a literature review. BMC Medical Informatics and Decision Making. 2008;8(Suppl 1):S2.
- [2]Bodenreider O. The Unified Medical Language System (UMLS): integrating biomedical terminology. Nucleic Acids Research. 2004;32(Database issue):D267-D270.