關係代數與邏輯最佳化規則 (一): 定義

祝文庄庄主發表於2024-05-08

作者: zhuwenzhuang, 2024.05.08.

閱讀前假設讀者熟悉資料庫使用,瞭解 SQL 的語法和關係運算元的大概含義, 能透過 EXPLAIN 命令檢視資料庫執行計劃.

0 前言

資料庫最佳化器的 查詢最佳化(Query Optimization) 指在查詢等價的前提下, 將代價更高的查詢轉化為代價更低的查詢的過程.
查詢最佳化可以分為 基於規則的最佳化(RBO)基於代價的最佳化(CBO).
在一個典型的最佳化器中RBO和CBO可以不嚴格的對應到最佳化器的邏輯最佳化和物理最佳化階段.

\[\rm{ SQL \xrightarrow[\text{Compiler}]{\text{Parse}} Initial\ Plan \xrightarrow[\text{RBO}]{\text{Logical Optimize}} Logical\ Plan \xrightarrow[\text{CBO}]{\text{Physical Optimize}} Physical\ Plan \xrightarrow[\text{Runtime}]{\text{Execute}} Result\ Data \hspace{100cm} } \]

邏輯最佳化主要是用啟發式規則將邏輯查詢計劃變化為最優形式, 支援消除不必要的計算, 如謂詞下推.
物理最佳化主要是指基於代價模型在多個物理執行計劃中選出最優的物理查詢計劃, 其包含了最優的計算路徑, 比如 join reorder 傾向減少中間產生資料最小的執行計劃.
邏輯最佳化中的 邏輯最佳化規則(Logical Rule) 關注在關係代數這一表示層上, 對查詢進行等價變換最佳化, 相對少的關注物理執行上的選擇.

內容關注邏輯最佳化規則的關係代數表示(Logical rules in relational algebra representation), 目標是作為一個相對完備的邏輯最佳化規則參考, 也可以作為理解最佳化器邏輯最佳化的一個資料.主要討論定義, 規則, 以儘可能簡化+精確的符號表達, 以逐步定義加推理的方式梳理規則, 最終在可視的範圍內表示規則.

使用關係代數符號避免了需要深入具體資料庫最佳化器的程式碼才能理解規則. 在初步瞭解時符號可能會增加識別的難度, 但在更為熟悉後, 符號表示相對虛擬碼表示, 以及畫圖說明都有優勢, 可以作為一個描述最佳化器規則設計的建模語言. 最終產出基於符號表達的規則梳理結果, 可以用於確認已有規則完整性, 以及發現缺失的規則.

主要內容來源於 Guido Moerkotte 所著 Building Query Compiler, 後文用 BQC 指代.

1 基本定義

Value, Type and Domain

每個 型別(Type) 對應到一個 值域(Domain).值(Value) 是在一個型別下值域中的一個點.值域是由有限的離散的值組成, 一個值域中的值之間可以用\(=\)比較, 判斷是否相等.如果值域中的值之間都可以用\(<\) 比較, 則是個有偏序關係的值域.

比如:
INT 型別的值域為 [-2^31, 2^31-1] 中每個整數 以及 NULL. 1 是 INT 型別的一個合法值.
STRING 型別的值域為任意長度的字串 以及 NULL. 'Bob' 是 STRING 型別的一個合法值.
DOUBLE 型別的值域為 [-Inf, Inf] 中每個可精確表示的雙精度浮點數 以及 NULL 和 NaN, 0.0 是 DOUBLE 型別的一個合法值.
INT/DOUBLE/ STRING 型別的值都是可排序的, BINARY 無法用 '<' 比較.
實際系統中往往 STRING/BINARY 有最大長度限制, 所以也可以看做一個有限離散的值域.

NULL 是一個特殊的值, 在每個型別的值域中存在, 用 \(\perp\) 表示.

Tuple

元組是屬性名到值的對映.
*('屬性名'後續簡稱為'屬性')
元組使用[]表示,包含逐項屬性和值. 如:
\(t_1:= [name: \text{"張有志"},\ age: 30]\)
\(t_2:= [job: \text{"Software\ Engneer"}]\)

用表格形式表示

\[t_1 =\def\arraystretch{1.5} \begin{array}{c:c} name & age \\ \hline \rm{張有志} & 30 \end{array} \hspace{100cm} \]

\[t_2 = \def\arraystretch{1.5} \begin{array}{c} job \\ \hline \rm{Software\ Engneer} \\ \end{array} \hspace{100cm} \]

\(t_1\)屬性(Attribute) \(A\) 為:
\(A := \mathcal{A}(t_1) = \{name, age\}\)
\(\mathcal{A}(t)\)獲取屬性函式,表示從元組中獲取屬性集.

使用 \(\circ\) 表示元組的連線(concat):
\(t_1\circ t_2 = [name: \text{"張有志"},\ age: 30,\ job: \rm{"Software\ Engneer"}]\)

用表格形式表示連線結果,

\[t_3 = t_1\circ t_2 = \def\arraystretch{1.5} \begin{array}{c:c:c} name & age & job \\ \hline \rm{張有志} & 30 & \rm{Software\ Engneer} \\ \end{array} \hspace{100cm} \]

使用 \(t.A\) 表示選擇 \(t\) 的一些屬性後剩餘的元組.
\(A_1=\{age, job\}\), 則 \(t_3.A_1 = [age:30, job:\text{Software Engneer}]\)

使用 \(.\) 加屬性名獲取單個屬性對應值\(t_1.age = 30\)

屬性可以看作資料庫表的列名.

Bag

是不去重的元組多重集(multiset).表示上使用下標b區分於一般的集合符號.\(bag:=\{t_1, t_2, ...\}_b\)

後續一般討論的包中, 所有元組都有相同的屬性集合, 同一屬性對應值的型別在多個元組中一致.

