小巧、快速、可靠。
選擇其中三項。
SQLite 的 quirks、注意事項和陷阱

1. 概述

SQL 語言是一個「標準」。即便如此,沒有兩個 SQL 資料庫引擎的運作方式完全相同。每個 SQL 實作都有其自身的特性和怪癖,SQLite 也不例外。

本文檔致力於突顯 SQLite 與其他 SQL 實作之間的主要差異,以幫助正在移植到或從 SQLite 移植的開發人員,或者正在嘗試構建跨多個資料庫引擎運作的系統的開發人員。

如果您是 SQLite 使用者,並且偶然發現了這裡未提到的 SQLite 的一些怪癖,請透過在 SQLite 論壇 上發布簡短訊息來告知開發人員。

2. SQLite 是嵌入式,而非用戶端-伺服器

在將 SQLite 與其他 SQL 資料庫引擎(例如 SQL Server、PostgreSQL、MySQL 或 Oracle)進行比較時,首先必須意識到 SQLite 並非旨在取代或與這些系統競爭。SQLite 是無伺服器的。沒有單獨的伺服器程序來管理資料庫。應用程式使用函數呼叫與資料庫引擎互動,而不是透過向單獨的程序或執行緒發送訊息。

SQLite 是嵌入式且無伺服器的,而不是用戶端/伺服器,這是一個特性,而不是錯誤。

像 MySQL、PostgreSQL、SQL Server、Oracle 等用戶端/伺服器資料庫是現代系統的重要組成部分。這些系統解決了一個重要的問題。但 SQLite 解決的是不同的問題。SQLite 和用戶端/伺服器資料庫都有各自的角色。將 SQLite 與其他 SQL 資料庫引擎進行比較的開發人員需要清楚地理解這種區別。

有關更多資訊,請參閱SQLite 的適用用途文件。

3. 彈性類型

SQLite 在資料類型方面很靈活。資料類型是建議性的,而不是強制性的。

一些評論員說 SQLite 是「弱類型」的,而其他 SQL 資料庫是「強類型」的。我們認為這些術語不準確,甚至帶有貶義。我們更願意說 SQLite 是「彈性類型」的,而其他 SQL 資料庫引擎是「剛性類型」的。

有關 SQLite 中類型系統的詳細討論,請參閱SQLite 中的資料類型文件。

SQLite 的重點在於它對於資料庫中資料的類型非常寬容。例如,如果一個欄位的資料類型是「INTEGER」,而應用程式卻插入一個文字字串到該欄位,SQLite 會先嘗試將該文字字串轉換為整數,就像其他所有 SQL 資料庫引擎一樣。因此,如果將 '1234' 插入 INTEGER 欄位,該值會被轉換為整數 1234 並儲存。但是,如果您將非數字字串(例如 'wxyz')插入 INTEGER 欄位,與其他 SQL 資料庫不同的是,SQLite 不會擲出錯誤。相反地,SQLite 會將實際的字串值儲存在欄位中。

同樣地,SQLite 允許您將 2000 個字元的字串儲存到 VARCHAR(50) 類型的欄位中。其他 SQL 實作可能會擲出錯誤或截斷字串。SQLite 會儲存整個 2000 個字元的字串,不會遺失任何資訊,也不會發出任何警告。

這種情況最終會造成問題,是因為開發人員最初使用 SQLite 進行編碼工作並讓他們的應用程式正常運作,但之後嘗試轉換到其他資料庫(例如 PostgreSQL 或 SQL Server)進行部署。如果應用程式最初利用了 SQLite 的彈性類型,那麼當轉移到對資料類型更嚴格的其他資料庫時,它將會失敗。

