CQL 語法速查
CQL(Clinical Quality Language,臨床品質語言)讀規則時最常碰到的語法,分組整理成速查表:每一項列出寫法、一句說明和一個小例子。例子都屬於同一份糖尿病追蹤邏輯,資料模型是 FHIR R4。
第一次讀 CQL?先看讀懂工作台的 CQL,它用一條健保條文從頭講到尾;這頁適合邊讀規則邊查。遇到不懂的名詞,可以查名詞表。
檔案結構
一份 CQL 檔案就是一個 library。開頭是一連串宣告,慣例上依下面的順序排列,接著才是 define。這段開頭是本頁所有例子共用的,後面各節的例子都可以直接接在它後面。
library DiabetesCare version '1.0.0'
using FHIR version '4.0.1'
include FHIRHelpers version '4.0.1' called FHIRHelpers
codesystem "LOINC": 'http://loinc.org'
valueset "Diabetes": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.103.12.1001'
code "HbA1c": '4548-4' from "LOINC" display 'Hemoglobin A1c/Hemoglobin.total in Blood'
code "eGFR": '62238-1' from "LOINC"
// Measurement year 2026
parameter "Measurement Period" Interval<DateTime>
default Interval[@2026-01-01T00:00:00.000, @2027-01-01T00:00:00.000)
context Patientlibrary DiabetesCare version '1.0.0'- 這份 library 的名稱和版本。其他 library 用 include 引用它時,就是指名這個名稱和版本。
using FHIR version '4.0.1'- 資料模型。FHIR 4.0.1 就是 FHIR R4;retrieve 取得到哪些資源、資源有哪些欄位,都由它決定。
include FHIRHelpers version '4.0.1' called FHIRHelpers- 引用另一份 library,called 後面是在這份檔案裡用的別名。FHIRHelpers 負責把 FHIR 的資料型別轉成 CQL 的型別,讀 FHIR 資料的 library 幾乎都會引用它。
codesystem "LOINC": 'http://loinc.org'- 代碼系統:冒號後面是它的正式網址(URI)。
valueset "Diabetes": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.103.12.1001'- 值集:一組代碼,用正式網址指名。這個網址是美國 VSAC(Value Set Authority Center)收錄的糖尿病值集。
code "HbA1c": '4548-4' from "LOINC" display 'Hemoglobin A1c/Hemoglobin.total in Blood'- 單一代碼:代碼值和所屬的代碼系統。display 是給人看的名稱,可以省略,像 eGFR 那一行就沒寫。
// Measurement year 2026- 註解:// 到行尾,或用 /* */ 包住多行,不影響計算。
parameter "Measurement Period" Interval<DateTime> default Interval[@2026-01-01T00:00:00.000, @2027-01-01T00:00:00.000)- 參數:執行時由外部傳入,沒有傳入就用 default 的值。這裡是 2026 整年,結尾的小括號表示不含 2027-01-01。
context Patient- 這行之後的 define 都針對一位病人計算,retrieve 也只取這位病人的資料。
小提醒:雙引號包的是名稱,例如 "HbA1c",名稱可以有空格;單引號包的是文字,例如 'http://loinc.org'。
規格:Author’s Guide: DeclarationsUsing CQL with FHIRFHIRHelpers
define 與 retrieve
define 替一段運算取名字;retrieve 是 CQL 取資料的寫法,結果是一個清單。
define "名稱": 運算式替運算式取名字,其他地方用雙引號裡的名稱引用它。第二個 define 用 exists 檢查第一個的結果是否至少有一筆。
define "Diabetes Conditions": [Condition: "Diabetes"] define "Has Diabetes": exists "Diabetes Conditions"[資源類型: "值集"]取出這位病人的 Condition(診斷)中,代碼在 Diabetes 值集裡的那些。
[Condition: "Diabetes"][資源類型: "代碼"]用單一代碼取資料:代碼是 LOINC 4548-4 的 Observation,也就是 HbA1c 檢驗結果。
[Observation: "HbA1c"][資源類型]不加篩選,取出這位病人所有的 Encounter(就診紀錄)。
[Encounter][資源類型: 欄位 in "值集"]指定用哪個欄位比對。不寫欄位時用資源的主要代碼欄位,Condition 和 Observation 都是 code,所以這行和第一個寫法的結果相同。
[Condition: code in "Diabetes"]
retrieve 不會過濾狀態:已取消、登錄錯誤(entered-in-error)的資料也會取出來,要在查詢的 where 裡自己篩選,見下一節。
查詢
在清單後面加一個別名就成了查詢,別名代表清單裡「目前這一筆」。後面可以接 with/without、where、return、sort 等子句,順序就是這樣。
來源 別名O 是別名。只有別名的查詢會原樣回傳整個清單,通常還會接下面的子句。
[Observation: "HbA1c"] Owhere 條件只留下條件是 true 的資料,false 和 null 都會被排除。Observation 的 effective 在 FHIR 可能是時間點或一段期間,
as dateTime只取時間點;如果是期間,結果是 null,這一筆就不會留下。define "HbA1c In Period": [Observation: "HbA1c"] O where O.status in { 'final', 'amended', 'corrected' } and (O.effective as dateTime) during "Measurement Period"with 來源 別名 such that 條件只留下找得到至少一筆相符資料的項目:就診期間內有驗 HbA1c 的就診。
define "Visits With HbA1c": [Encounter] E with [Observation: "HbA1c"] O such that (O.effective as dateTime) during E.periodwithout 來源 別名 such that 條件反過來,留下一筆相符資料都找不到的項目:就診期間內沒有驗 HbA1c 的就診。
define "Visits Without HbA1c": [Encounter] E without [Observation: "HbA1c"] O such that (O.effective as dateTime) during E.periodreturn 運算式決定每一筆輸出什麼;
Tuple可以組成有欄位名稱的新結構。Tuple 裡不會自動轉換型別,所以這裡呼叫FHIRHelpers.ToDateTime,讓 takenAt 成為 CQL 的 DateTime;在比較運算裡,FHIRHelpers 會自動轉換。define "HbA1c Results": "HbA1c In Period" O return Tuple { takenAt: FHIRHelpers.ToDateTime(O.effective as dateTime), result: O.value as Quantity } sort by takenAtsort by 欄位 ascsort by 欄位 desc排序:asc 由小到大(預設),desc 由大到小。sort by 後面直接寫欄位名稱,不加別名;
start of period是就診開始的時間。define "Encounters Latest First": [Encounter] E sort by start of period desc
型別與字面值
CQL 的每個值都有型別。下面是讀規則時最常見的型別和它們的寫法。
Boolean布林值。查不到資料時是 null,見三值邏輯與 null。
true falseInteger整數。
42Decimal小數。
7.5String文字,用單引號。雙引號包的是名稱,不是文字。
'final'Date日期,以 @ 開頭。
@2026-03-15DateTime日期加時間,中間用 T 隔開;+08:00 是台灣的時區。
@2026-03-15T08:30:00.000+08:00Time只有時間,以 @T 開頭。
@T08:30Quantity數值加單位。單位用 UCUM 寫法並加單引號;時間長度用 years、months、days 等英文字,不加引號。
7 '%' 30 'mL/min/{1.73_m2}' 90 daysInterval區間。中括號包含端點,小括號不包含:
Interval[18, 65)是 18 以上、未滿 65。Interval[18, 65) Interval[@2026-01-01, @2026-12-31]List清單,用大括號。retrieve 和查詢的結果也都是清單。
{ 'final', 'amended', 'corrected' }Tuple有欄位名稱的結構,每個欄位寫成「名稱: 值」。
Tuple { takenAt: @2026-03-15, result: 6.8 '%' }Code單一代碼:代碼值和代碼系統,可以加 display。
Code '4548-4' from "LOINC" display 'Hemoglobin A1c/Hemoglobin.total in Blood'Concept代表同一件事的一組代碼,例如同一項檢驗的兩個 LOINC 代碼。FHIR 的 CodeableConcept 在 CQL 裡就是 Concept。
Concept { Code '4548-4' from "LOINC", Code '17856-6' from "LOINC" } display 'Hemoglobin A1c'
清單與值集
retrieve 和查詢的結果都是清單。這些運算回答「有沒有」「有幾筆」「是哪一筆」。
元素 in 清單清單 contains 元素是否在清單裡。in 和 contains 是同一件事,只是左右顛倒。
'final' in { 'final', 'amended', 'corrected' } // true { 'final', 'amended', 'corrected' } contains 'preliminary' // false值 in 區間是否落在區間內:18 到 75 歲,兩端都包含。區間也可以用 contains。
AgeInYears() in Interval[18, 75]代碼 in "值集"代碼是否在值集裡。C.code 是 FHIR 的 CodeableConcept,其中任一個代碼在值集裡就算,結果和
[Condition: "Diabetes"]相同。define "Diabetes By Code": [Condition] C where C.code in "Diabetes"exists 清單清單至少有一筆就是 true。空清單和 null 都得到 false,不會是 null。
exists "HbA1c In Period"Count(清單)清單有幾筆,不算 null。這裡是「量測期間內至少驗了兩次 HbA1c」。
Count("HbA1c In Period") >= 2First(清單)Last(清單)第一筆、最後一筆,照清單現有的順序取,清單是空的時候是 null。HbA1c Results 已經依 takenAt 由舊到新排好,所以 Last 是最近一次。
define "Latest HbA1c": Last("HbA1c Results")
規格:CQL Reference: List OperatorsCQL Reference: Aggregate Functions
比較與邏輯
比較、邏輯運算和檢查 null 的寫法。遇到 null 時結果怎麼算,見本頁最後一節。
= != < <= > >=比較。數量要單位相容才能比;任一邊是 null,例如沒有 HbA1c 結果,比較的結果就是 null。
define "HbA1c Controlled": "Latest HbA1c".result < 7 '%'~ !~等價。比較文字時不分大小寫;比較代碼時只看代碼系統和代碼值,不看 display;兩邊都是 null 時是 true,所以 ~ 不會回傳 null。
'Final' ~ 'final' // true 'Final' = 'final' // falseand or not且、或、非。遇到 null 的算法見三值邏輯與 null。
define "Adult With Diabetes": "Has Diabetes" and AgeInYears() >= 18A implies B「如果 A,那麼 B」:A 不成立時結果是 true,A 成立時結果就看 B。這裡是「有糖尿病的話,量測期間內要驗過 HbA1c」。
define "HbA1c Checked If Diabetic": "Has Diabetes" implies exists "HbA1c In Period"X is nullX is not null檢查是不是 null,結果只會是 true 或 false。
"Latest HbA1c" is nullX is trueX is false值剛好是 true(或 false)才成立,null 會得到 false。為什麼需要它,見結果與 is true。
"HbA1c Controlled" is trueif 條件 then A else B條件是 null 時會走 else。所以先用 is true、is false 分出兩種確定的情況,最後的 else 才是資料不足。
define "HbA1c Status": if "HbA1c Controlled" is true then 'controlled' else if "HbA1c Controlled" is false then 'uncontrolled' else 'no-result'
規格:CQL Reference: Comparison OperatorsCQL Reference: Logical OperatorsDeveloper’s Guide: Conditional Expressions
日期與時間
時間點和期間的比較,以及日期運算。規則裡最常見的是「某件事發生在量測期間內」。
X during 區間整段落在區間內。左邊是時間點時,意思是這個時間點在區間內,例如查詢一節的
(O.effective as dateTime) during "Measurement Period"。define "Encounters In Period": [Encounter] E where E.period during "Measurement Period"X before YX after Y早於、晚於。start of、end of 取出區間的開頭或結尾:這裡是量測期間開始以前的 HbA1c。
define "HbA1c Before Period": [Observation: "HbA1c"] O where (O.effective as dateTime) before start of "Measurement Period"X 3 months or less before Y加上時間量:Y 之前的 3 個月內;or more 則是 3 個月以上。這裡是量測期間結束前 3 個月內的 HbA1c。
define "HbA1c Near Period End": [Observation: "HbA1c"] O where (O.effective as dateTime) 3 months or less before end of "Measurement Period"X overlaps Y兩段期間有重疊:跨年的住院也算進 2026 年。
define "Encounters Overlapping Period": [Encounter] E where E.period overlaps "Measurement Period"X starts during YX ends during Y只看期間開始或結束的那一刻:結束時間落在量測期間內的就診。
define "Encounters Ending In Period": [Encounter] E where E.period ends during "Measurement Period"start of 區間end of 區間區間的開頭和結尾。量測期間的右端是小括號,所以 end of 是 2027-01-01 的前一毫秒。
start of "Measurement Period" // @2026-01-01T00:00:00.000 end of "Measurement Period" // @2026-12-31T23:59:59.999years between A and B兩個時間之間經過幾個完整的單位,單位可以是 years、months、days 等。
years between @1960-05-01 and @2026-10-28 // 66 days between @2026-03-01 and @2026-03-15 // 14difference in years between A and B跨過幾次單位的界線。12 月 31 日到隔天只差一天:完整的年是 0,但跨過一次年份界線,所以是 1。
years between @2025-12-31 and @2026-01-01 // 0 difference in years between @2025-12-31 and @2026-01-01 // 1日期 + 時間量日期 - 時間量日期加減。Today() 是執行當天,Today() - 90 days 就是 90 天前。
@2026-01-15 + 3 months // @2026-04-15 Today() - 90 daysduration in days of 區間區間的長度:3 月 1 日到 3 月 4 日是 3 天。
duration in days of Interval[@2026-03-01, @2026-03-04] // 3AgeInYears()AgeInYearsAt(時間)病人年齡,依 Patient 的 birthDate 計算,沒有生日時是 null。AgeInYears() 以執行當天計算;AgeInYearsAt 以指定的時間計算,結果不會因為哪天執行而改變,品質指標多用這個。
AgeInYears() >= 18 AgeInYearsAt(end of "Measurement Period") >= 18
規格:Author’s Guide: Timing and Interval OperatorsCQL Reference: Date and Time OperatorsCQL Reference: Clinical Operators
函式
同樣的判斷要用很多次時,可以寫成函式。函式和 define 一樣放在 library 裡,也能被其他 library 引用。
define function "名稱"(參數 型別): 運算式參數要寫型別,這裡的 Observation 是 FHIR 的 Observation;呼叫時把值放進括號。要寫明回傳型別,可以在括號後面加
returns Boolean。define function "Is Final"(obs Observation): obs.status in { 'final', 'amended', 'corrected' } define "Final HbA1c": [Observation: "HbA1c"] O where "Is Final"(O)define fluent function 名稱(參數 型別): 運算式加了 fluent,就能接在值後面用點號呼叫:O.isFinal() 等於 isFinal(O)。這是 CQL 1.5 加入的寫法。
define fluent function isFinal(obs Observation): obs.status in { 'final', 'amended', 'corrected' } define "Final eGFR": [Observation: "eGFR"] O where O.isFinal()別名.函式(參數)呼叫 include 進來的 library 裡的函式,前面加 called 指定的別名。
define "HbA1c Dates": "HbA1c In Period" O return FHIRHelpers.ToDateTime(O.effective as dateTime)
三值邏輯與 null
CQL 用 null 表示「不知道」,例如病歷裡查不到某項檢驗。null 會一路往下傳:大部分運算只要有一個輸入是 null,結果就是 null。少數運算一定回傳 true 或 false,判斷缺資料時就靠它們。
| 運算式 | 結果 | 說明 |
|---|---|---|
true and null | null | 還要看 null 那一邊,所以無法確定。 |
false and null | false | 已經有一邊不成立,另一邊是什麼都一樣。 |
true or null | true | 已經有一邊成立。 |
not null | null | 不知道的反面還是不知道。 |
null = 7 | null | 比較遇到 null,結果就是 null。 |
null ~ 7 | false | 等價只會回傳 true 或 false。 |
null is null | true | 檢查 null 的運算不會回傳 null。 |
null is true | false | 只有 true 才成立。 |
Count({ 1, null, 3 }) | 2 | Count 不算 null。 |
Coalesce(null, 'unknown') | 'unknown' | 回傳第一個不是 null 的值。 |
if null then 'yes' else 'no' | 'no' | 條件是 null 時走 else。 |
Coalesce 和 if 會把「不知道」變成答案:Coalesce("HbA1c Controlled", false) 會把沒有 HbA1c 結果的病人算成控制不佳。判斷是否符合條件時,先用 is true、is false 分出確定的兩種結果,剩下的才是資料不足,見結果與 is true。
and、or、not 的完整真值表在三值邏輯;想自己切換條件看結果怎麼變,可以用情境模擬器。
規格:CQL Reference: Nullological OperatorsDeveloper’s Guide: Nullological Operators