Sequence

序列使用\(⟨t_1, t_2, ...⟩\) 表示, 用於表示有序的資料

Relation

關係是一個有相同屬性元組的包, 常用R或者A,B等大寫字母表示.
關係的schema是包的屬性集,用\(sch(R)\)表示.
\(\mathcal{A}(R)\) 表示獲取一個關係的屬性集, 兩者含義等價.
\(\mathcal{A}(R)= sch(R)\)

包的表示

bag 是個無序多重集, 所以對於相同元組可以使用多重度 m 來簡化表示
關係/包的表示示例如下.

基本表示:
\(R = \{[a:1,b:1],[a:1,b:1],[a:2,b:2]\}_b\)

\[\begin{array}{cc} A & B \\ \hline 1 & 1 \\ 1 & 1 \\ 2 & 2 \\ \end{array} \hspace{100cm} \]

多重度表示, \(\mathfrak{m}\) 表示 multiplicity:
\(R = \{[a:1,b:1,\mathfrak{m}:2],[a:2,b:2, \mathfrak{m}:1]\}_b\)

\[\begin{array}{c|cc} \mathfrak{m} & A & B \\ \hline 2 & 1 & 1 \\ 1 & 2 & 2 \\ \end{array} \hspace{100cm} \]

元組 id 表示, 可以表示元組的順序. i 表示元組序號:
\(R = ⟨[a:1,b:1]^{1,3},[a:2,b:2]^2⟩_b\)

\[\begin{array}{c|cc} i & A & B \\ \hline 1 & 1 & 1 \\ 2 & 2 & 2 \\ 3 & 1 & 1 \\ \end{array} \hspace{100cm} \]

注意在資料庫中列名大小寫不敏感, 所以 A 和 a 作為屬性名是一樣的.

Algebra Expression

代數表示式分為標量表示式和關係表示式, 都使用e表示.標量表示式和關係表示式除了返回值不同外,沒有更多的區分.

Operator

運算元(又稱運算子)分為關係運算元(Relational Operator)和標量運算元(Scalar Operator).
運算返回標量的運算元是標量運算元. 運算返回關係的是關係運算元.

Scalar Operator

標量運算元是標量表示式中最小粒度的基本計算函式.標量運算元對應到是資料庫的一般標量函式, 聚合函式, 視窗函式等. 比如 +,-,*,/, TO_DATE, LIKE, MAX, COUNT, SUM, RANK等返回標量的函式都是標量函式.

比較運算元(comparison operator) 是一類返回布林值的二元標量運算元, 用 \(θ\) 表示:
\(aθb, θ ∈ \{=,≤,<,≥,>,\ne \}\)
在基本的比較運算元之外, 其他的運算元也可以構造出偏序關係和等價類, 同樣可以應用和比較運算元類似的規則, 這些不在討論範圍內.

Relational Operator

關係運算元是關係表示式中最小粒度的計算後返回關係的基本計算函式.關係運算元對應到是資料庫的表值函式(Table Valued Function), 如 EXPLODE, TRANS_ARRAY.

使用關係運算元組成關係表示式時,關係運算元可以包含一些標量表示式,如
\(\sigma_p(R)\) 中的 \(p\) 為返回布林型別值的標量表示式, 表示按照 \(p\) 計算結果過濾.

Scalar Expression

標量表示式指返回一個標量值的表示式.一般由標量運算元,自由變數和常量組成, 可以包含關係表示式在內部.

謂詞(Predicate) 是指一個返回 true,false 或 unknown 的標量表示式. 常用 p 表示,從表示式的角度看也可以用 e 表示

標量表示式可以包含關係運算元和關係
\(\sigma_{e_1(t) \neq \emptyset_b, t∈e_2}(e_2)\) 中 predicate 部分是一個返回標量結果的子查詢.
不含關係運算元和關係資料的標量表示式稱為純標量表示式.
比如 a+b 中 + 只接受數值型別, 所以 a+b是一個純標量表示式.

Relational Expression

關係表示式指返回一個'關係'(一組元組)的表示式.一般由關係運算元,自由變數和常量組成.

邏輯運算元 Filter 定義
\(\sigma_p(R) := \{r|r ∈ R, p(r)\}\)
Filter 運算過程: 遍歷關係 R 中每一行(元組) r, p(r)的結果為true則保留.

透過自由變數與繫結的概念, 關係表示式可以消費輸入, 產生輸出, 巢狀成更復雜的關係表示式.

自由變數(Free variable)
指表示式中需要從輸入中獲取繫結值的屬性
以 謂詞 \(p\)\(name='Bob'\) 為例,其中 name 是一個自由變數

獲取自由變數函式 \(\mathcal{F}(e)\)
\(\mathcal{F}(e)\) 表示獲取關係表示式(或標量表示式) \(e\) 中的自由變數集,如:
\(\mathcal{F}(\sigma_{name='Bob'\ ∧\ age > 18}) = \mathcal{F}(name='Bob'\ ∧\ age > 18)= \{name,age\}\)

按照屬性繫結自由變數
對於一個表示式 e 有自由變數集
\(\mathcal{F}(e) = \{a_1,a_2, ...,a_n\}\)\(\mathcal{F}(e) ⊆ \mathcal{A}(t)\) ,
定義
\(e(t) := e[a_1 \leftarrow t.a_1, ..., a_n \leftarrow t.a_n]\)
表示將元組 t 按照屬性繫結在 e 的自由變數上,從而完成求值.
同樣,定義可以擴充套件到包上以及表示式的複合上:
\(e_2(e_1) = e_2(R') = e_2(t_1',t_2',...,t_n')\)

