CQL Workbench文件
文件目錄
CQL 入門

CQL 語法速查

CQL(Clinical Quality Language,臨床品質語言)讀規則時最常碰到的語法,分組整理成速查表:每一項列出寫法、一句說明和一個小例子。例子都屬於同一份糖尿病追蹤邏輯,資料模型是 FHIR R4。

第一次讀 CQL?先看讀懂工作台的 CQL,它用一條健保條文從頭講到尾;這頁適合邊讀規則邊查。遇到不懂的名詞,可以查名詞表。

檔案結構

一份 CQL 檔案就是一個 library。開頭是一連串宣告,慣例上依下面的順序排列,接著才是 define。這段開頭是本頁所有例子共用的,後面各節的例子都可以直接接在它後面。

DiabetesCare.cql
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 Patient
library 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 裡自己篩選,見下一節。

規格:Author’s Guide: StatementsAuthor’s Guide: Retrieve

查詢

在清單後面加一個別名就成了查詢,別名代表清單裡「目前這一筆」。後面可以接 with/without、where、return、sort 等子句,順序就是這樣。

來源 別名

O 是別名。只有別名的查詢會原樣回傳整個清單,通常還會接下面的子句。

[Observation: "HbA1c"] O
where 條件

只留下條件是 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.period
without 來源 別名 such that 條件

反過來,留下一筆相符資料都找不到的項目:就診期間內沒有驗 HbA1c 的就診。

define "Visits Without HbA1c":
  [Encounter] E
    without [Observation: "HbA1c"] O
      such that (O.effective as dateTime) during E.period
return 運算式

決定每一筆輸出什麼;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 takenAt
sort by 欄位 ascsort by 欄位 desc

排序:asc 由小到大(預設),desc 由大到小。sort by 後面直接寫欄位名稱,不加別名;start of period 是就診開始的時間。

define "Encounters Latest First":
  [Encounter] E
    sort by start of period desc

規格:Author’s Guide: Queries

型別與字面值

CQL 的每個值都有型別。下面是讀規則時最常見的型別和它們的寫法。

Boolean

布林值。查不到資料時是 null,見三值邏輯與 null。

true
false
Integer

整數。

42
Decimal

小數。

7.5
String

文字,用單引號。雙引號包的是名稱,不是文字。

'final'
Date

日期,以 @ 開頭。

@2026-03-15
DateTime

日期加時間,中間用 T 隔開;+08:00 是台灣的時區。

@2026-03-15T08:30:00.000+08:00
Time

只有時間,以 @T 開頭。

@T08:30
Quantity

數值加單位。單位用 UCUM 寫法並加單引號;時間長度用 years、months、days 等英文字,不加引號。

7 '%'
30 'mL/min/{1.73_m2}'
90 days
Interval

區間。中括號包含端點,小括號不包含: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'

規格:Author’s Guide: ValuesUsing CQL with FHIR: UCUM

清單與值集

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") >= 2
First(清單)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'  // false
and or not

且、或、非。遇到 null 的算法見三值邏輯與 null。

define "Adult With Diabetes":
  "Has Diabetes" and AgeInYears() >= 18
A 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 null
X is trueX is false

值剛好是 true(或 false)才成立,null 會得到 false。為什麼需要它,見結果與 is true。

"HbA1c Controlled" is true
if 條件 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.999
years 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  // 14
difference 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 days
duration in days of 區間

區間的長度:3 月 1 日到 3 月 4 日是 3 天。

duration in days of Interval[@2026-03-01, @2026-03-04]  // 3
AgeInYears()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)

規格:Developer’s Guide: Defining Functions

三值邏輯與 null

CQL 用 null 表示「不知道」,例如病歷裡查不到某項檢驗。null 會一路往下傳:大部分運算只要有一個輸入是 null,結果就是 null。少數運算一定回傳 true 或 false,判斷缺資料時就靠它們。

運算式結果說明
true and nullnull還要看 null 那一邊,所以無法確定。
false and nullfalse已經有一邊不成立,另一邊是什麼都一樣。
true or nulltrue已經有一邊成立。
not nullnull不知道的反面還是不知道。
null = 7null比較遇到 null,結果就是 null。
null ~ 7false等價只會回傳 true 或 false。
null is nulltrue檢查 null 的運算不會回傳 null。
null is truefalse只有 true 才成立。
Count({ 1, null, 3 })2Count 不算 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

延伸閱讀