彈性類型是 SQLite 的一個特性,而不是錯誤。彈性類型代表自由。然而,我們也認識到這個特性有時會讓習慣於使用對資料類型規則更嚴格的其他資料庫的開發人員感到困惑。回顧過去,如果 SQLite 只是實作一個 ANY 資料類型,讓開發人員可以明確地指出他們何時想要使用彈性類型,而不是將彈性類型設為預設值,或許就不會造成這麼多混淆。為了遷就那些期望嚴格類型的人,SQLite 3.37.0 版 (2021-11-27) 引入了STRICT 資料表的選項。這些資料表要麼強制執行其他 SQL 資料庫引擎中的強制資料類型約束,要麼允許明確的 ANY 資料類型來保留 SQLite 的彈性類型。

3.1. 沒有單獨的 BOOLEAN 資料類型

與大多數其他 SQL 實作不同,SQLite 沒有單獨的 BOOLEAN 資料類型。相反地,TRUE 和 FALSE 通常分別以整數 1 和 0 表示。這似乎沒有造成太多問題,因為我們很少收到相關的抱怨。但務必了解這一點。

從 SQLite 3.23.0 版 (2018-04-02) 開始,SQLite 也將 TRUE 和 FALSE 關鍵字分別識別為整數值 1 和 0 的別名。這提供了與其他 SQL 實作更好的相容性。但為了向後相容,如果存在名為 TRUE 或 FALSE 的欄位,則這些關鍵字會被視為參照這些欄位的識別符號,而不是 BOOLEAN 字面值。

3.2. 沒有單獨的 DATETIME 資料類型

SQLite 沒有 DATETIME 資料類型。日期和時間可以用以下任何方式儲存:

SQLite 的內建日期和時間函式可理解上述所有格式的日期/時間,並可在它們之間自由轉換。您使用哪種格式完全取決於您的應用程式。

3.3. 資料類型是選用的

由於 SQLite 在資料類型方面具有彈性和寬容性,因此可以建立沒有指定任何資料類型的資料表欄位。例如:

CREATE TABLE t1(a,b,c,d);

資料表「t1」有四個欄位:「a」、「b」、「c」和「d」,它們沒有指定任何特定的資料類型。您可以在這些欄位中儲存任何您想要儲存的內容。

4. 外鍵強制執行預設為關閉

SQLite 長久以來都能解析外鍵約束,但很久以後才在 3.6.19 版 (2009-10-14) 加入實際強制執行這些約束的功能。在加入外鍵約束強制執行功能時,已經有數不清的資料庫在流通,其中包含了外鍵約束,而有些約束並不正確。為了避免破壞這些既有的資料庫,SQLite 預設關閉外鍵約束強制執行。

應用程式可以在執行時期使用 PRAGMA foreign_keys 語句來啟用外鍵強制執行。或者,可以使用編譯時期選項 -DSQLITE_DEFAULT_FOREIGN_KEYS=1 在編譯時期啟用外鍵強制執行。

5. 主鍵 (PRIMARY KEY) 有時可以包含 NULL 值

SQLite 表格中的主鍵通常只是一個 UNIQUE 限制。由於歷史上的疏忽,允許主鍵的欄位值為 NULL。這是一個錯誤,但當發現這個問題時,已經有太多流通中的資料庫依賴這個錯誤,因此決定繼續支援這個錯誤的行為。您可以透過在主鍵的每個欄位上添加 NOT NULL 限制來解決這個問題。

例外

6. 聚合查詢可以包含不在 GROUP BY 子句中的非聚合結果欄位

在大部分的 SQL 實作中,聚合查詢的輸出欄位只能參考聚合函數或 GROUP BY 子句中指定的欄位。在聚合查詢中參考一般欄位並不合理,因為每個輸出列可能由輸入表格中的兩個或多個列組成。

