達夢SQL優化及執行計劃解讀

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;
  • 避免隱式轉換不走索引;
  • 將過濾條件上拉,走索引;
  • 用分析函數,減少表掃描。
  • … …
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章