網站報表選了某一天,結果只出現零點資料;把結束時間改到23:59:59,又少了最後一秒裡的小數時間。查詢先別怪資料沒寫入:DATETIME欄位包含時間,而日期輸入並不代表「整天的所有時刻」。用當日開始到次日開始的半開區間,可以把這個邊界寫清楚。
BETWEEN兩端都包含,但日期不是一整天
BETWEEN包含下限與上限。在本篇DATETIME案例中,兩端都寫同一個日期,得到的是同一天午夜到同一天午夜,不是從早到晚。日期與日期時間在比較時會涉及型態轉換,因此最好先寫出你實際要比較的兩個時刻,而不是靠「選了一天」這句介面文案猜測SQL意思。
SELECT id
FROM events
WHERE occurred_at BETWEEN
'2026-09-20' AND '2026-09-20';
這個查詢在本次資料只找到id 1,也就是00:00:00那筆。若表裡沒有午夜資料,可能一筆都找不到。它不代表BETWEEN不能用,而是兩個界線相同;即使使用者輸入同一天,應用程式仍要把需求轉成正確的時間區間。
用三筆邊界資料實測
案例使用自有隔離MySQL 8.4.9,欄位明確設定DATETIME(6),讓微秒能保留下來。三筆分別是當日午夜、當日最後一個微秒,以及次日午夜。所有字面值沿用同一個時間約定,沒有在案例裡做時區換算,也沒有操作正式報表資料。
CREATE TABLE events (
id INT PRIMARY KEY,
occurred_at DATETIME(6) NOT NULL
);
INSERT INTO events VALUES
(1,'2026-09-20 00:00:00'),
(2,'2026-09-20 23:59:59.999999'),
(3,'2026-09-21 00:00:00');
如果欄位只是DATETIME而沒有指定小數精度,不能直接宣稱同樣保存六位微秒。先用SHOW CREATE TABLE核對真實欄位,再選符合其精度的測試資料。本例刻意用DATETIME(6),就是為了讓「只寫到整秒的結束時間」與「該秒內稍後的資料」出現實際差異。
結束寫23:59:59,仍會漏掉小數秒
SELECT id
FROM events
WHERE occurred_at BETWEEN
'2026-09-20 00:00:00'
AND '2026-09-20 23:59:59'
ORDER BY id;
這次仍只得到id 1。id 2的時間是23:59:59.999999,比23:59:59更晚,所以不在區間內。把上限寫成23:59:59.999999可以配合目前六位精度,但往後換欄位或組合不同時間來源時,仍得重新考慮精度。用次日開始作排他上限通常更容易表達整天的需求。
也不要直接把上限改成次日00:00:00後繼續用BETWEEN,因為上限仍會被包含,id 3就可能落入前一天的報表。如果隔天的報表又包含自己的午夜,同一筆資料可能被兩天重複計算。問題的關鍵是上限是否包含,不是換一個看起來更晚的日期就完成了。
改成大於等於開始,小於次日開始
SELECT id
FROM events
WHERE occurred_at >= '2026-09-20'
AND occurred_at < '2026-09-21'
ORDER BY id;
這個條件包含當日開始,排除次日開始,實測得到id 1與2,沒有id 3。它常被稱為半開區間:左端包含,右端不包含。當日最後的資料只要早於次日午夜,就不用把「最後一個可能的時間」硬寫成某個小數精度。
如果介面選了九月二十日至二十二日,而且意思是三天全包含,右端應轉成二十三日開始,再用小於比較。不要直接把使用者選的結束日當排他界線,否則最後一天會全部被排除。前台日期區間與資料庫時間區間的定義,要在轉換處對齊。
對相鄰區間也能直接驗證:第一天用二十日開始到小於二十一日開始;第二天用二十一日開始到小於二十二日開始。次日午夜只屬於第二段,既不會同時被計兩次,也不會在兩段間留下空隙。這個性質來自界線寫法,不是資料庫自動猜測報表日期。
時間型態與時區仍要另外核對
本例DATETIME保存的是日期時間值,沒有TIMESTAMP那種依連線時區進行的儲存與讀取轉換。但這不表示它天然知道香港時間或UTC:系統必須自行約定存入值代表什麼時間。若應用程式把UTC寫進DATETIME,查香港某一天時就要先依約定換算兩個界線。
TIMESTAMP與DATETIME不能因為畫面都像日期時間,就用同一段說明保證所有結果相同。查看欄位型態、應用程式時區與連線time_zone,再確認兩個界線和資料處於相同時間座標。本篇沒有測跨時區或夏令時間,因此不把固定的一天直接保證為所有地區的二十四小時。
應用程式產生次日界線時,使用可靠的日期時間處理方式,保留日期、時區與型態。不要把任意字串拼成SQL;透過參數綁定傳入已計算的開始與結束值。若需要動態欄位名稱,另由可信的欄位清單處理,參數綁定本身不能把欄位名稱變成安全的查詢結構。
以三個界線點驗收,再看查詢效能
若表單顯示的日期與SQL參數不一致,先在開發環境記錄最終傳入的兩個界線。瀏覽器、後端框架與資料庫都可能處理日期,但報表查詢最終比較的是傳入值;只看介面選了什麼,無法判斷哪一層把日期轉錯。保留可重現參數,才能對準轉換位置。
相鄰日期切片也可用總集合核對:第一段與第二段不應共享午夜那筆,兩段合併後應回到原來兩天的資料。這是邊界驗收,並不要求修改正式資料;小表中的三個時間點已足以看出包含與排除的差異。
重現時先查完整三筆時間,確認小數位真的保存。依序跑同日起迄、整秒結束與半開區間,預期id集合是1、1、1和2。把集合與SQL一起保存,比只截總筆數更容易看出究竟漏了哪個界線。正式資料不必照測試日期改值,另建小表驗證即可。
真實表若有occurred_at為NULL的列,這兩個範圍比較不會把它們當成某一天的資料。是否要在報表裡另列「日期未填」,是另一個業務選擇,不應用零日期或任意午夜代替來湊數。本篇欄位NOT NULL,沒有把缺值案例算進三筆實測。
寫好範圍後,再用實際查詢和索引核對執行方式。欄位保持原值比較,通常也比先把每列轉成日期更容易檢查範圍條件;但這篇沒有做大量資料性能測試,不把查詢改寫冒充索引加速保證。先確保時間邊界正確,再按正式資料量評估索引與計畫。
日期篩選驗收不只測中午的一筆。把當日午夜、最後小數秒與次日午夜都放進測試,才能知道開始有沒有包含、結束有沒有漏掉,以及次日是否誤入。這三個小點核對清楚,報表的一天才真正對上讀者選的一天。

評論0