SQLite 並未強制執行此限制。聚合查詢的輸出欄位可以是包含未在 GROUP BY 子句中找到的欄位的任意表達式。此功能有兩種用途:

  1. 在 SQLite 中(但我們所知的其他 SQL 實作則不然),如果聚合查詢包含單個 min() 或 max() 函數,則輸出中使用的欄位值將取自達到 min() 或 max() 值的列。如果兩個或多個列具有相同的 min() 或 max() 值,則會從這些列中任意選擇一個列值。

    例如,要查找薪水最高的員工:

    SELECT max(salary), first_name, last_name FROM employee;
    

    在上述查詢中,first_name 和 last_name 欄位的值將對應於滿足 max(salary) 條件的列。

  2. 如果查詢完全不包含聚合函數,則可以添加 GROUP BY 子句來取代 DISTINCT ON 子句。換句話說,會篩選輸出列,以便只顯示 GROUP BY 子句中每組不同值的一列。如果兩個或多個輸出列原本會具有相同的 GROUP BY 欄位值組,則會任意選擇其中一列。(SQLite 支援 DISTINCT,但不支援 DISTINCT ON,其功能由 GROUP BY 提供。)

7. SQLite 預設不會執行完整的 Unicode 大小寫轉換

SQLite 並不完全了解所有 Unicode 字元的大小寫區別。像 upper() 和 lower() 這樣的 SQL 函數僅適用於 ASCII 字元。這有兩個原因:

  1. 雖然現在已經穩定,但在 SQLite 最初設計時,Unicode 大小寫轉換的規則仍在變動中。這表示行為可能會隨著每個新的 Unicode 版本而改變,從而干擾應用程式並損壞索引。

  2. 要進行完整且正確的 Unicode 大小寫摺疊所需的表格比整個 SQLite 函式庫還要大。

如果使用 -DSQLITE_ENABLE_ICU 選項編譯 SQLite 並連結 國際 Unicode 元件 函式庫,則支援完整的 Unicode 大小寫摺疊。

8. 接受雙引號字串字面值

SQL 標準要求識別符號使用雙引號括起來,而字串字面值使用單引號括起來。例如:

SQLite 接受以上兩種用法。但是,為了與 MySQL 3.x(在 SQLite 首次設計時最廣泛使用的關聯式資料庫管理系統之一)相容,如果雙引號字串與任何有效識別符號不符,SQLite 也會將其解譯為字串字面值。

這個設計缺陷意味著拼寫錯誤的雙引號識別符號將被解譯為字串字面值,而不是產生錯誤。它也會讓剛接觸 SQL 語言的開發人員養成使用雙引號字串字面值的壞習慣,而他們真正需要學習的是使用正確的單引號字串字面值形式。

事後看來,我們不應該嘗試讓 SQLite 接受 MySQL 3.x 語法,也不應該允許雙引號字串字面值。然而,有無數應用程式使用雙引號字串字面值,因此我們繼續支援此功能以避免破壞舊版程式碼。

從 SQLite 3.27.0 (2019-02-07) 開始,使用雙引號字串字面值會導致將警告訊息發送到 錯誤日誌

從 SQLite 3.29.0 (2019-07-10) 開始,可以使用 SQLITE_DBCONFIG_DQS_DDLSQLITE_DBCONFIG_DQS_DML 動作搭配 sqlite3_db_config() 在執行階段停用雙引號字串字面值。可以使用 -DSQLITE_DQS=N 編譯時期選項在編譯時期更改預設設定。建議應用程式開發人員使用 -DSQLITE_DQS=0 進行編譯,以便預設停用雙引號字串字面值設計缺陷。如果無法這樣做,則可以使用以下 C 語言程式碼停用個別資料庫連線的雙引號字串字面值:

sqlite3_db_config(db, SQLITE_DBCONFIG_DQS_DDL, 0, (void*)0);
sqlite3_db_config(db, SQLITE_DBCONFIG_DQS_DML, 0, (void*)0);

或者,如果預設停用雙引號字串字面值,但需要針對某些舊版資料庫連線選擇性地啟用,則可以使用與上面顯示的相同的 C 語言程式碼,但將第三個參數從 0 更改為 1。

從 SQLite 3.41.0 (2023-02-21) 開始,在 命令列介面 (CLI) 中預設停用 SQLITE_DBCONFIG_DQS_DDL 和 SQLITE_DBCONFIG_DQS_DML。如果需要,請使用 ".dbconfig" 點命令重新啟用舊版行為。

9. 關鍵字通常可以用作識別符號

