每個 SQL 資料庫引擎的工作方式大致相同:它首先將輸入的 SQL 文字轉換為「預備語句」。然後「執行」預備語句以產生結果。
預備語句是一個物件,表示完成輸入 SQL 所需的步驟。或者,換個角度思考,預備語句是翻譯成電腦更容易理解的形式的 SQL 語句。
在 SQLite 中,預備語句是 sqlite3_stmt 物件 的一個實例。在其他系統中,預備語句通常是一個應用程式開發人員無法直接看到的內部資料結構。其他 SQL 資料庫引擎的開發人員不一定將這些物件稱為「預備語句」。但無論它們的名稱為何,這些物件都存在。本文將使用「預備語句」一詞。
實現預備語句的方法數不勝數。本文將探討兩種最常用的方法
位元組碼 → 輸入的 SQL 被轉換為虛擬機器語言,然後由虛擬機器直譯器執行。這是 SQLite 使用的技術。
物件樹 → 輸入的 SQL 被轉換為表示要執行的處理的物件樹。SQL 透過走訪這棵樹來執行。這是 MySQL 和 PostgreSQL 使用的技術。
預備語句的這兩種表示方法各有優缺點。本文的目的是闡述其中的一些優缺點。
本文檔是從 SQLite 原作者的角度撰寫的。如果您不同意本文檔中提出的任何觀點,歡迎在 SQLite 論壇 上提供更正和/或相反的觀點。或者您可以直接電郵作者。
SQLite 產生的位元組碼 可能與許多讀者認為的位元組碼略有不同。(例如)Java 虛擬機器 或 WebAssembly 使用的位元組碼幾乎完全由低階操作組成,類似於實體 CPU 所執行的操作:基本數學運算子、比較、條件跳躍,以及在不同記憶體位置之間移動內容的指令。SQLite 位元組碼也包含這類低階指令。但 SQLite 位元組碼也包含一些專為資料庫引擎需求設計的高階操作。以下僅列舉幾個例子
OP_Column → 從特定游標目前指向的資料庫列中提取第 N 個欄位的值。
OP_CreateBtree → 在資料庫檔案中配置一個新的 B 樹的空間。
OP_ParseSchema → 重新讀取並重新解析全部或部分 sqlite_schema 表格,並相應地刷新內部符號表。
OP_SeekGE → 將特定 B 樹上的游標移動到第一個大於或等於給定鍵值的項目。
OP_Next → 將特定 B 樹上的游標推進到 B 樹中的下一個項目,如果該 B 樹中沒有更多項目,則跳轉或直接跳過。
換句話說,SQLite 使用的「位元組碼」並不像一組 CPU 指令,而更像是一個按照特定順序執行的資料庫基本操作列表。
「抽象語法樹」或 AST 是一種描述某種形式語言的程式或陳述式之資料結構。在我們的上下文中,這種形式語言是 SQL。AST 通常以物件樹的形式實作,其中每個物件代表整個 SQL 陳述式的一小部分。AST 自然地從形式語言的解析器中產生。常用的技術是使用 LALR(1) 解析器。使用這種解析器,每個終端符號都持有將成為 AST 葉子的中繼資料,而每個非終端符號都持有將成為 AST 子分支的中繼資料。隨著解析器「簡化」文法的規則,AST 的新節點會被分配並連接到子節點。解析完成後,文法的起始符號將持有 AST 的根。
AST 是一個物件樹。但 AST 並不適合作為準備好的陳述式。在產生之後,AST 首先需要以各種方式進行轉換,然後才能執行。需要解析符號。需要檢查語義規則。需要應用將輸入 SQL 陳述式轉換為可以更快執行的不同形式的最佳化。最後,需要將 AST 轉換為更易於執行的替代表示形式。
有些人將 MySQL 和 PostgreSQL 用作可執行形式的物件樹稱為 AST。這可能是對「AST」一詞的誤用,因為當物件樹準備好執行時,它已經發生了很大的變化,與原始 SQL 文字幾乎沒有相似之處。這種混淆部分是由於最終準備好的陳述式物件和原始 AST 都是物件樹。通常的做法是,直接從解析器產生的原始 AST 經過多次遍歷,一點一點地轉換,直到最後完全轉換為一個不再嚴格是 AST 但可以評估以產生結果的物件樹。在此過程中,物件樹何時停止是 AST 並變成準備好的陳述式,並沒有明確的界限。而且因為 AST 和準備好的陳述式之間沒有明確的界限,所以人們通常將以物件樹表示的準備好的陳述式稱為「AST」,即使這種描述並不精確。
資料流程程式設計 是一種程式設計風格,其中個別節點專門負責執行整體計算的一小部分。每個節點從其他節點接收輸入,並將其輸出發送到其他節點。因此,節點形成一個將輸入傳送到輸出的有向圖。
對於 SQL 資料庫引擎用作準備好的陳述式的物件樹,「資料流程程式」可能比「AST」更貼切。
SQLite 編譯成位元組碼,SQLite 開發人員對這種方法非常滿意。原因如下:
扁平的 opcode 列表可以輕鬆列印出來,以確切了解 SQL 陳述式是如何執行的。這就是 SQLite 中當您在 SQL 陳述式前加上 "EXPLAIN" 關鍵字時會發生的事情:它不會實際執行 SQL,而是列出用於執行該 SQL 的位元組碼。
位元組碼很適合這樣做,因為位元組碼程式很容易以表格形式表示。在 SQLite 位元組碼中,每條指令都有一個 opcode 和五個運算元。因此,準備好的陳述式可以像查詢六欄表格一樣呈現。
物件樹表示法比較難以人類可讀的形式發佈。組成樹的物件通常都非常不同,因此很難想出一個一致且簡單的表格表示法來顯示這些物件。任何您想出的表格表示法幾乎肯定會超過六欄,甚至更多。將物件樹呈現為表格的問題非常困難,據我所知,沒有人這麼做。因此,沒有任何物件樹資料庫引擎在其 "EXPLAIN" 輸出中提供 SQLite 所提供的詳細程度。
位元組碼在前端解析和分析與後端評估 SQL 陳述式之間提供了明確的區分。當出現問題(不正確的答案和/或效能不佳)時,開發人員可以檢查位元組碼,以快速確定問題的根源是產品的前端分析還是後端資料儲存部分。
在 SQLite 的除錯建置版本中,PRAGMA vdbe_trace=ON; 命令會導致位元組碼執行的追蹤顯示在控制台上。
以位元組碼編寫的 SQL 陳述式可以逐步評估。例如,可以執行一個陳述式,直到它產生第一列輸出為止。然後該陳述式會暫停,直到再次執行。不必在檢查第一列輸出之前將陳述式執行完成。
這在物件樹設計中比較難以實現。當準備好的陳述式是物件樹時,通常透過遍歷樹來完成執行。要在計算過程中暫停陳述式,意味著要將堆疊回溯到呼叫者,同時儲存足夠的狀態以便從上次停止的地方繼續評估。這並非不可能做到,但非常困難,我從未見過有人真正做到。
大多數 SQL 資料庫引擎並不需要逐步執行準備好的陳述式,因為大多數 SQL 資料庫引擎都是客戶端/伺服器架構。在客戶端/伺服器引擎中,單個 SQL 陳述式會發送到伺服器,然後完整的回覆會一次性透過網路返回。因此,每個陳述式都會一次性執行完成。但 SQLite 不是客戶端/伺服器架構。SQLite 是一個在與應用程式相同的位址空間中執行並使用相同堆疊的程式庫。能夠輕鬆可靠地逐步執行 SQL 陳述式對 SQLite 來說非常重要。
SQLite 產生的位元組碼通常比解析器輸出的相應 AST 更小。在初始處理 SQL 文字期間(在呼叫 sqlite3_prepare() 及類似函式期間),AST 和位元組碼同時存在於記憶體中,因此此時會使用更多記憶體。但這是一個暫態狀態。AST 會很快被丟棄並回收其記憶體,甚至在呼叫 sqlite3_prepare() 返回之前,因此產生的 準備好的陳述式 最終以位元組碼表示法佔用的記憶體比以 AST 表示法更少。這一點很重要,因為對 sqlite3_prepare() 的呼叫是暫態的,但準備好的陳述式通常會被快取以供重複使用,並在記憶體中保留很長時間。
我相信準備好的陳述式(prepared statement)的位元組碼表示法執行速度更快,因為計算的每個步驟需要做出的決策更少。強調前一句中的「相信」→ 要以實驗方式驗證這個說法很困難,因為從來沒有人投入多年的精力去產生等效的位元組碼和物件樹表示法來比較哪個實際上執行得更快。我們確實知道 SQLite 速度非常快,但我們沒有與其他 SQL 資料庫進行良好的並列比較,因為其他資料庫花費大量時間在用戶端/伺服器訊息處理上,而且難以區分訊息往返的開銷和實際的處理時間。
SQLite 開發人員認為位元組碼方法是最好的,至少對於 SQLite 嘗試滿足的應用場景來說是如此,但物件樹處理 SQL 的方法確實比位元組碼有一些優點。總是有取捨的。
當準備好的陳述式是位元組碼時,一旦產生了位元組碼,演算法就固定了,並且在沒有完全重寫位元組碼的情況下無法更改。物件樹的準備好的陳述式則不然。物件樹更容易即時修改。查詢計畫是可變的,並且可以在執行過程中根據查詢的進度進行調整。因此,查詢可以動態自我調整。
在資料流程程式中,每個處理節點都可以分配給不同的執行緒。需要某種執行緒安全的佇列機制來將中間結果從一個節點傳輸到下一個節點。但在程式的每個節點內通常不需要同步原語。節點排程很簡單:當一個節點有可用資料且其輸出佇列中有空間時,它就符合執行條件。
這對於設計在大型多核心伺服器上執行大型分析查詢(OLAP)的資料庫引擎來說是一個重要的考慮因素。SQLite 的主要重點是物聯網上的交易處理(OLTP),因此在 SQLite 中以資料流程程式表示準備好的陳述式的需求較少。
本頁面最後修改時間:2024-05-09 17:38:03 UTC