自由變數與屬性繫結示例
比如令關係 \(R=\{\{year:2000,month:12\},\{id:2050,month: 1\}\}_b\) ,
想過濾出 year > 2008 的月份資料時, 令含自由變數的表示式 \(e_1 = \sigma_{year>2008}, e_2=\Pi_{month}\),
則完成自由變數按屬性繫結後的表示式的結果為:
\(R'' = e_2(e_1(R))= \sigma_{year>2008}(\Pi_{month}(R)) = \{\{month:1\}\}_b\)

含自由變數表示式等值的含義
當後續討論兩個含自由變數表示式等價時(如 \(e_1 \equiv e_2\)), 則表示兩個表示式自由變數部分一致.
也就是說, 等價意味著在兩個表示式分別繫結相同的任意輸入時, 產生相同的計算結果.
另外, 為了討論方便, 後續也會用符號 \(=\) 表示與符號 \(\equiv\) 同樣的含義, 即表示式等價.

邏輯關係運算元(Logical Relational Operator)簡稱邏輯運算元(Logical Operator),主要指區別於物理運算元的關係運算元, 不關注物理屬性, 只關注在資料在包表示上的變化.

比如 Join 是個邏輯運算元, 其物理運算元有HashJoin/SortMergeJoin 等,這些物理運算元不會影響在包語義下計算的結果.
物理運算元會影響排序屬性, 從而序列(Sequence)以及分割槽序列(Partitioned Sequence)可以用於描述物理屬性(排序,分佈等)上的規則, 不過這不在這主要的討論範圍內.

2 布林表示式

標量表示式的最佳化一般包括常量摺疊, 表示式化簡, 公共子表示式提取, 輸入範圍(謂詞)提取等.
布林表示式是一類化簡規則明確的標量表示式.

二值邏輯

二值邏輯可以看做由

  • 邏輯運算元 \(∧, ∨, ¬\)
  • 量詞 \(∃,∀\)
  • \(true,false\), 以及一些在比較運算元作用下的其他型別值
  • 比較運算元 \(<, >, \le, \ge, =, \ne\), 兩兩為相反符號
  • 替代符號 \(p, \theta, t\)
  • 表達兩個表示式相等的全等符號 \(\equiv\)

等等符號組成的經典布林代數系統.有很多基於真值表推理的已知的等價規則, 這裡省略不作完整列舉.

比如消去非的規則
\(¬(¬(p)) ≡ p\)
\(¬t_1\theta t_2 ≡ t_1\bar\theta t_2\)

用比較符號的取反消去非例項, 如
\(¬t_1 = t_2 ≡ t_1 \ne t_2\)

另外, 量詞在 SQL 中直接關注較少, 可以認為在子查詢展開部分已經解決, 後續討論也不包含.

二值邏輯的已有規則各種等價規則,在已知資料中沒有 null 時, 可以應用於表示式等價變換(化簡,拆分等), 否則應使用三值邏輯的等價規則.

三值邏輯

一般關係代數中使用的是三值邏輯(Three-Valued Logic), 因為需要處理 null 值.

null 在邏輯表示式中又稱為 unknown, 從後續真值表可以看出將null當做unknown語義來處理.
SQL 中沒有蘊含(entail)運算元, 所以不用討論各種三值邏輯定義.

三值邏輯真值表

由於有了 null 的存在, 在二值邏輯也需要定義 null 在邏輯運算元下的行為.
比如對於非, null 的非也是null.

當擴充套件二值邏輯的真值表後, 對於 \(\neg, ∧, ∨\) 運算元, 真值表為

\[\def\arraystretch{1.5} \begin{array}{c|ccc} \neg & true & false & \perp \\ \hline & false & true & \perp \\ \end{array} \hspace{100cm} \]

\[\def\arraystretch{1.5} \begin{array}{c|ccc} \vee & true & false & \perp \\ \hline true & true & true & true \\ false & true & false & \perp \\ \perp & true & \perp & \perp \\ \end{array}\hspace{100cm} \]

\[\def\arraystretch{1.5} \begin{array}{c|ccc} \wedge & true & false & \perp \\ \hline true & true & false & \perp \\ false & false & false & false \\ \perp & \perp & false & \perp \\ \end{array} \hspace{100cm} \]

梳理真值表時, 可以發現:
如果讓
\(true := 1, false := -1, \perp :=0\)\(\neg := -, ∨ := max, ∧ := min\)
則可以得到一模一樣的乘法表, 也就是說可以用min/max/- 和 0,1,-1 編碼對應到三值邏輯.
比如對於 ∨ := max 的乘法表

\[\begin{array}{c|ccc} \text{max} & 1 & -1 & 0 \\ \hline 1 & 1 & 1 & 1 \\ -1 & 1 & -1 & 0 \\ 0 & 1 & 0 & 0 \\ \end{array} \hspace{100cm} \]

這說明, 對於 min/max/- 的等價規則和對於三值邏輯的等價規則是一樣的.

基於真值表可以得到一些等價規則, 如