SQL 語言包含豐富的關鍵字。大多數 SQL 實作不允許關鍵字用作識別符號(表格或欄位的名稱),除非它們用雙引號括起來。但 SQLite 更具彈性。許多關鍵字可以在不必加引號的情況下用作識別符號,只要這些關鍵字用於可以清楚地看出它們是用作識別符號的上下文中。

例如,以下陳述式在 SQLite 中有效:

CREATE TABLE union(true INT, with BOOLEAN);

由於關鍵字 "union"、"true" 和 "with" 被用作識別符號,因此我們所知的其他所有 SQL 實作都會執行失敗。

將關鍵字用作識別符號的能力可提升回溯相容性。新增新的關鍵字時,恰好使用這些關鍵字作為表格或欄位名稱的舊版結構描述將繼續運作。但是,將關鍵字用作識別符號的能力有時會導致令人驚訝的結果。例如:

CREATE TRIGGER AFTER INSERT ON tableX BEGIN
  INSERT INTO tableY(b) VALUES(new.a);
END;

由前述陳述式建立的觸發程序名為 "AFTER",它是一個 "BEFORE" 觸發程序。"AFTER" 標記被用作識別符號而不是關鍵字,因為這是剖析陳述式的唯一方法。另一個例子:

CREATE TABLE tableZ(INTEGER PRIMARY KEY);

tableZ 表格只有一個名為 "INTEGER" 的欄位。該欄位未指定資料類型,但它是 PRIMARY KEY(主鍵)。由於它沒有資料類型,所以該欄位*不是*表格的 INTEGER PRIMARY KEY(INTEGER 主鍵)。"INTEGER" 標記是用作欄位名稱的識別符號,而不是資料類型關鍵字。

10. 允許存疑的 SQL 且不顯示任何錯誤或警告

SQLite 的原始實作旨在遵循 波斯特爾定律 (Postel's Law),其中一部分指出「寬進嚴出」。這曾經被認為是良好的設計——系統會接受有問題的輸入,並盡力處理而不會過多抱怨。但最近,人們開始意識到,有時嚴格接受輸入會更好,以便更容易發現輸入中的錯誤。

11. AUTOINCREMENT 的運作方式與 MySQL 不同

SQLite 中的 AUTOINCREMENT 功能與 MySQL 中的運作方式不同。這常常會讓最初在 MySQL 上學習 SQL,然後開始使用 SQLite 並期望兩個系統以相同方式運作的人感到困惑。

有關 AUTOINCREMENT 在 SQLite 中的功能和限制的詳細說明,請參閱 SQLite AUTOINCREMENT 文件

12. 文字字串中允許使用 NUL 字元

NUL 字元(ASCII 代碼 0x00 和 Unicode \u0000)可能會出現在 SQLite 字串的中間。這可能會導致非預期的行為。有關更多資訊,請參閱「字串中的 NUL 字元」文件。

13. SQLite 區分整數和文字字面值

SQLite 認為以下查詢返回 false

SELECT 1='1';

它這樣做的原因是整數不是字串。其他所有主要的 SQL 資料庫引擎都認為這是 true,SQLite 的建立者不理解這樣做的原因。

14. SQLite 錯誤處理逗號連接的優先順序

SQLite 賦予所有連接運算子相同的優先順序,並從左到右處理它們。但這並不完全正確。逗號連接的優先順序應低於所有其他連接運算子。換句話說,像這樣的 FROM 子句

... FROM a, b RIGHT JOIN c, d ...

應該解析如下

JOIN JOIN D RIGHT JOIN A B C

但 SQLite 卻將 FROM 子句解析如下

JOIN RIGHT JOIN D JOIN C A B

只有在 FROM 子句中同時使用 RIGHT OUTER JOIN 或 FULL OUTER JOIN 和逗號連接時,這個問題才會造成結果的差異,而這在實務中很少發生。而且可以使用 FROM 子句中的括號輕鬆解決這個問題

... FROM a, (b RIGHT JOIN c), d ...

本頁面最後修改時間:2024-08-14 17:04:32 UTC