在數據庫系統的核心層,查詢優化器如同一位精明的策略家,不斷分析數據特徵並制定最優執行計劃。Apache Doris 作為一款高性能的 MPP 分析型數據庫,其優化器內置的 Data Trait 分析機制,通過挖掘數據內在的統計特徵和語義約束,為查詢優化提供了基礎設施。讓我們一起來探索這個強大的功能!
什麼是 Data Trait?
想象一下,如果你能提前知道數據的 “性格特徵”,是不是就能更聰明地處理它們?Data Trait 正是這樣一種對查詢數據和中間結果的 “性格描述”。在 Doris 中,它目前實現了四種關鍵特徵:
- 唯一性(Uniqueness):數據的 “身份證” “在這個世界上,我是獨一無二的!”—— 當某列數據這樣 “宣稱” 時,它就具有唯一性特徵。數學上表示為:NDV(不同值的數量) = 表的總行數。
- 均勻性(Uniformity):數據的 “複製粘貼” “我們全都一樣!”—— 當一列數據都是相同值時,它就展現出均勻性。具體指非空不同值數量不超過 1。 有趣事實:這種列就像軍隊的制服,整齊劃一,優化器看到它們可以採取特殊處理策略。
- 等值集(Equal Set):數據的 “雙胞胎” “我們形影不離,永遠相同!”—— 當兩列數據在所有行中都完美匹配時(包括 NULL 值),它們就構成等值集。 Doris 的等值集判斷是 NULL 敏感的,NULL ≠ NULL 哦!
- 函數依賴(Functional Dependency):數據的 “因果關係” “只要知道 X,就必然知道 Y!”—— 當一組列(X)能唯一決定另一組列(Y)的值時,就存在函數依賴。 X 稱為決定因素(Determinant),Y 稱為被決定因素(Dependecy) 定義如下: ∀X, Y ⊆ R, X → Y ⇔ ∀t1, t2 ∈ R, t1 [X] = t2 [X] ⇒ t1 [Y] = t2 [Y] 其中,t [X] 表示元組 t 在屬性集 X 上的投影。
Data Trait 的表示
唯一性
唯一性使用 UniqueDescription 描述, 想象一個公司的員工管理系統:
- 獨立唯一性(slots):就像每個員工的工牌 ID(如 101、102),這些值在整個公司內是獨一無二的
- 聯合唯一性(slotSets):例如“部門+姓名”的組合,單獨看部門可能重複(多個研發部員工),單獨看姓名也可能重複(同名員工),但“研發部+張三”的組合在全公司是唯一的
均勻性
均勻性使用 UniformDescription 描述, 包括:
-
已知值的均勻列(有具體 value):
- 比如查詢 WHERE department='研發部',這時 department 列在結果集中所有值都是“研發部”
- 類似 SELECT 1 as const_value 中的 const_value 列,所有行都是 1
-
未知值的均勻列(無具體 value):
- 例如 LIMIT 1 後的所有列,雖然知道它們值相同,但不確定具體是什麼值
等值集
等值集採用並查集(一種高效的數據結構)來管理,就像家族關係網:
每個數據列最初都是獨立的“個體”, 當發現兩列值完全相同時(如存在謂詞 a = b),它們就被劃歸到同一個家族,最終所有相等的列會形成一棵棵“家族樹”,樹中的成員彼此等價。
函數依賴
函數依賴關係使用有向圖實現, 就像公司彙報關係:
- 節點:代表一組數據列(如員工 ID、部門 ID 等)
-
邊( → ):表示決定關係,比如:
- 員工 ID → 員工姓名(知道 ID 就能確定姓名)
- 部門 ID+項目 ID → 項目經理(聯合決定)
這個關係網具有傳遞性:如果 A→B 且 B→C,那麼 A→C
Data Trait 是如何推導出來的?
- 逐層調查:從查詢計劃的最底層開始,每個處理節點(如掃描、過濾、連接等)都會根據自身特點生成對應的數據特徵報告
- 懶加載機制:只有當優化器真正需要這些特徵時才會進行計算,避免不必要的分析工作
- 特徵合成:高層節點會綜合下層節點的特徵信息,並結合自身操作特點,生成新的特徵描述。 同時,Data Trait 之間也可以相互推導:具有 unique 屬性的 slot 能決定所有其他 slot;具有 uniform 屬性的 slot 依賴於所有其他的 slot;相等的 slot 互相依賴;相等的 slot 具有相同的 Unique 屬性和 Uniform 屬性。
Data Trait 的推導過程示例
CREATE TABLE employees (
emp_id INT NOT NULL,
emp_name VARCHAR(100),
email VARCHAR(100),
dept_id INT,
salary DECIMAL(10,2),
hire_date DATE
) UNIQUE KEY(emp_id) DISTRIBUTED BY HASH(emp_id) PROPERTIES('replication_num'='1');
SELECT dept_id, COUNT(*) AS emp_count FROM employees
GROUP BY dept_id;
以上面的 SQL 為例,説明一下 DATA TRAIT 的推導過程,查詢計劃如下:
[Aggregate]
|
[Scan]
基表掃描層
當掃描 employees 表時,優化器發現:
- 唯一標識:emp_id 是 UNIQUE KEY,具有唯一性特徵
- 函數依賴:由於 emp_id 是 UNIQUE KEY,它可以決定表中所有其他列的值(知道員工 ID 就能確定他的部門、姓名等信息)
聚合操作層
進行 GROUP BY 聚合時,數據特徵發生了轉變:
-
唯一性變化
- 新增的特性:dept_id 是分組鍵,具有唯一性特徵;
- 丟失的特性:原本 emp_id 的唯一性不再有效,因為多行數據被摺疊成了分組形式。
-
函數依賴變化
- 新增的關係:現在 dept_id 成為新的“決定因素”,可以確定該部門的員工數量 emp_count(就像知道部門編號就能查到該部門的人數統計)
- 丟失的關係:原先基於 emp_id 的所有函數依賴都失效了,因為員工級別的信息已被聚合操作摺疊。
Data Trait 如何優化查詢?魔法般的規則應用
下面通過完整示例演示 Data Trait 如何在實際查詢優化中發揮作用。先從建表開始,逐步展示優化器如何利用數據特徵進行優化。
準備測試環境
建表語句和插入數據 SQL 如下:
-- 員工表(包含唯一ID)
DROP TABLE IF EXISTS employees;
CREATE TABLE employees (
emp_id INT NOT NULL,
emp_name VARCHAR(100),
email VARCHAR(100),
dept_id INT,
salary DECIMAL(10,2),
hire_date DATE
) UNIQUE KEY(emp_id) DISTRIBUTED BY HASH(emp_id) PROPERTIES('replication_num'='1');
-- 部門表(包含唯一ID)
DROP TABLE IF EXISTS departments;
CREATE TABLE departments (
dept_id INT NOT NULL,
dept_name VARCHAR(100),
location VARCHAR(100)
) UNIQUE KEY(dept_id) DISTRIBUTED BY HASH(dept_id) PROPERTIES('replication_num'='1');
-- 訂單表(包含唯一ID)
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
order_id INT NOT NULL,
customer_id INT,
order_date DATE,
amount DECIMAL(10,2)
) UNIQUE KEY(order_id) DISTRIBUTED BY HASH(order_id) PROPERTIES('replication_num'='1');
-- 插入測試數據
INSERT INTO departments SELECT number, concat('name',cast(number as string)), concat('location', cast(number as string)) from numbers("number"="30");
INSERT INTO employees SELECT number, concat('name',cast(number as string)), concat('email',cast(number as string)),number % 30, number, '2025-01-01' from numbers("number" = "100000000");
INSERT INTO orders VALUES
(1001, 5001, '2023-01-10', 999.99),
(1002, 5001, '2023-02-15', 1499.99),
(1003, 5002, '2023-01-20', 799.99),
(1004, 5003, '2023-03-05', 2499.99);
Data Trait 優化實戰演示
根據連接鍵唯一性消除連接(ELIMINATE_JOIN_BY_UK)
場景:在進行 left join 時,右表的連接鍵是唯一的(允許存在多個 NULL 值),且查詢只需要左表數據時...
魔法:直接去掉這個 Join!因為右表要麼匹配一行,要麼不匹配,不影響左表數據完整性。
示例如下:
-- 原始查詢
SELECT COUNT(emp_id) FROM (
SELECT e.emp_id
FROM employees e
LEFT OUTER JOIN departments d ON e.dept_id = d.dept_id) t;
-- 優化後等效查詢
SELECT COUNT(emp_id) FROM employees e;
關閉 ELIMINATE_JOIN_BY_UK 優化時(使用 set disable_nereids_rules = 'ELIMINATE_JOIN_BY_UK'關閉優化),執行時間為 0.1sec, 在開啓 ELIMINATE_JOIN_BY_UK 優化時,執行時間為 0.05sec,性能提升了 100%。
根據唯一性消除冗餘聚合鍵(ELIMINATE_GROUP_BY)
場景: 當 Group By Key 列中存在具有 unique 並且非空屬性的列時...
魔法:可以直接把 Group By 刪除
--原始查詢
SELECT COUNT(c2) FROM
(SELECT emp_id c1, sum(salary) c2 from employees GROUP BY emp_id, emp_name) t;
-- 優化後等效查詢
SELECT COUNT(c2) FROM
(SELECT emp_id c1, salary c2 from employees) t;
在關閉 ELIMINATE_GROUP_BY 優化時,執行時間為 0.96sec, 在開啓 ELIMINATE_GROUP_BY 優化時, SQL 的執行時間是 0.08sec, 性能提升了 1100%。
消除存在依賴關係的聚合鍵(ELIMINATE_GROUP_BY_KEY)
場景:Group By 多列,並且這些列中具有函數依賴關係
魔法:使用函數依賴中被決定的 slot 可以在 Group By Key 列中被消除。
示例如下:
--案例1: 根據函數提供的函數依賴消除
-- email -> SUBSTR(email, 1, INSTR(email, '@')-1)
-- 原始查詢
SELECT count(email) FROM (SELECT email
FROM employees
GROUP BY email, SUBSTR(email, INSTR(email, 'l')+1)) t;
-- 優化後等效查詢
SELECT count(email) FROM (SELECT email
FROM employees
GROUP BY email) t;
--案例2: 根據等值集提供的函數依賴消除
-- 原始查詢
SELECT COUNT(*)
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
GROUP BY e.dept_id, d.dept_id;
-- 優化後等效查詢
SELECT COUNT(*)
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
GROUP BY e.dept_id;
以案例 1 為例,在關閉 ELIMINATE_GROUP_BY_KEY 優化時,執行時間為 2.11s, 在開啓 ELIMINATE_GROUP_BY_KEY 優化時,SQL 的執行時間為 1.41sec, 性能提升了 50%。
消除均一列的聚合鍵(ELIMINATE_GROUP_BY_KEY_BY_UNIFORM)
場景:Group By 的列所有值都相同...
魔法:直接去掉 Group By,因為所有行都屬於同一組。
示例如下:
-- 原始查詢
SELECT hire_date, max(salary) FROM employees WHERE hire_date='2025-01-01' GROUP BY dept_id,hire_date;
-- 優化後等效查詢
SELECT '2025-01-01', max(salary) FROM employees WHERE hire_date='2025-01-01' GROUP BY dept_id;
在關閉 ELIMINATE_GROUP_BY_KEY_BY_UNIFORM 優化時,執行時間為 0.15sec,在開啓 ELIMINATE_GROUP_BY_KEY_BY_UNIFORM 優化時,SQL 的執行時間是 0.11sec,性能提升了 36%。
消除無意義排序(ELIMINATE_ORDER_BY_KEY)
場景:按唯一鍵排序, 或者排序鍵中包含具有函數依賴時。
魔法:去掉這個 Order By,因為數據已經自然有序, 去掉函數依賴中被決定的 Key。
示例如下:
-- 案例1:唯一性推導的函數依賴簡化
SELECT sum(c1) FROM
(SELECT emp_id c1, emp_name c2
FROM employees
ORDER BY emp_id,emp_name,email,dept_id,salary,hire_date
LIMIT 1000000) t;
-- 優化後等效查詢
SELECT sum(c1) FROM
(SELECT emp_id c1, emp_name c2
FROM employees
ORDER BY emp_id
LIMIT 1000000) t;
-- 案例2:表達式推導的函數依賴簡化
SELECT hire_date, EXTRACT(YEAR FROM hire_date), EXTRACT(MONTH FROM hire_date)
FROM employees
ORDER BY hire_date, EXTRACT(YEAR FROM hire_date), EXTRACT(MONTH FROM hire_date);
-- 優化後等效查詢
SELECT hire_date, EXTRACT(YEAR FROM hire_date), EXTRACT(MONTH FROM hire_date)
FROM employees
ORDER BY hire_date;
-- 案例3: 多列表達式依賴簡化
SELECT emp_name
FROM employees
ORDER BY emp_name, email, CONCAT(emp_name, ' ', email);
-- 優化後等效查詢
SELECT emp_name
FROM employees
ORDER BY emp_name, email;
-- 案例4: 根據均勻性消除
SELECT emp_name
FROM employees
WHERE emp_id = 101
ORDER BY emp_id;
--優化後等效查詢
SELECT emp_name
FROM employees
WHERE emp_id = 101;
以案例 1 為例,在關閉 ELIMINATE_ORDER_BY_KEY 優化時,SQL 的執行時間是 0.37sec, 在開啓 ELIMINATE_ORDER_BY_KEY 優化時,SQL 的執行時間是 0.13sec,性能提升了 185%。
優化效果對比
五個優化規則的效果對比如下圖所示,藍色代表關閉優化規則的 SQL 執行時間,橙色代表開啓優化規則的 SQL 執行時間。
最佳實踐
建議做法:為所有唯一鍵列添加明確的 UNIQUE 約束
無論列是否為主鍵,只要具有唯一性特徵(包括業務唯一鍵、組合唯一鍵和外鍵關聯列),都應通過 UNIQUE 約束明確定義。
CREATE TABLE orders (
order_id INT,
order_code VARCHAR(20));
ALTER TABLE orders ADD CONSTRAINT orders_uk UNIQUE (order_id);
優化收益:幫助優化器識別唯一性特徵,實現 Join 消除、Group By 簡化等優化。
建議做法:合理使用 NOT NULL 約束
對業務上不允許為空的列添加 NOT NULL,因為上面提到的一些優化規則,是要求 slot 唯一且非空或者均勻且非空才能應用, 添加 NOT NULL 可以避免 NULL 值對優化的干擾。
CREATE TABLE products (
product_id INT NOT NULL,
product_name VARCHAR(100) NOT NULL);
避免做法:過度使用 SELECT *
-- 應避免的寫法
explain logical plan
SELECT *
FROM employees e
LEFT OUTER JOIN departments d ON e.dept_id = d.dept_id;
-- 推薦寫法
explain logical plan
SELECT e.*
FROM employees e
LEFT OUTER JOIN departments d ON e.dept_id = d.dept_id;
問題原因:例如,優化規則 ELIMINATE_JOIN_BY_UK 能夠應用的條件之一是投影列中只出現 LEFT OUTER JOIN 左表的列, 所以當您只需要左表數據時,SELECT * 的使用會阻礙優化器應用優化規則。
避免做法:避免冗餘的分組鍵和排序鍵
-- 冗餘分組列(即使用者知道product_id決定product_name,但是數據庫系統未識別此依賴,此時product_name為冗餘,可以刪除)
SELECT product_id, product_name
FROM products
GROUP BY product_id, product_name;
通過主動避免冗餘的分組鍵和排序鍵,即使某些函數依賴無法被系統自動識別,您仍然可以提高查詢執行效率, 減少資源消耗, 保持代碼簡潔性。
總結和展望
Data Trait 通過四大核心特徵(唯一性、均勻性、等值集、函數依賴)為查詢優化器提供了深度的數據認知能力:
- 數據特徵識別:精確捕捉數據的本質屬性,如主鍵唯一性、常量列均勻性等
- 查詢語義理解:解析 SQL 操作背後的真實數據關係,識別冗餘操作
- 優化決策支持:為查詢重寫、計劃選擇等優化提供理論依據
Data Trait 的設計採用了高度模塊化的架構,為未來的功能擴展預留了充分空間。特別是在 Uniform 特徵的擴展方面,計劃引入更精細化的取值分佈描述能力。當前 Uniform 主要記錄列值完全均勻(單一值)的情況,下一步將擴展為支持記錄有限離散值的場景。例如當查詢包含 WHERE status IN ('active','pending')這樣的 IN 謂詞過濾時,優化器可以精確獲知 status 列在此查詢上下文中只有 2 個可能的取值。這種擴展後的 Uniform 特徵將為優化器帶來更豐富的決策依據。