\[\begin{align*} true ∨ p ≡& true \\ \perp ∨ p ≡& \perp \qquad //當 p 不為 true \\ false ∨ p ≡& p \end{align*} \hspace{100cm} \]

對應到以下幾個表示式

\[\begin{align*} true ∨ \perp ∨ false ≡& true \\ \perp ∨ false ∨ \perp ≡& \perp \\ false ∨ false ≡& false \\ \end{align*} \hspace{100cm} \]

對於比較運算元, 當兩個值中存在 null 時結果也為 null
擴充套件的等值運算元 \(\doteq\) (加點等號) 將 null 視為一個單獨的量, 用\(\doteq\)做等值判斷不會返回 null.

\[\begin{array}{c|cc} x = y & x \text{ is null} & x \text{ not null} \\ \hline y \text{ is null} & \perp & \perp \\ y \text{ not null} & \perp & x = y \\ \end{array} \hspace{100cm} \]

\[\begin{array}{c|cc} x \doteq y & x \text{ is null} & x \text{ not null} \\ \hline y \text{ is null} & true & false \\ y \text{ not null} & false & x \doteq y \\ \end{array} \hspace{100cm} \]

偏序比較運算元在用於排序(order by)時, 需要顯式指明 null 的順序應該作為 first 還是 last
\(\doteq\) 常用於 group by 與 distinct, 也可以用SQL語句 IS NOT DISTINCT FROM 表示

null 轉換為 true/false

在表示式如 x = 1 < null , x 的求值結果不是true或者false, 而是 null.
比如 select 1 < null; 的結果為 null.
當該表示式在where子句中時, null 結果會被視作 false.
比如 select 1 from t where 1 < null; 最後結果為0行.
對於這類轉換null 為true或false的行為, 稱為 null as true (true-interpreted) 或 null as false (false-interpreted).類似於取頂和取底.
\(⌈·⌉,⌊·⌋\) 表示, 對應真值表為:

\[\begin{array}{c|cc} x & \lceil x \rceil_\perp & \lfloor x \rfloor_\perp \\ \hline true & true & true \\ false & false & false \\ \perp & true & false \\ \end{array} \hspace{100cm} \]

用SQL語句表達則是 IF(x is null then false else x) 或 IF(x is null then true else x)

將 null 轉換成 true/false 的操作可以在巢狀的三值邏輯表示式中下推/上拉.
規則為隨著非運算元變換方向, 且可以分配到 \(∧, ∨\)

\[\begin{align*} ⌈¬x⌉_\perp ≡& ¬⌊x⌋_\perp \\ ⌊¬x⌋_\perp ≡& ¬⌈x⌉_\perp \\ ⌈p_1 ∧ p_2⌉_\perp ≡& ⌈p_1⌉_\perp ∧ ⌈p_2⌉_\perp \\ ⌊p_1 ∧ p_2⌋_\perp ≡& ⌊p_1⌋_\perp ∧ ⌊p_2⌋_\perp \\ ⌈p_1 ∨ p_2⌉_\perp ≡& ⌈p_1⌉_\perp ∨ ⌈p_2⌉_\perp \\ ⌊p_1 ∨ p_2⌋_\perp ≡& ⌊p_1⌋_\perp ∨ ⌊p_2⌋_\perp \end{align*} \hspace{100cm} \]

當上層的 \(⌈·⌉,⌊·⌋\) 下推時, 上層運算元不再需要處理可能為 null 的場景, 從而可以應用上各種二值邏輯的等價變換規則.

\(⌈·⌉,⌊·⌋\) 下推程式碼例項可以參考: Calcite UnknownAs

三值邏輯的等價規則

三值邏輯下二值邏輯成立的規則需要調整, 主要是在等價變換時需要保留返回 null 場景的不變, 以及上面提到的 null 在不同運算元下轉化的規則.

比如比較運算元除了會返回 true, false 之外, 在任意運算元為 null 時返回 null. 所以取反時需要保持 null.
null 謂詞 is null 和 is not null 用於判斷一個值是否為 null, 一定不會返回 null.
and / or 對應到含null值域的求交和求並.
其他的布林表示式運算元按照同樣的方式推理.

基於這個規則變換值域, 可化簡一個巢狀的布林表示式為最簡形式.

三值邏輯化簡程式碼例項: Calcite RexSimplify,Sarg

Conjunction Normal Form / Disjunction Normal Form 轉化已有討論較多, 略.

3 邏輯運算元定義

邏輯運算元分為五部分和一個擴充套件.
五部分: 輸入輸出, 資料重排, 投影過濾, 分組計算, 集合與連線.
一個擴充套件: 巢狀與展開可以看做邏輯運算元的一個特例, 用於將包壓縮成一個標量值, 或將一個標量值展開成一個包.

輸入輸出(Input/Output)

Sink

\(Sink(e)\)表示表的輸出.

TableScan

\(Scan(T)\)表示表的輸入. \(T\) 表示一個表資料的快照.

Values

\(\{t_1, t_2, ...\}_b\)\(Values(e)\)Values 用於表示一個包常量.

Values 的實用意義包括將常量值封裝為一個運算元, 在處理運算元樹時可以統一處理.

TableFunction

\(T_f(e) := \{e_2|e_2 = f(e)\}, f\) 為表值函式

TableFunction 嚴格來說不是一個輸入運算元, 需要一個關係作為輸入, 和後續提到的對映(map)類似. 但其可以一次性消費整個關係上的元組,以及可以從空關係中生成任意多個元組, 超越了一般逐行對映的能力, 所以在處理上看做輸入運算元會更容易, 對這個運算元的最佳化討論較少.

資料重排(Enforcer)

資料重排一般不改變包語義下關係的邏輯屬性, 只改變物理屬性(排序, 分佈).
Limit是一個例外, 其可以將包中元組按照 offset 選取最多 num 個.

\(Enforcer_{dist,coll,limit,offset}(e)\)

細化的 Enforcer:
\(Limit_{num,offset}(e)\)
\(Sort_{coll}(e)\)
\(Exchange_{dist}(e)\)

在後續暫不討論資料重排相關運算元, 因為包語義無法體現資料重排中的排序和分佈屬性.

投影與過濾(Project/Filter)

Filter 運算元

\(\sigma_p(e) := \{x|x ∈ e, p(x)\}\)\(p\) 是個謂詞(predicate),返回布林值作為結果, 注意 \(p\) 中可以巢狀關係運算元等.

計算過程為從 e 中取出逐行元組, 以謂詞 p 求值結果過濾, 過濾剩餘的多個元組構造為一個新的包.
\(p\) 可以巢狀子查詢以用於exists等場景,如\(\sigma_{e_1(t) \neq \emptyset_b, t∈e_2}(e_2)\) 表示只保留 e_2 中滿足 e1 表示式的元組

Project 類運算元

擴充套件投影主要是指投影(Projection), 對映(Map) 兩部分, 去重(Distinct) 和 重新命名(Rename) 也與擴充套件投影相關, 所以統一歸為 Project 類運算元.

在一般的資料庫教材中, 擴充套件投影是一個運算元同時最投影和標量函式計算.
這裡沿用BQC定義的兩個運算元, 分別表達投影和計算.

Project投影選擇部分屬性令 \(A = \{a_1, ... a_n\}\) 為屬性集\(\Pi_A(e):=\{[a_1:x.a_1, ..., a_n:x.a_n] | x ∈ e\}_b\)

計算過程為從 e 逐行取出元組, 按照屬性集選取值, 構造成元組, 並將新構造的多個元組構造為一個新的包.

取剩餘屬性的投影.
\(\Pi_{\overline A}(e) := \Pi_{\mathcal{A}(e)\backslash A}(e)\)

Distinct
\(\Pi^D\) 表示將包中所有元素的多重度 \(\mathfrak{m}\) 置為 1

帶去重(Distinct)的投影, 去重後元組都是唯一的.
\(\Pi_A^D(e):= \Pi^D(\Pi_A(e))\)
\(\Pi^D_A\)的符號雖然是投影,在分析上可以看做一種只做去重聚合的Aggregate.

等價表示:
\(\Pi^D_A(e) = \Gamma_{A;()}(e)\)

計算示例:
\(\Pi^D(\{[a:1,b:1,\mathfrak{m}:2],[a:2,b:2, \mathfrak{m}:1]\}_b)=\{[a:1,b:1,\mathfrak{m}:1],[a:2,b:2, \mathfrak{m}:1]\}_b\)

\(\Pi^D_{\{a\}}(\{[a:1,b:1,\mathfrak{m}:2],[a:2,b:2, \mathfrak{m}:1]\}_b)=\{[a:1,\mathfrak{m}:1],[a:2, \mathfrak{m}:1]\}_b\)

Map對映是執行多個表示式, 並返回結果的運算元.

有的地方將對映和投影表示合併, 作為擴充投影運算元整體看待.
比如 Calcite 的 Project 也同時包含了投影,對映以及下面提到的重新命名.
不過這裡我們沿用經典的符號表示, 將兩者分開.

執行表示式, 並將結果以屬性 a 和原有元組連線:
\(\chi_{a:e_2}(e_1):=\{y\circ[a:e_2(y)]|y ∈ e_1 \}_b\)

執行表示式, 並將結果整體作為輸出:
\(\chi_{e_2}(e_1):=\{e_2(y)|y ∈ e_1 \}_b\)

擴充套件到多個表示式:令含有多個標量函式的 屬性賦值向量(attribute assignment vector):\(F=a_1:e_1,...a_k:e_k\)\(\chi_F(e)=\chi_{a_k:e_k}(...\chi_{a_1:e_1} (e)...) = \{x \circ [{a_1} : e_1(x)) ,..., a_k : e_k(x)) ] | x \in e\}\)相當於多個表示式逐個作用, 並指定結果元組的屬性名.

