DuckDB 四個回 NULL 而不報錯的行為
DuckDB 以寬容著稱,而那正是問題。四個日常操作會安靜地給你錯的答案——其中一個是同一個檔案 count(*) 成功、SELECT * 直接炸掉。

報錯是好結果。它會讓 pipeline 停下來、指出行號,然後有人去修。貴的失敗是那種 會回傳一個數字的。
DuckDB 特別寬容,而它四個日常行為會給你一個看起來很合理的錯答案,而不是抱怨。
挑一個,看真實輸出
SELECT ([10, 20, 30])[1]
DuckDB 實際回傳什麼
10
正確
上面每一個結果都是 2026-08-06 在 DuckDB 1.4.5 上真的執行後錄下來的。這一頁不跑 DuckDB——DuckDB-WASM 有好幾 MB,而互動頁的 JavaScript 預算是 220KB。
那個元件裡的每一個結果都是在 DuckDB 1.4.5 上真的執行後錄下來的,不是在你的瀏覽器 裡跑——DuckDB-WASM 有好幾 MB,而這一頁的 JavaScript 預算是 220KB。版本印在元件 下方,你可以自己判斷它是不是還適用於你手上那版。
一、list[0] 是 NULL,不是錯誤
DuckDB 的 LIST 跟 SQL 其他地方一樣,索引從 1 開始。讓它從註腳升級成陷阱的, 是你去拿索引 0 的時候會發生什麼事:
SELECT ([10, 20, 30])[1]; -- 10
SELECT ([10, 20, 30])[0]; -- NULL
SELECT ([10, 20, 30])[-1]; -- 30
不報錯。 任何從 Python 或 JavaScript 過來的人都會寫 [0],然後每一列都拿到
NULL,接著要花時間判斷到底是資料是空的、還是查詢寫錯了。負索引可以用而且從尾端
數,這很貼心,同時也是又一種差一位的方法。
二、MAP 取不到鍵是 NULL,STRUCT 取不到鍵是錯誤
這兩個看起來是同一種操作,行為完全不同:
SELECT MAP{'a': 1}['zzz']; -- NULL
SELECT {'a': 1}.zzz; -- Binder Error: Could not find key "zzz" in struct
STRUCT 那個在 bind 階段就失敗,一列資料都還沒讀,而且訊息裡還會列出實際存在的鍵。
那是你要的行為。MAP 做不到——MAP 的鍵是資料不是 schema,DuckDB 在規劃階段
沒有任何方法知道 zzz 永遠不會出現。
也就是說,選 MAP 還是 STRUCT 不只是彈性問題。STRUCT 順便買到一個錯字檢查。 如果你的鍵是固定的,那個檢查比你放棄的彈性值錢。
三、CSV 推斷只看開頭,而 count(*) 會把它藏起來
這是會跑到生產環境的那一個。一個有 20,500 列乾淨整數、最後一列是 N/A 的檔案:
SELECT count(*) FROM read_csv('late.csv');
-- 20501
SELECT * FROM read_csv('late.csv') WHERE id = 99999;
-- Conversion Error: CSV Error on Line: 20502
-- Could not convert string "N/A" to 'BIGINT'
同一個檔案、同一個連線。count 之所以成功是因為投影下推:count(*) 從來不會真的
取出 value 欄位,所以那個壞字串根本沒被轉換。一旦有查詢真的讀那個欄位,就會炸。
實務後果是:一個列數檢查會在一個即將讓下一步壞掉的檔案上通過。 如果你的匯入 流程是「載入 → 計數 → 比對預期 → 繼續」,它會繼續。
掃完整份檔案可以修好推斷:
SELECT typeof(value) FROM read_csv('late.csv', sample_size = -1) LIMIT 1;
-- VARCHAR
代價是完整掃過一次檔案。比較便宜的習慣是把你真的依賴的欄位直接宣告出來, 讓推斷沒有東西可以猜錯。
四、7 / 2 是 3.5
SELECT 7 / 2 AS v, typeof(v); -- 3.5 · DOUBLE
SELECT 7 // 2 AS v, typeof(v); -- 3 · INTEGER
DuckDB 的 / 是真除法,兩個整數相除也回 DOUBLE。PostgreSQL 會截斷。
在兩者之間複製過來的查詢會安靜地改變語意,而且兩邊都是一個數字,
所以沒有任何東西會告訴你。
值得帶走的那個模式
這四個裡有三個是用回傳 NULL 的方式失敗的,而 NULL 會傳播。它穿過 join、 在聚合時以「被跳過的一列」存活下來,最後變成儀表板上一個略低、而沒有人會去查的數字。
所以真正的防禦習慣不是「記住這四個」,而是:當一個欄位莫名其妙全是 NULL 的時候, 先懷疑存取語法,再懷疑資料。 資料通常是好的。
常見追問
為什麼同一個檔案 count(*) 成功、SELECT * 失敗?
投影下推。`count(*)` 從來不會真的取出那個有問題的欄位,所以那個壞掉的值 根本沒有被轉換。一旦有查詢真的讀到那個欄位,轉換才發生,然後丟出錯誤。 也就是說,列數檢查會通過,而 pipeline 仍然會在下一步壞掉。
怎麼避免 CSV 型別推斷猜錯?
傳 `sample_size = -1` 掃完整份檔案,正確但要付一次完整掃描的代價。 比較便宜的習慣是用 `columns` 參數把你真的依賴的欄位型別直接宣告出來—— 推斷沒有東西可以猜錯,就不會給你驚喜。
list[0] 回 NULL 是 bug 嗎?
不是。它與 SQL 對超出範圍的存取的處理一致,而 DuckDB 的 LIST 跟 SQL 其他地方 一樣是 1-based。它是陷阱而不是 bug——危險之處在於 0 正好是寫 Python 或 JavaScript 的人第一個會打出來的索引。
7/2 真的回 3.5 嗎?
是。DuckDB 的 `/` 是真除法,兩個整數相除也回 DOUBLE,這與 PostgreSQL 的整數 除法截斷不同。要截斷行為請用 `//`。