← 回首頁

DuckDB 四個回 NULL 而不報錯的行為

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

一隻圓潤的鴨子玩具站在一疊試算表紙上,紙張邊緣有幾格被打成空白。

報錯是好結果。它會讓 pipeline 停下來、指出行號,然後有人去修。貴的失敗是那種 會回傳一個數字的。

DuckDB 特別寬容,而它四個日常行為會給你一個看起來很合理的錯答案,而不是抱怨。

挑一個,看真實輸出

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 的整數 除法截斷不同。要截斷行為請用 `//`。