後續使用 \(\chi_f(e)\) 來表示一般性的 map 運算元.

Rename
重新命名運算元符號為:

\(A = \{a_1, ..., a_n\}, B=\{b_1,...b_n\}\) 為兩個屬性集\(\rho_{A \leftarrow B} :=\Pi_{\overline A}(\chi_{b_1:a_1,..., b_n:a_n}(e))\)

Rename 一般在生成邏輯計劃之前就可以消除. 對於關係代數等價性的討論不重要.

分組計算 (Aggregate/Window) 類

分組計算包含 Aggregate/Window 類的邏輯運算元.

討論聚合相關關係運算元之前, 先定義聚合函式.

聚合函式(Aggregate Function)
聚合函式是一種標量運算元, 典型的有 min/max/sum/count/avg 等, 可以包含 distinct 屬性. 比如 sum 和 sum(distinct) 在 \(e\) 上作用的結果可能不同.

可分解性指聚合函式可以分解成多個聚合函式逐一計算, 比如 \(agg(e)=agg^2(agg^1(e))\)

本文使用Markdown格式編寫, 在文字較多時, 使用 Markdown 原生表格, 符號較多時使用 \(\KaTeX\) 表格.

有以下聚合函式分解表(Decomposition of aggregate functions):

\(agg\) \(agg^1\) \(agg^2\)
min min min
max max max
count count sum0
avg sum,count sum,sum

注意 count 的特殊語義, 無 group key 時, 不返回空集而返回0,所以使用 sum0 表示這種語義.
比如以下計算, 其中c表示繫結自由變數對應的屬性名.
\(\Gamma_{();a:count(c)} (\emptyset_b) = \Gamma_{();a:sum0(c')} (\Gamma_{();c':count(c)} (\emptyset_b)) = \{[a:0]\}_b\)

聚合函式向量
表示一組聚合函式
\(F:=(b_1 :agg_1(a_1),...,b_k :agg_k(a_k)),\)

聚合函式向量中聚合函式逐個分解後,可以表示為:

\[\begin{align*} F^1:=&(b_1' :agg_1^1(a_1),...,b_k' :agg_k^1(a_k))\\ F^2:=&(b_1 :agg_1^2(b_1'),...,b_k :agg_k^2(b_k'))\\ F(e)=&F^2(F^1(e)) \end{align*} \hspace{100cm} \]

