0、概述
在數據庫的使用中,數據庫的性能往往是至關重要的問題,而數據庫的性能問題最終基本都要涉及到SQL優化。本文就將詳細介紹一些達夢中SQL優化的知識。
1、執行計劃詳解
1.1、執行計劃解讀
無論是什麼數據庫,一般SQL優化我們都需要去查看SQL的執行計劃,瞭解SQL具體是慢在哪裏,才知道從哪裏開始優化。
那麼什麼是執行計劃呢?
執行計劃是SQL語句的執行方式,由查詢優化器爲語句設計的執行方式,交給執行器去執行。在達夢中我們可以在SQL命令行使用EXPLAIN可以打印出語句的執行計劃。
例如下面就是一個最基本的執行計劃:
SQL> explain select * from SYSOBJECTS;
1 #NSET2: [0, 1531, 396]
2 #PRJT2: [0, 1531, 396]; exp_num(17), is_atom(FALSE)
3 #CSCN2: [0, 1531, 396]; SYSINDEXSYSOBJECTS(SYSOBJECTS as SYSOBJECTS)
從上面的執行計劃中我們可以看到哪些信息呢?
- 首先,一個執行計劃由若干個計劃節點組成,如上面的1、2、3。
- 然後我們看到,每個計劃節點中包含操作符(CSCN2)和它的代價([0, 1711, 396])等信息。
- 代價由一個三元組組成[代價,記錄行數,字節數]。
- 代價的單位是毫秒,記錄行數表示該計劃節點輸出的行數,字節數表示該計劃節 點輸出的字節數。
- 拿上面第三個計劃節點舉例:操作符是CSCN2即全表掃描,代價估算是0ms,掃描的記錄行數是1711行,輸出字節數是396個。
1.2、執行計劃操作符介紹
達夢中執行計劃涉及到的一些主要操作符有:
- CSCN :基礎全表掃描(a),從頭到尾,全部掃描
- SSCN :二級索引掃描(b), 從頭到尾,全部掃描
- SSEK :二級索引範圍掃描(b) ,通過鍵值精準定位到範圍或者單值
- CSEK :聚簇索引範圍掃描© ,通過鍵值精準定位到範圍或者單值
- BLKUP :根據二級索引的ROWID 回原表中取出全部數據(b + a)
接下來我們結合實例來介紹下這些操作符:
–準備測試表和數據:
DROP TABLE T1;
DROP TABLE T2;
CREATE TABLE T1(C1 INT ,C2 CHAR(1),C3 VARCHAR(10) ,C4 VARCHAR(10) ); CREATE TABLE T2(C1 INT ,C2 CHAR(1),C3 VARCHAR(10) ,C4 VARCHAR(10) ); INSERT INTO T1
SELECT LEVEL C1,CHR(65+MOD(LEVEL,57)) C2,'TEST',NULL FROM DUAL CONNECT BY LEVEL<=10000;
INSERT INTO T2
SELECT LEVEL C1,CHR(65+MOD(LEVEL,57)) C2,'TEST',NULL FROM DUAL CONNECT BY LEVEL<=10000;
CREATE INDEX IDX_C1_T1 ON T1(C1); SP_INDEX_STAT_INIT(USER,'IDX_C1_T1');
–NSET:收集結果集
說明:用於結果集收集的操作符, 一般是查詢計劃的頂層節點。
SQL> EXPLAIN SELECT * FROM T1;
1 #NSET2: [1, 10000, 156]
2 #PRJT2: [1, 10000, 156]; exp_num(5), is_atom(FALSE)
3 #CSCN2: [1, 10000, 156]; INDEX33555571(T1)
–PRJT:投影
說明:關係的“投影”(project)運算,用於選擇表達式項的計算;廣泛用於查詢,排序,函數索引創建等。
SQL> EXPLAIN SELECT * FROM T1;
1 #NSET2: [1, 10000, 156]
2 #PRJT2: [1, 10000, 156]; exp_num(5), is_atom(FALSE)
3 #CSCN2: [1, 10000, 156]; INDEX33555571(T1)
–SLCT:選擇
說明:關係的“選擇” 運算,用於查詢條件的過濾。
SQL> EXPLAIN SELECT * FROM T1 WHERE C2='TEST';
1 #NSET2: [1, 250, 156]
2 #PRJT2: [1, 250, 156]; exp_num(5), is_atom(FALSE)
3 #SLCT2: [1, 250, 156]; T1.C2 = 'TEST'
4 #CSCN2: [1, 10000, 156]; INDEX33555571(T1)
–AAGR:簡單聚集
說明:用於沒有group by的count,sum,age,max,min等聚集函數的計算。
SQL> EXPLAIN SELECT COUNT(*) FROM T1 WHERE C1 = 10;
1 #NSET2: [0, 1, 4]
2 #PRJT2: [0, 1, 4]; exp_num(1), is_atom(FALSE)
3 #AAGR2: [0, 1, 4]; grp_num(0), sfun_num(1)
4 #SSEK2: [0, 1, 4]; scan_type(ASC), IDX_C1_T1(T1), scan_range[10,10]
–FAGR:快速聚集
說明:用於沒有過濾條件時從表或 索引快速獲取 MAX/MIN/COUNT值,DM數據庫是世界上單表不帶過濾條件下取COUNT值最快的數據庫。
SQL> EXPLAIN SELECT COUNT(*) FROM T1;
1 #NSET2: [1, 1, 0]
2 #PRJT2: [1, 1, 0]; exp_num(1), is_atom(FALSE)
3 #FAGR2: [1, 1, 0]; sfun_num(1),
SQL> EXPLAIN SELECT MAX(C1) FROM T1;
1 #NSET2: [1, 1, 4]
2 #PRJT2: [1, 1, 4]; exp_num(1), is_atom(FALSE)
3 #FAGR2: [1, 1, 4]; sfun_num(1), IDX_C1_T1
–HAGR:HASH分組聚集
說明:用於分組列沒有索引只能走全表掃描的分組聚集,C2列沒有創建索引。
SQL> EXPLAIN SELECT COUNT(*) FROM T1 GROUP BY C2;
1 #NSET2: [2, 100, 48]
2 #PRJT2: [2, 100, 48]; exp_num(1), is_atom(FALSE)
3 #HAGR2: [2, 100, 48]; grp_num(1), sfun_num(1);
4 #CSCN2: [1, 10000, 48]; INDEX33555571(T1)
–SAGR:流分組聚集
說明:用於分組列是有序的情況下可以使用流分組聚集,C1上已經創建了索引,SAGR2性能優於HAGR2。
SQL> EXPLAIN SELECT COUNT(*) FROM T1 GROUP BY C1;
1 #NSET2: [2, 100, 4]
2 #PRJT2: [2, 100, 4]; exp_num(1), is_atom(FALSE)
3 #SAGR2: [2, 100, 4]; grp_num(1), sfun_num(1)
4 #SSCN: [1, 10000, 4]; IDX_C1_T1(T1)
–BLKUP:二次掃描
說明:先使用2級別索引定位,再根據表的主鍵、聚集索引、 rowid等信息定位數據行。
SQL> EXPLAIN SELECT * FROM T1 WHERE C1=10;
1 #NSET2: [0, 1, 156]
2 #PRJT2: [0, 1, 156]; exp_num(5), is_atom(FALSE)
3 #BLKUP2: [0, 1, 156]; IDX_C1_T1(T1)
4 #SSEK2: [0, 1, 156]; scan_type(ASC), IDX_C1_T1(T1), scan_range[10,10]
–CSCN:全表掃描
說明:CSCN2是CLUSTER INDEX SCAN的縮寫即通過聚集索引掃描全表,全表掃描是最簡單的查詢,如果沒有選擇謂詞,或者沒有索引可以利用,則系統一般只能做全表掃描。在一個高併發的系統中應儘量避免全表掃描。
SQL> EXPLAIN SELECT * FROM T1;
1 #NSET2: [1, 10000, 156]
2 #PRJT2: [1, 10000, 156]; exp_num(5), is_atom(FALSE)
3 #CSCN2: [1, 10000, 156]; INDEX33555571(T1)
–SSEK、CSEK、SSCN:索引掃描
說明:
- SSEK2是二級索引掃描即先掃描索引,再通過主鍵、聚集索引、ROWID等信息去掃描表;
- CSEK2是聚集索引掃描只需要掃描索引,不需要掃描表;
- SSCN是索引全掃描,不需要掃描表。
–SSEK
SQL> EXPLAIN SELECT * FROM T1 WHERE C1=10;
1 #NSET2: [0, 1, 156]
2 #PRJT2: [0, 1, 156]; exp_num(5), is_atom(FALSE)
3 #BLKUP2: [0, 1, 156]; IDX_C1_T1(T1)
4 #SSEK2: [0, 1, 156]; scan_type(ASC), IDX_C1_T1(T1), scan_range[10,10]
–CSEK
SQL> CREATE CLUSTER INDEX IDX_C1_T2 ON T2(C1);
SQL> EXPLAIN SELECT * FROM T2 WHERE C1=10;
1 #NSET2: [0, 250, 156]
2 #PRJT2: [0, 250, 156]; exp_num(5), is_atom(FALSE)
3 #CSEK2: [0, 250, 156]; scan_type(ASC), IDX_C1_T2(T2), scan_range[10,10]
–SSCN
SQL> CREATE INDEX IDX_C1_C2_T1 ON T1(C1,C2);
SQL> EXPLAIN SELECT C1,C2 FROM T1;
1 #NSET2: [1, 10000, 60]
2 #PRJT2: [1, 10000, 60]; exp_num(3), is_atom(FALSE)
3 #SSCN: [1, 10000, 60]; IDX_C1_C2_T1(T1)
至此,主要的執行計劃操作符就介紹的差不多了,更多的操作符解釋可以參考:DM7系統管理員手冊附錄4《執行計劃操作符》。
2、表連接詳解
2.1、嵌套循環連接
NEST LOOP原理:
兩層嵌套循環結構,有驅動表和被驅動表之分。 選定一張表作爲驅動表,遍歷驅動表中的每一行,根據連接條件去匹配第二 張表中的行。驅動表的行數就是循環的次數,這個很大程度影響了執行效率。
需注意的問題:
選擇小表作爲驅動表。統計信息儘量準確,保證優化器選對驅動表。
大量的隨機讀。如果沒有索引,隨機讀很致命,每次循環只能讀一塊, 不能讀多塊。使用索引可以解決這個問題。
使用場景:
- 驅動表有很好的過濾條件。
- 表連接條件能使用索引。
- 結果集比較小。
例子:
過濾列和連接列都沒有索引,也可以走nest loop,但是該計劃很差。如下面的計劃代價就很大。
SQL> explain select /*+use_nl(t1,t2)*/*
from t1 inner join t2
on t1.c1=t2.c1 where t1.c2='A';
1 #NSET2: [17862, 24950, 296]
2 #PRJT2: [17862, 24950, 296]; exp_num(8), is_atom(FALSE)
3 #SLCT2: [17862, 24950, 296]; T1.C1 = T2.C1
4 #NEST LOOP INNER JOIN2: [17862, 24950, 296];
5 #SLCT2: [1, 250, 148]; T1.C2 = 'A'
6 #CSCN2: [1, 10000, 148]; INDEX33555571(T1)
7 #CSCN2: [1, 10000, 148]; IDX_C1_T2(T2)
我們可以加上索引來進行優化:
create index idx_t1_c2 on t1(c2);
create index idx_t2_c1 on t2(c1); dbms_stats.gather_index_stats(user,'IDX_ T1_C2'); dbms_stats.gather_index_stats(user,'IDX_ T2_C1');
優化後執行計劃:
SQL> explain select /*+use_nl(t1,t2)*/*
from t1 inner join t2
on t1.c1=t2.c1 where t1.c2='A';
1 #NSET2: [17821, 24950, 296]
2 #PRJT2: [17821, 24950, 296]; exp_num(8), is_atom(FALSE)
3 #SLCT2: [17821, 24950, 296]; T1.C1 = T2.C1
4 #NEST LOOP INNER JOIN2: [17821, 24950, 296];
5 #BLKUP2: [0, 250, 148]; IDX_T1_C2(T1)
6 #SSEK2: [0, 250, 148]; scan_type(ASC), IDX_T1_C2(T1), scan_range['A','A']
7 #CSCN2: [1, 10000, 148]; IDX_C1_T2(T2)
2.2、哈希連接
hash join原理:
使用較小的Row source 作爲Hash table和Bitmap, 而第二個row source被hashed,根據bitmap與第一個row source生成的hash table 相匹配,bitmap查找的速度極快。
hash join特點:
- 一般沒索引或用不上索引時會使用該連接方式。
- 選擇小的表(或row source)做hash表。
- 只適用等值連接中的情形。
由於hash連接比較消耗內存,如果系統有很多這種連接時,需調整以下3個參數:
- HJ_BUF_GLOBAL_SIZE
- HJ_BUF_SIZE
- HJ_BLK_SIZE
例子:
SQL> explain select *
from t1 inner join t2
on t1.c1=t2.c1 where t1.c2='A';
1 #NSET2: [1, 24950, 296]
2 #PRJT2: [1, 24950, 296]; exp_num(8), is_atom(FALSE)
3 #HASH2 INNER JOIN: [1, 24950, 296]; KEY_NUM(1);
4 #NEST LOOP INDEX JOIN2: [1, 24950, 296]
5 #ACTRL: [1, 24950, 296];
6 #BLKUP2: [0, 250, 148]; IDX_T1_C2(T1)
7 #SSEK2: [0, 250, 148]; scan_type(ASC), IDX_T1_C2(T1), scan_range['A','A']
8 #CSEK2: [1, 2, 0]; scan_type(ASC), IDX_C1_T2(T2), scan_range[T1.C1,T1.C1]
9 #CSCN2: [1, 10000, 148]; IDX_C1_T2(T2)
需要注意:如果不是等值連接則會走nest loop連接。
SQL> explain select *
from t1 inner join t2
on t1.c1 > t2.c1 where t1.c2='A';
1 #NSET2: [2, 125000, 296]
2 #PRJT2: [2, 125000, 296]; exp_num(8), is_atom(FALSE)
3 #NEST LOOP INDEX JOIN2: [2, 125000, 296]
4 #BLKUP2: [0, 250, 148]; IDX_T1_C2(T1)
5 #SSEK2: [0, 250, 148]; scan_type(ASC), IDX_T1_C2(T1), scan_range['A','A']
6 #CSEK2: [2, 375, 0]; scan_type(ASC), IDX_C1_T2(T2), scan_range(null2,T1.C1)
2.3、排序合併連接
MERGE SORT的特點:
- 無驅動表之分,隨機讀很少。
- 兩個表都需要按照連接列排序,需要消耗大量的cpu和額外的內存。
應用場景:
通常情況下,merge sort join需要消耗大量的cpu和內存,效率都不會太高。如果存在相關索引可以消除sort,那麼CBO可能會考慮該連接方式。
例子:
SQL> explain select /*+use_merge(t1 t2)*/ t1.c1,t2.c1
from t1 inner join t2 on t1.c1=t2.c1 where t2.c2='b';
1 #NSET2: [4, 24950, 56]
2 #PRJT2: [4, 24950, 56]; exp_num(2), is_atom(FALSE)
3 #SLCT2: [4, 24950, 56]; T2.C2 = 'b'
4 #MERGE INNER JOIN3: [4, 24950, 56];
5 #SSCN: [1, 10000, 4]; IDX_C1_T1(T1)
6 #CSCN2: [1, 10000, 52]; IDX_C1_T2(T2)
3、查詢轉換
3.1、什麼是查詢轉換?
查詢轉換是優化器自動做的,在生成執行計劃之前,等價改寫 查詢語句的形式,以便提升效率和產生更好的執行計劃。它決 定是否重寫用戶的查詢,常見的轉換有謂詞傳遞、視圖拆分、 謂詞推進、關聯/非關聯子查詢改寫等。
瞭解優化器查詢轉換的特性,會幫助我們更好的看懂執行計劃, 也會對我們優化sql起到指導的作用。優化器的查詢轉換有很 多限制條件,我們可以根據類似的原理舉一反三,進行手工的 sql改寫,從到得到更好的執行計劃。
3.2、謂詞轉換
什麼是謂詞轉換呢?大致就是指我們可以根據A=B,B=C,可以推導出A=C的形式。如下面的SQL:
select * from t1 inner join t2
on t1.c2=t2.c2 where t1.c1=100
and t2.c1=t1.c1
CBO經過謂詞轉換後,實際執行的語句其實是:
select * from t1 inner join t2
on t1.c2=t2.c2 where t1.c1=100
and t2.c1=t1.c1
and t2.c1=100 –-謂詞傳遞
3.3、視圖拆分
我們先創建一個視圖:
SQL> create or replace view v_t1 as select t1.c1+t2.c1 as c11,
t2.c2,t1.c1 from t1,t2
2 3 where t1.c2=t2.c2;
操作已執行
我們看看視圖定義裏面SQL的執行計劃:
SQL> explain select t1.c1+t2.c1 as c11,
t2.c2,t1.c1 from t1,t2
where t1.c2=t2.c2;
1 #NSET2: [5, 980099, 104]
2 #PRJT2: [5, 980099, 104]; exp_num(3), is_atom(FALSE)
3 #HASH2 INNER JOIN: [5, 980099, 104]; KEY_NUM(1);
4 #SSCN: [1, 10000, 52]; IDX_C1_C2_T1(T1)
5 #CSCN2: [1, 10000, 52]; INDEX33555575(T2)
而我們查詢使用到視圖時:
SQL> explain select a.c11,b.c2 from v_t1 a,t1 b where a.c1=b.c1 and a.c1=100;
1 #NSET2: [5, 98, 156]
2 #PRJT2: [5, 98, 156]; exp_num(2), is_atom(FALSE)
3 #NEST LOOP INNER JOIN2: [5, 98, 156];
4 #SSEK2: [0, 1, 52]; scan_type(ASC), IDX_C1_C2_T1(T1 as B), scan_range[(100,min),(100,max))
5 #PRJT2: [2, 98, 104]; exp_num(1), is_atom(FALSE)
6 #HASH2 INNER JOIN: [2, 98, 104]; KEY_NUM(1);
7 #SSEK2: [0, 1, 52]; scan_type(ASC), IDX_C1_C2_T1(T1), scan_range[(100,min),(100,max))
8 #CSCN2: [1, 10000, 52]; INDEX33555575(T2)
觀察上面sql的執行計劃,發現視圖部分的子計劃已經沒有了。說明優化器進行等價改寫,將視圖的查詢拆散了,和其他部分作爲一個整體來生 成計劃。視圖拆分有很多限制,如果視圖查詢中含有distinc、union、 group by等操作,優化器就無法進行視圖拆分。
Sql中使用過多的視圖,會使sql變得複雜,優化器也難以生成最佳的執行計劃,不能過度依賴優化器進行視圖拆分。開發時應儘量減少視圖的使用。
3.4、謂詞推進
我們先看看下面這樣一個SQL,可以看到子查詢x相當於一個內聯視圖。
SQL> explain select * from
(select c1,c2 from t1 where c2='C') x where c1=100;
1 #NSET2: [0, 1, 60]
2 #PRJT2: [0, 1, 60]; exp_num(3), is_atom(FALSE)
3 #PRJT2: [0, 1, 60]; exp_num(3), is_atom(FALSE)
4 #SSEK2: [0, 1, 60]; scan_type(ASC), IDX_C1_C2_T1(T1), scan_range[(100,'C'),(100,'C')]
觀察上面的執行計劃,由於C2字段無索引,子查詢X部分本應該走全表掃描, 但是計劃中卻走了C1字段的索引。說明優化器對原始sql做了如下的等價改寫,將條件c1=100推到子查詢X中:
–查詢轉換
select * from
(select c1,c2 from t1 where c2='C' and c1=100) x;
3.5、查詢轉換例子
–非關聯子查詢的轉換:
SQL> explain select * from t1
where c1 in (select c1 from t2 ) and c2='A';
1 #NSET2: [1, 250, 156]
2 #PRJT2: [1, 250, 156]; exp_num(5), is_atom(FALSE)
3 #INDEX JOIN SEMI JOIN2: [1, 250, 156];
4 #SLCT2: [1, 250, 156]; T1.C2 = 'A'
5 #CSCN2: [1, 10000, 156]; INDEX33555571(T1)
6 #SSEK2: [1, 2, 0]; scan_type(ASC), IDX_T2_C1(T2), scan_range[T1.C1,T1.C1]
觀察原始sql,T2的子查詢是個非關聯的子查詢,完全可以把它生成一個獨立的子計劃。但是計劃中TI和T2做了關聯,說明優化器進行了如下的等價改寫:
select * from t1
where exists (select 1 from t2 where t1.c1=t2.c1) and c2='A';
相關INI參數: REFED_EXISTS_OPT_FLAG,影響in和exists子查詢的轉換。
–外連接轉換:
SQL> explain select t1.c1,t2.c2 from t1 left join t2
on t1.c1=t2.c1 where t2.c1=100 and t1.c2='A';
1 #NSET2: [0, 250, 104]
2 #PRJT2: [0, 250, 104]; exp_num(2), is_atom(FALSE)
3 #NEST LOOP INNER JOIN2: [0, 250, 104];
4 #SSEK2: [0, 1, 52]; scan_type(ASC), IDX_C1_C2_T1(T1), scan_range[(100,'A'),(100,'A')]
5 #BLKUP2: [0, 250, 52]; IDX_T2_C1(T2)
6 #SSEK2: [0, 250, 52]; scan_type(ASC), IDX_T2_C1(T2), scan_range[100,100]
觀察上面的計劃發現,原始sql是外連接,計劃中卻變成了內連接。這是優化器根 據sql語義判斷,就是等價於下面的內連接:
select t1.c1,t2.c2
from t1 inner join t2 on t1.c1=t2.c1 where t2.c1=100 and t1.c2='A';
4、總結
關於SQL優化主要還是需要先分析系統當前哪些語句是性能影響最大的,一般是那些單個SQL執行慢且執行頻率高的。
然後再結合執行計劃去進行優化,優化大致思路爲:
使用索引:選擇合適的索引。
改寫SQL:
- 將left join等價改爲inner join;
- 避免隱式轉換不走索引;
- 將過濾條件上拉,走索引;
- 用分析函數,減少表掃描。
- … …