重複敏感
重複敏感指部分agg函式會因為資料是否做過去重而改變結果.
重複無關(duplicate agnostic) 函式:
min, max, sum(distinct), count(distinct), avg(distinct)
重複敏感(duplicate sensitive) 函式:
sum, count, avg

重複無關的函式加distinct場景, 顯然可以消去distinct, 如以下SQL變換在 agg 是重複無關函式時成立.
\(\text{select agg(distinct col) ...} \Rightarrow \text{ select agg(col) ...}\)

Aggregate

承載一般標量計算的是 Map 運算元, 而承載聚合函式計算的是 Aggregate 運算元.

Aggregate (單聚合函式場景)定義\(f\) 為一個聚合函式, \(g\) 為聚合函式結果的屬性名, \(G\) 為 group keys 列表.\(\Gamma_{G;g:f}(e):=\{y\circ[g:x]|y\in\Pi_G^D(e), x=f(\{z | z \in e, z.G \dot{=} y.G\}_b)\}_s\)

注意 \(\Gamma_{G;g:f}(e)\) 返回的包中元素是無重複的, 所以是集合.

\(\Gamma_{G;g:f}\) 表示式的求值過程1.獲取去重聚合元組

首先對 e 按照 G 進行去重投影,結果元組逐一列舉為 y

2.以每個去重聚合元組為鍵, 劃分e為多個組

對於每個y, 從e中再次列舉與y在聚合鍵G上相等(\(\dot{=}\))的元組 z, 構造為一個包(對應到一個唯一的y).

3.每組內做聚合計算

對每個 y 構造出來的包逐個作為聚合函式 f 的輸入, 計算出聚合結果 x, 和屬性名 g 組成聚合結果元組 [g:x]
含有distinct的聚合函式, 內部實現需要先計算 \(\Pi^D_{\mathcal{F}(f)}(e)\)結果 (將包按照聚合函式distinct指定列做去重), 再進行逐個元組流式計算. 無distinct的聚合函式可以直接列舉元組流式計算.

4.聚合計算結果與group key元組連線

最終連線 y 和其對應聚合出來的元組 [g:x] 作為結果元組, 多個元組組成一個包.因為聚合鍵是無重複的,所以也可以看做一個元組集合(比包約束更強).

等值的語義問題
\(\dot{=}\) 是把 null 視作常量比較的等於符號.
\(\perp\dot{=}\perp\ is\ true, 1\dot{=}\perp\ is\ false,\)
一般的等號在三值邏輯中遇到null會返回 unknown. grouping 分組所使用的等值比較是二值邏輯\(\dot{=}\),從而可以正確的將 null 獨立分組.

Aggregate (多聚合函式場景)完整定義
基於 group key 列表 \(G\) 和 屬性賦值聚合函式列表 \(F\)聚合運算元表示:

\[\begin{align*} \Gamma_{G;F}(e) :=& \Gamma_{G;b_1:f_1,...,b_k:f_k}(e) \\ :=& \{y\circ[b_1:x_1, ..., b_k:x_k]|y\in\Pi_G^D(e), x_i=f_i(\{z | z \in e, z.G \dot{=} y.G\}_b)\}_s \end{align*} \hspace{100cm} \]

與單聚合函式求值過程的區別為: 對於分組後的\(z\)對應元組包, \(F\) 中多個聚合函式會逐一計算並concat到結果中.
\(\Gamma_{G;g;F}(e)\) 這種表示法在後續也會用到, 其中 \(g\) 表示 \(F\) 的自由變數列表.

預聚合函式列表的符號表示
分裂成預聚合函式後, 對同一個函式有多種表示方式, 分別對應聚合的不同階段.
比如一種可能的劃分方法, 將聚合函式劃分成 4 種執行階段:
(\(F\)上標表示所處階段和子階段)

\(type\ name\) \(common\ name\) \(input\) \(output\) \(symbol\)
Partial 預聚合 Raw Data Intermediate Results \(F^{1,1}\)\(F^1\)
Intermediate 中間聚合 Intermediate Results Intermediate Results \(F^{1,2}\)
Final 最終聚合 Intermediate Results Final Results \(F^2\)
Single 一次性完全聚合 Raw Data Final Results \(F\)

Group Sets定義
group sets 是一種多組 group keys 集合分別進行聚合運算, 並把結果 UNION ALL 在一起的語法糖. 符號表示上增加一個帽子作為區分(\(\widehat{G}, \widehat{\Gamma}\)).

\(\widehat{G} =\{G_1, ... G_n\}\) 為 n 個 分組鍵集合列表 的集合, 有:
\(\widehat{\Gamma}_{\widehat{G};F}(e) := \cup(\Gamma_{G_1;F}(e), ...,\Gamma_{G_n;F}(e))\)

Window

\(W_{w;F}\)
表示一個視窗運算元, \(F\) 表示視窗函式的屬性賦值向量. \(w\) 表示視窗的定義, 包含 partition by, sort by, frame. frame 表示視窗幀的定義, 包含視窗幀模式, 視窗範圍等.

Window 運算元不會改變關係的行數, 也不會刪除原有的屬性, 會增加一些聚合計算結果到關係的屬性中, 和 \(\chi_f\) 行為很像. 區別在於視窗函式處理的範圍不是單行而是一個視窗幀.定義視窗幀涉及到序列和分片, 不是包語義能描述的. 所以後續預設不詳細討論視窗運算元.

瞭解視窗運算元計算過程以及更多說明, 可以參考:
Efficient Processing of Window Functions in Analytical SQL Queries

巢狀與展開(nest/unnest) 類

巢狀是一個特殊的聚合運算元, 展開類運算元屬於一類表值函式.

巢狀與展開是對偶的操作.
對巢狀和展開進行專門的符號定義, 主要目的是幫助到後續的等價性推理過程.

Nest

巢狀指將一個多屬性元組壓縮到一個屬性上.

\(G ⊂ \mathcal{A}(e), \overline{G} = \mathcal{A}(e) \backslash G\)
\(\nu_{G;g}(e):=Γ_{G;g:Π_{\overline{G}}} (e)\)

巢狀是一種特殊的聚合, 將資料按照分組鍵分組後, 將非分組鍵範圍內的多個元組整體作為屬性的值.
如以 {a} 為 G, g為屬性名對示例資料做巢狀:

\([a:1,g:[[b:2,c:3],[b:3, c:\perp]]] = \nu_{a:g}([a:1,b:2,c:3], [a:1, b:3, c:\perp])\)

可以看出, a 作為分組鍵沒有受影響, 同一分組鍵下 b,c 對應的包整體作為屬性 g 的值. 資訊是沒有損失的, 只是a被去重, b,c 被壓縮排一個屬性中, 應該可以透過展開將資料還原.

Unnest 類運算元

展開(Unnest)
展開是一種特殊的表值函式, 執行過程為列舉逐行, 把選定部分元組解巢狀,並逐個和非展開部分連線.
下面第一個表示式處理巢狀的包, 帶有屬性名, 類似於 SQL 中的 array<struct>, struct 成員在展開後成為屬性.
第二個表示式處理類似 array<int> 這種純粹值的集合, 賦予a作為屬性名.

\(\mu_g(e):=\{y.[\mathcal{A}(y) \backslash\{g\}] \circ x | y \in e, x \in y.g\}_b\)\(\mu_{a:g}(e):=\{y.[\mathcal{A}(y) \backslash\{g\}] \circ[a: x] | y \in e, x \in y. g\}_b\)

\([\mathcal{A}(y) \backslash\{g\}]\)表示的是從 y 中屬性去除 g 剩餘的屬性集.
上面表示式執行過程先從 e 中取出每個元組 y, 再從 y.g 包中取出每個元組 x, 最後將 x 和 y 中非 g 部分進行連線.

運算元 \(\mu\)\(\nu\) 互為逆運算,
\(令\ G ⊂ \mathcal{A}(e), 則 R=\mu_g(\nu_{G:g}(R))\)
符號可表示為 \(\mu = \nu^{-1}\).

一個具體示例:
\([a:1,b:2,c:3], [a:1, b:3, c:\perp] = \mu_{g}([a:1,g:[[b:2,c:3],[b:3, c:\perp]]])\)

展開對映(Unnest Map)
展開對映的輸入來源於對映運算元的表示式 \(e_2\), 後續的投影表示丟棄展開的中間結果, 也就是說\(\Upsilon\)是個從\(e_1\)作為資料來源計算\(e_2\)同時並展開的簡化符號表示.

\(\Upsilon_{e_2}(e_1) :=\Pi_{\bar{g}}(\mu_g(\chi_{g: e_2}(e_1)))\)\(\Upsilon_{a: e_2}(e_1) :=\Pi_{\bar{g}}(\mu_{a: g}(\chi_{g: e_2}(e_1)))\)

展開對映後續使用較少, 不做進一步討論

集合與連線 (Set/Join) 類

union all 與 join 是僅有的可以有多個輸入的運算元.join 是二元輸入的運算元, union all可以看做二元輸入運算元,也可以看做多元輸入運算元.

只關注 union all 與 join
集合相關的很多操作,如並集,交集,差集,補集 以及一些 in/not in 的語法對應的實現運算元, 在資料庫系統實現的實踐上,都可以在編譯階段Parser生成關係運算元樹之前比較好的轉換為 union all 和 join 等運算元的組合操作,簡化實現的同時也不損失效能.
所以不用分析 union all 與 join 之外的其他二元/多元運算元.

Union All

\(\cup(e_1, e_2, ....)\)

為union符號增加下標b更準確的區分於集合語義的並, 如 \(\cup_b\).
因為大多數情況下不會討論集合相關的代數, 所以預設\(\cup\)作用於包上表示的是 union all 而非 union.

Join 類

符號與對應 join 名稱

\[\begin{aligned} & × & cross\\ & ⋈ & inner\\ & ⟕ & left\\ & ⟖ & right\\ & ⟗ & full\\ & ⋉ & semi\\ & \vartriangleright & anti \\ \end{aligned} \hspace{100cm} \]

定義

\[\begin{aligned} e_1 × e_2:= & \{y \circ x|y \in e_1, x \in e_2\}_b, \\ e_1 ⋈_p e_2:= & \{y \circ x|y \in e_1, x \in e_2, p(y, x)\}_b, \\ e_1 ⋉_p e_2:= & \{y|y \in e_1, \exists x \in e_2, p(y, x)\}_b, \\ e_1 \vartriangleright_p e_2:= & \{y|y \in e_1, \neg \exists x \in e_2, p(y, x)\}_b, \\ e_1 ⟕_p e_2:= & (e_1 ⋈_p e_2) \cup((e_1 \vartriangleright_p e_2) ×\{\perp_{\mathcal{A}(e_2)}\}), \\ e_1 ⟗_p e_2:= & (e_1 ⋈_p e_2) \\ & \cup((e_1 \vartriangleright_p e_2) ×\{\perp_{\mathcal{A}(e_2)}\}) \\ & \cup(\{\perp_{\mathcal{A}(e_1)}\} ×(e_2 \vartriangleright_p e_1)) . \end{aligned} \hspace{100cm} \]

下標 \(p\) 表示謂詞.
類似 \(\cup((e_1 \vartriangleright_p e_2) ×\{\perp_{\mathcal{A}(e_2)}\})\) 的表示式,表示外連線的補 null 步驟.

內連線, 叉積, 半連線, 反連線的在 \(\sigma\) 下的變換

叉積和內連線的轉換僅在於謂詞繫結的位置不同.
因為 \(\sigma_p(e):= \{x| x \in e, p(x)\}_b\) 所以:

\(\sigma_p(e_1 \times e_2) \equiv e_1 \bowtie_p e_2\)

半連線和反連線的計算是在對主表做過濾, 所以可以轉化為 filter 運算元

\(e_1 \ltimes_p e_2 = \sigma_{\sigma_p(e_2) \neq \emptyset}(e_1) \\ e_1 \vartriangleright_p e_2 = \sigma_{\sigma_p(e_2) = \emptyset}(e_1)\)

謂詞 \((\sigma_p(e_2) \neq \emptyset)(x)\) 作用於 x 時,計算過程為將輸入 x 繫結到 \(\sigma_p(e_2)\)\(p\) 的自由變數, 得到結果, 再判斷是否為空.

定義部分總結

以下給出之前提到的概念擴充關係總結, 也就是如何從基本定義出發推理出這些新增的概念, 也方便將概念串接起來.

從值的擴張出發.

\[\rm{ \begin{aligned} &值 \xrightarrow{\rm{列舉所有值的集合與null}} 值域 \xrightarrow{\rm{將值域分類}}\ 型別 \\ &值 \xrightarrow{屬性} 元組 \xrightarrow{多重集} 包\xrightarrow{共享屬性} 關係 \\ &值 \xrightarrow{計算} 標量函式 \xrightarrow{邏輯相關部分} 二值邏輯 \xrightarrow{\rm{null}} 三值邏輯 \\ &關係 \xrightarrow{計算} 關係運算元\\ \end{aligned} } \hspace{100cm} \]

從資料分析需求出發.

\[\rm{ \begin{aligned} &資料 \xrightarrow{資料邏輯獨立性, 約束資料質量, 簡明} 關係(表格) \xrightarrow{空值處理} \rm{null} \\ &分析 \xrightarrow{在表格上} 關係運算元 \xrightarrow{列選,計算,過濾} \Pi,\sigma \xrightarrow{集合處理能力,含合併, 拆分等} 集合運算元, \cup_b \\ &關係運算元\xrightarrow{分組聚合統計} \Gamma \xrightarrow{排序, 行選} Sort, Limit \xrightarrow{有序分組聚合統計} Window\\ &關係運算元\xrightarrow{多表資料關聯分析} \times, \ltimes, \bowtie,\vartriangleright, ... \\ &關係運算元\xrightarrow{管理資料生命週期} Sink,TableScan, ... \end{aligned} } \hspace{100cm} \]

從提供價值的需求出發.
\(資料\xrightarrow{預測與改進資料需要} 計算 \xrightarrow{預測與改進計算需要} 最佳化器 \xrightarrow{改進最佳化器需要} 更多最佳化規則, 包含邏輯最佳化規則 \xrightarrow{發現和改進邏輯最佳化規則}定義和分析邏輯運算元等價變換\)

運算元定義彙總

\[\begin{align*} \sigma_p(e) :=& \{x|x \in e, p(x)\}_b \\ \Pi_A(e) := & \{[a_1:x.a_1, ..., a_n:x.a_n]| x \in e\}_b \qquad //A = \{a_1, ..., a_n\}_b \\ \Pi^D(e) := & _{\mathfrak m=1}(e)\\ \chi_{a:f}(e) := & \{x \circ [a:f(x)] | x \in e\}_b\\ \chi_{e_2}(e) :=& \{e_2(x) | x \in e\}_b \\ \chi_{F}(e) := & \{x \circ [a_1 : f_1(x) ,..., a_n : f_n(x)) ] | x \in e\}_b \qquad //F = a_1:f_1, ..., a_n:f_n \\ \Gamma_{G;g:f}(e) :=& \{y \circ [g:x] | y \in \Pi_G^D, x = f(\{z|z \in e, z.G \dot= y.G\}) \}_b \\ \Gamma_{G;F}(e):=&\{y\circ [a_1:x_1, ..., a_n:x_n] | y \in \Pi^D_G, x_i = f_i(\{z|z \in e, z.G \dot= y.G \}))\}_b \qquad //F = a_1:f_1, ..., a_n:f_n\\ \nu_{G;g}(e) := & \Gamma_{G;g:\Pi_{\overline{G}}}(e)\\ \mu_g(e):=& \{y.[\mathcal{A}(e)/g] \circ x | y \in e, x \in y.g\}_b\\ \mu_{a:g}(e):=& \{y.[\mathcal{A}(e)/g] \circ [a:g] | y \in e, x \in y.g\}_b\\ \\ e_1\cup_b e_2 :=& _{\mathfrak {m=m_1 + m_2}}(e_1, e_2)\\ e_1 × e_2:= & \{y \circ x|y \in e_1, x \in e_2\}_b, \\ e_1 ⋈_p e_2:= & \{y \circ x|y \in e_1, x \in e_2, p(y, x)\}_b, \\ e_1 ⋉_p e_2:= & \{y|y \in e_1, \exists x \in e_2, p(y, x)\}_b, \\ e_1 \vartriangleright_p e_2:= & \{y|y \in e_1, \neg \exists x \in e_2, p(y, x)\}_b, \\ e_1 ⟕_p e_2:= & (e_1 ⋈_p e_2) \cup((e_1 \vartriangleright_p e_2) ×\{\perp_{\mathcal{A}(e_2)}\}), \\ e_1 ⟗_p e_2:= & (e_1 ⋈_p e_2) \\ & \cup((e_1 \vartriangleright_p e_2) ×\{\perp_{\mathcal{A}(e_2)}\}) \\ & \cup(\{\perp_{\mathcal{A}(e_1)}\} ×(e_2 \vartriangleright_p e_1)) . \end{align*} \hspace{100cm} \]

相關文章