MySQL表連線及其優化
導讀:
在做MySQL資料庫的優化工作時,如果只涉及到單表查詢,那麼95%的慢SQL都只需從索引上入手優化即可,通過新增索引來消除全表掃描或者排序操作,大概率能實現SQL語句執行速度質的飛躍。對於單表的優化操作,相信大部分DBA甚至開發人員都可以完成。
然而,在實際生產中,除了單表操作,更多的是多個表聯合起來查詢,這樣的查詢通常是慢SQL的重災區,查詢速度慢,使用伺服器資源較多,高CPU,高I/O。本文通過對錶連線的表現形式以及內部理論進行探究,以及思考如何優化表連線操作。
本文基於MySQL 5.7版本進行探究,由於MySQL 8中引入了新的連線方式hash join,本文可能不適用MySQL8版本
(一)MySQL的七種連線方式介紹
在MySQL中,常見的表連線方式有4類,共計7種方式:
- INNER JOIN:inner join是根據表連線條件,求取2個表的資料交集;
- LEFT JOIN :left join是根據表連線條件,求取2個表的資料交集再加上左表剩下的資料;此外,還可以使用where過濾條件求左表獨有的資料。
- RIGHT JOIN:right join是根據表連線條件,求取2個表的資料交集再加上右表剩下的資料;此外,還可以使用where過濾條件求右表獨有的資料。
- FULL JOIN:full join是左連線與右連線的並集,MySQL並未提供full join語法,如果要實現full join,需要left join與right join進行求並集,此外還可以使用where檢視2個表各自獨有的資料。
通過圖形來表現,各種連線形式的求取集合部分如下,藍色部分代表滿足join條件的資料:
接下來,我們通過例子來理解各種JOIN的含義。
首先建立測試資料:
-- 1.建立部門表 -- 部門表記錄部門資訊,公司共有4個部門:財務(FINANCE)、人力(HR)、銷售(SALES)、研發(RD)。
-- 不一定每個部門都有人,例如,公司雖然有研發部,但是沒有在編人員 create table dept (deptno int,dname varchar(14),loc varchar(20)); insert into dept values(10,'FINANCE','BEIJING'); insert into dept values(20,'HR','BEIJING'); insert into dept values(30,'SALES','SHANGHAI'); insert into dept values(40,'RD','CHENGDU'); -- 2.建立員工表-- 員工表記錄了員工工號、姓名、部門編號。
-- 不一定每個員工都有部門。例如,外包人員dd就沒有部門
create table emp (empno int,ename varchar(14),deptno int); insert into emp values(1,'aa',10); insert into emp values(2,'bb',20); insert into emp values(3,'cc',30); insert into emp values(4,'dd',null); insert into emp values(5,'ee',30); insert into emp values(6,'ff',20);
ER圖如下:
(1.1)INNER JOIN
業務場景:檢視公司正式員工的詳細資訊,包括工號、姓名、部門名稱。
需求分析:正式員工都有對應部門,使用INNER JOIN,通過部門編號關聯部門與員工求交集。
SQL語句:
mysql> select e.empno,e.ename,d.dname from emp e inner join dept d on e.deptno = d.deptno; +-------+-------+---------+ | empno | ename | dname | +-------+-------+---------+ | 1 | aa | FINANCE | | 2 | bb | HR | | 3 | cc | SALES | | 5 | ee | SALES | | 6 | ff | HR | +-------+-------+---------+
INNER JOIN就是求取2個表的共有資料(交集),我們可以這樣來理解表INNER JOIN過程:
- 從驅動表按順序資料,然後到被驅動表中逐行進行比較
- 如果條件滿足,則取出該行資料(注意取出的是2個表連線之後的資料),如果條件不滿足,則丟棄資料,然後繼續向下比較,直到遍歷完被驅動表的所有行
- 一致迴圈上面2步,知道步驟1的驅動表也遍歷結束。
對於上面SQL,其執行過程我們可以使用虛擬碼來描述:
// 特別注意:2個for迴圈,哪個表用來做外部迴圈,哪個表用來做內部迴圈,是由執行計劃決定的,可用explain來檢視,通常使用結果集較小的表來做驅動表,
// 本例子中,SQL中順序為emp,dept,但在執行計劃中卻是dept,emp。因此內外表順序需要看MySQL的執行計劃
for (i=1;i<=d.counts;i++) { for (j=1;j<=e.counts;j++>) { if (d[i].key = e[j].key) { return d[i].dname,e[j].empno,e[j].ename; } } }
(1.2)LEFT JOIN
業務場景:檢視每一個部門的詳細資訊,包括工號、姓名、部門名稱。
需求分析:既然包含每一個部門,那麼可以使用部門表進行LEFT JOIN,通過部門編號關聯部門與員工求交集。
SQL語句:
mysql> select d.dname,e.empno,e.ename from dept d left join emp e on e.deptno = d.deptno; +---------+-------+-------+ | dname | empno | ename | +---------+-------+-------+ | FINANCE | 1 | aa | | HR | 2 | bb | | SALES | 3 | cc | | SALES | 5 | ee | | HR | 6 | ff | | RD | NULL | NULL | +---------+-------+-------+
LEFT JOIN就是求取2個表的共有資料(交集)再加上左表剩下的資料,也就是左表的資料全部都要,左表的資料只要滿足關聯條件的。
我們可以這樣來理解表LEFT JOIN過程:
- 從左表按順序資料,然後到右表中逐行進行比較
- 如果條件滿足,則取出該行資料(注意取出的是2個表連線之後的資料),如果條件不滿足,則丟棄資料,然後繼續向下比較,直到遍歷完被驅動表的所有行,如果遍歷完右表所有的行都沒有與左表匹配的資料,則返回左表的行,右表的記錄用NULL填充。
- 一致迴圈上面2步,知道步驟1的驅動表也遍歷結束。
對於上面SQL,其執行過程我們可以使用虛擬碼來描述:
/*
關於外連線查詢演算法描述(https://dev.mysql.com/doc/refman/5.7/en/nested-join-optimization.html):
通常,對於外部聯接操作中第一個內部表的任何巢狀迴圈,都會引入一個標誌,該標誌在迴圈之前關閉並在迴圈之後檢查。當針對外部表中的當前行找到表示內部運算元的表中的匹配項時,將開啟該標誌。如果在迴圈週期結束時該標誌仍處於關閉狀態,則未找到外部表的當前行的匹配項。在這種情況下,該行由NULL內部表的列的值補充 。結果行將傳遞到輸出的最終檢查項或下一個巢狀迴圈,但前提是該行滿足所有嵌入式外部聯接的聯接條件。
*/
for (i=1;i<=d.counts;i++) { var is_success=false; // 確認d.[i]是否匹配到至少1行資料,預設未匹配到 for (j=1;j<=e.counts;j++>) { if (d[i].key = e[j].key) { return d[i].dname,e[j].empno,e[j].ename; is_success = true; } } if (is_success=false) // 如果左邊的表沒有匹配到資料,也會將左邊表返回,右邊表用null代替 { return d[i].key,null,null; } }
LEFT JOIN的補充:使用LEFT JOIN來獲取左表獨有的資料
業務場景:檢視哪些部門沒有員工
需求分析:要檢視沒有部門的員工,只需要先查出所有的部門與員工關係資料,然後過濾掉有員工的資料。
SQL語句:
mysql> select d.dname,e.empno,e.ename from dept d left join emp e on d.deptno = e.deptno where e.deptno is null; +-------+-------+-------+ | dname | empno | ename | +-------+-------+-------+ | RD | NULL | NULL | +-------+-------+-------+
使用LEFT JOIN獲取2個表的共有資料(交集)再加上左表剩下的資料,然後又把交集去除。
(1.3)RIGHT JOIN
業務場景:檢視每一個員工的詳細資訊,包括工號、姓名、部門名稱。
需求分析:既然包含每一個員工,那麼可以使用部門表進行LEFT JOIN,通過部門編號關聯部門與員工求交集。
SQL語句:
mysql> select d.dname,e.empno,e.ename from dept d right join emp e on e.deptno = d.deptno; +---------+-------+-------+ | dname | empno | ename | +---------+-------+-------+ | FINANCE | 1 | aa | | HR | 2 | bb | | HR | 6 | ff | | SALES | 3 | cc | | SALES | 5 | ee | | NULL | 4 | dd | +---------+-------+-------+
需要注意的是,右連線和左連線是可以相互轉換的,即右連線的語句,通過調換表位置並修改連線關鍵字為左連線,即可實現等價轉換。上面的SQL的等價左連線為:
mysql> select d.dname,e.empno,e.ename from emp e left join dept d on e.deptno = d.deptno; +---------+-------+-------+ | dname | empno | ename | +---------+-------+-------+ | FINANCE | 1 | aa | | HR | 2 | bb | | HR | 6 | ff | | SALES | 3 | cc | | SALES | 5 | ee | | NULL | 4 | dd | +---------+-------+-------+
實際上,MySQL在解析SQL階段,會自動將右外連線轉換等效的左外連線(文件:https://dev.mysql.com/doc/refman/5.7/en/outer-join-simplification.html),所以我們也無需深入的去了解右連線。
(1.4)FULL JOIN
業務場景:檢視所有部門及其所有員工的詳細資訊,包括工號、姓名、部門名稱。
需求分析:既然包含每一個部門及所有員工,那麼可以使用全連接獲取資料。然而,MySQL並沒有關鍵字去獲取全連線的資料,我們可以通過合併左連線
SQL語句:
mysql> select d.dname,e.empno,e.ename from dept d left join emp e on e.deptno = d.deptno union select d.dname,e.empno,e.ename from dept d right join emp e on e.deptno = d.deptno; +---------+-------+-------+ | dname | empno | ename | +---------+-------+-------+ | FINANCE | 1 | aa | | HR | 2 | bb | | SALES | 3 | cc | | SALES | 5 | ee | | HR | 6 | ff | | RD | NULL | NULL | | NULL | 4 | dd | +---------+-------+-------+
FULL JOIN的補充:
如果要查詢沒有員工的部門或者沒有部門的員工,即求取兩個表各自獨有的資料
SQL語句:
mysql> select d.dname,e.empno,e.ename from dept d left join emp e on e.deptno = d.deptno where e.deptno is null union select d.dname,e.empno,e.ename from dept d right join emp e on e.deptno = d.deptno where d.deptno is null; +-------+-------+-------+ | dname | empno | ename | +-------+-------+-------+ | RD | NULL | NULL | | NULL | 4 | dd | +-------+-------+-------+
(二)MySQL Join演算法
在MySQL 5.7中,MySQL僅支援Nested-Loop Join演算法及其改進型Block-Nested-Loop Join演算法,在8.0版本中,又新增了Hash Join演算法,這裡只討論5.7版本的表連線方式。
(2.1)Nested-Loop Join演算法
巢狀迴圈連線演算法(NLJ)從第一個迴圈的表中讀取1行資料,並將該行傳遞到下一個表進行連線運算,如果符合條件,則繼續與下一個表的行資料進行連線,知道連線完所有的表,然後重複上面的過程。簡單來講Nested-Loop Join就是程式設計中的多層for迴圈。假設存在3個表進行連線,連線方式如下:
table join type
------ -------------
t1 range
t2 ref
t3 ALL
如果使用NLJ演算法進行連線,虛擬碼如下:
for each row in t1 matching range { for each row in t2 matching reference key { for each row in t3 { if row satisfies join conditions, send to client } } }
(2.2)Block Nested-Loop Join演算法
塊巢狀迴圈(BLN)連線演算法使用外部表的行緩衝來減少對內部表的讀次數。例如,將外部表的10行資料讀入緩衝區並將緩衝區傳遞到下一個內部迴圈,則可以將內部迴圈中的每一行與緩衝區的10行資料進行比較,此時,內部表讀取的次數將減少為1/10。
如果使用BNL演算法,上述連線的虛擬碼可以寫為:
for each row in t1 matching range { for each row in t2 matching reference key { store used columns from t1, t2 in join buffer if buffer is full { for each row in t3 { for each t1, t2 combination in join buffer { if row satisfies join conditions, send to client } } empty join buffer } } } if buffer is not empty { for each row in t3 { for each t1, t2 combination in join buffer { if row satisfies join conditions, send to client } } }
MySQL Join Buffer有如下特點:
- join buffer可以被使用在表連線型別為ALL,index,range。換句話說,只有索引不可能被使用,或者索引全掃描,索引範圍掃描等代價較大的查詢才會使用Block Nested-Loop Join演算法;
- 僅僅用於連線的列資料才會被存在連線快取中,而不是整行資料
- join_buffer_size系統變數用來決定每一個join buffer的大小
- MySQL為每一個可以被快取的join語句分配一個join buffer,以便每一個查詢都可以使用join buffer。
- 在執行連線之前分配連線緩衝區,並在查詢完成後釋放連線緩衝區。
(三)表連線順序
在關係型資料庫中,對於多表連線,位於巢狀迴圈外部的表我們稱為驅動表,位於巢狀迴圈內部的表我們稱為被驅動表,驅動表與被驅動表的順序對於Join效能影響非常大,接下來我們探索一下MySQL中表連線的順序。因為RIGHT JOIN和FULL JOIN在MySQL中最終都會轉換為LEFT JOIN,所以我們只需討論INNER JOIN和LEFT JOIN即可。
這裡為了確保測試準確,我們使用MySQL提供的測試資料庫employees,下載地址為:https://github.com/datacharmer/test_db。其ER圖如下:
(3.1)INNER JOIN
對應INNER JOIN,MySQL永遠選擇結果集小的表作為驅動表。
例子1:檢視員工部門對應資訊
-- 將employees,dept_manager , departments 3個表進行內連線即可 select e.emp_no,e.first_name,e.last_name,d.dept_name from employees e inner join dept_manager dm on e.emp_no = dm.emp_no inner join departments d on dm.dept_no = d.dept_no;
我們來看一下3個表的大小,需要注意的是,這裡僅僅是MySQL粗略統計行數,在這個例子中,實際行數與之有一定的差距:
+--------------+------------+ | table_name | table_rows | +--------------+------------+ | departments | 9 | | dept_manager | 24 | | employees | 299468 | +--------------+------------+
最終的執行計劃為:
+----+-------------+-------+------------+--------+-----------------+-----------+---------+---------------------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+--------+-----------------+-----------+---------+---------------------+------+----------+-------------+ | 1 | SIMPLE | d | NULL | index | PRIMARY | dept_name | 42 | NULL | 9 | 100.00 | Using index | | 1 | SIMPLE | dm | NULL | ref | PRIMARY,dept_no | dept_no | 4 | employees.d.dept_no | 2 | 100.00 | Using index | | 1 | SIMPLE | e | NULL | eq_ref | PRIMARY | PRIMARY | 4 | employees.dm.emp_no | 1 | 100.00 | NULL | +----+-------------+-------+------------+--------+-----------------+-----------+---------+---------------------+------+----------+-------------+
可以看到,在INNER JOIN中,MySQL並不是按照語句中表的出現順序來按順序執行的,而是首先評估每個表結果集的大小,選擇小的作為驅動表,大的作為被驅動表,不管我們如何調整SQL中的表順序,MySQL優化器選擇表的順序與上面相同。
這裡需要特別說明的是:通常我們所說的"小表驅動大表"是非常不嚴謹的,在INNER JOIN中,MySQL永遠選擇結果集小的表作為驅動表,而不是小表。這有什麼區別呢?結果集是指表進行了資料過濾後形成的臨時表,其資料量小於或等於原表。下面提及的"小表和大表"都是指結果集大小。
例子2:檢視工號為110567的員工部門對應資訊
select e.emp_no,e.first_name,e.last_name,d.dept_name from employees e inner join dept_manager dm on e.emp_no = dm.emp_no and e.emp_no = 110567 inner join departments d on dm.dept_no = d.dept_no;
最終的執行計劃為:
+----+-------------+-------+------------+--------+-----------------+---------+---------+----------------------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+--------+-----------------+---------+---------+----------------------+------+----------+-------------+ | 1 | SIMPLE | e | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL | | 1 | SIMPLE | dm | NULL | ref | PRIMARY,dept_no | PRIMARY | 4 | const | 1 | 100.00 | Using index | | 1 | SIMPLE | d | NULL | eq_ref | PRIMARY | PRIMARY | 4 | employees.dm.dept_no | 1 | 100.00 | NULL | +----+-------------+-------+------------+--------+-----------------+---------+---------+----------------------+------+----------+-------------+
可以看到,這裡驅動表是employees,這個表是資料量最大的表,但是為什麼選擇它作為驅動表呢?因為他的結果集最小,在執行查詢時,MySQL會首先選擇employees表中emp_no=110567的資料,而這樣的資料只有1條,其結果集也就最小,所以優化器選擇了employees作為驅動表。
(3.2)LEFT JOIN
對於LEFT JOIN,執行順序永遠是從左往右,我們可以通過例子來看一下。
例子2:LEFT JOIN表順序的選擇測試
-- 表順序:e --> dm --> d mysql> explain select e.emp_no,e.first_name,e.last_name,d.dept_name from employees e left join dept_manager dm on e.emp_no = dm.emp_no left join departments d on dm.dept_no = d.dept_no; +----+-------------+-------+------------+--------+---------------+---------+---------+----------------------+--------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+--------+---------------+---------+---------+----------------------+--------+----------+-------------+ | 1 | SIMPLE | e | NULL | ALL | NULL | NULL | NULL | NULL | 299468 | 100.00 | NULL | | 1 | SIMPLE | dm | NULL | ref | PRIMARY | PRIMARY | 4 | employees.e.emp_no | 1 | 100.00 | Using index | | 1 | SIMPLE | d | NULL | eq_ref | PRIMARY | PRIMARY | 4 | employees.dm.dept_no | 1 | 100.00 | NULL | +----+-------------+-------+------------+--------+---------------+---------+---------+----------------------+--------+----------+-------------+ -- 表順序:dm --> e --> d mysql> explain select e.emp_no,e.first_name,e.last_name,d.dept_name from dept_manager dm left join employees e on e.emp_no = dm.emp_no left join departments d on dm.dept_no = d.dept_no; +----+-------------+-------+------------+--------+---------------+---------+---------+----------------------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+--------+---------------+---------+---------+----------------------+------+----------+-------------+ | 1 | SIMPLE | dm | NULL | index | NULL | dept_no | 4 | NULL | 24 | 100.00 | Using index | | 1 | SIMPLE | e | NULL | eq_ref | PRIMARY | PRIMARY | 4 | employees.dm.emp_no | 1 | 100.00 | NULL | | 1 | SIMPLE | d | NULL | eq_ref | PRIMARY | PRIMARY | 4 | employees.dm.dept_no | 1 | 100.00 | NULL | +----+-------------+-------+------------+--------+---------------+---------+---------+----------------------+------+----------+-------------+ -- 表順序:e --> dm --> d mysql> explain select e.emp_no,e.first_name,e.last_name,d.dept_name from employees e left join dept_manager dm on e.emp_no = dm.emp_no left join departments d on dm.dept_no = d.dept_no; +----+-------------+-------+------------+--------+---------------+---------+---------+----------------------+--------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+--------+---------------+---------+---------+----------------------+--------+----------+-------------+ | 1 | SIMPLE | e | NULL | ALL | NULL | NULL | NULL | NULL | 299468 | 100.00 | NULL | | 1 | SIMPLE | dm | NULL | ref | PRIMARY | PRIMARY | 4 | employees.e.emp_no | 1 | 100.00 | Using index | | 1 | SIMPLE | d | NULL | eq_ref | PRIMARY | PRIMARY | 4 | employees.dm.dept_no | 1 | 100.00 | NULL | +----+-------------+-------+------------+--------+---------------+---------+---------+----------------------+--------+----------+-------------+
如果右表存在謂詞過濾條件,MySQL會將left join轉換為inner join,詳見本文:(5.3)left join優化
(四)ON和WHERE的思考
在表連線中,我們可以在2個地方寫過濾條件,一個是在ON後面,另一個就是WHERE後面了。那麼,這兩個地方寫謂詞過濾條件有什麼區別呢?我們還是通過INNER JOIN和LEFT JOIN分別看一下。
(4.1)INNER JOIN
使用INNER JOIN,不管謂詞條件寫在ON部分還是WHERE部分,其結果都是相同的。
-- 將過濾條件寫在ON部分 mysql> select e.empno,e.ename,d.dname from emp e inner join dept d on e.deptno = d.deptno and d.dname = 'HR'; +-------+-------+-------+ | empno | ename | dname | +-------+-------+-------+ | 2 | bb | HR | | 6 | ff | HR | +-------+-------+-------+ -- 將過濾條件寫在WHERE部分 mysql> select e.empno,e.ename,d.dname from emp e inner join dept d on e.deptno = d.deptno where d.dname = 'HR'; +-------+-------+-------+ | empno | ename | dname | +-------+-------+-------+ | 2 | bb | HR | | 6 | ff | HR | +-------+-------+-------+ -- 使用非標準寫法,將表連線條件和過濾條件寫在WHERE部分 mysql> select e.empno,e.ename,d.dname from emp e inner join dept d where e.deptno = d.deptno and d.dname = 'HR'; +-------+-------+-------+ | empno | ename | dname | +-------+-------+-------+ | 2 | bb | HR | | 6 | ff | HR | +-------+-------+-------+
實際上,通過trace報告可以看到,在inner join中,不管謂詞條件寫在ON部分還是WHERE部分,MySQL都會將SQL語句的謂詞條件等價改寫到where後面。
(4.2)LEFT JOIN
我們繼續來看LEFT JOIN中ON與WHERE的區別。
使用ON作為謂詞過濾條件:
mysql> select e.empno,e.ename,d.dname from emp e left join dept d on e.deptno = d.deptno and d.dname = 'HR'; +-------+-------+-------+ | empno | ename | dname | +-------+-------+-------+ | 1 | aa | NULL | | 2 | bb | HR | | 3 | cc | NULL | | 4 | dd | NULL | | 5 | ee | NULL | | 6 | ff | HR | +-------+-------+-------+
我們可以把使用ON的情況用下圖來描述,先使用ON條件進行關聯,並在關聯的時候進行資料過濾:
再看看使用where的結果:
mysql> select e.empno,e.ename,d.dname from emp e left join dept d on e.deptno = d.deptno where d.dname = 'HR'; +-------+-------+-------+ | empno | ename | dname | +-------+-------+-------+ | 2 | bb | HR | | 6 | ff | HR | +-------+-------+-------+
我們可以把使用where的情況用下圖來描述,先使用ON條件進行關聯,然後對關聯的結果進行資料過濾:
可以看到,在LEFT JOIN中,過濾條件放在ON和WHERE之後結果是不同的:
- 如果過濾條件在ON後面,那麼將使用左表與右表每行資料進行連線,然後根據過濾條件判斷,如果滿足判斷條件,則左表與右表資料進行連線,如果不滿足判斷條件,則返回左表資料,右表資料用NULL值代替;
- 如果過濾條件在WHERE後面,那麼將使用左表與右表每行資料進行連線,然後將連線的結果集進行條件判斷,滿足條件的行資訊保留。
(五)JOIN優化
JOIN語句相對而言比較複雜,我們根據SQL語句的結構考慮優化方法,JOIN相關的主要SQL結構如下:
- inner join
- inner join + 排序(group by 或者 order by)
- left join
(5.1)inner join優化
常規inner join的SQL語法如下:
SELECT <select_list> FROM <left_table> inner join <right_table> ON <join_condition> WHERE <where_condition>
優化方法:
1.對於inner join,通常是採用小表驅動大表的方式,即小標作為驅動表,大表作為被驅動表(相當於小表位於for迴圈的外層,大表位於for迴圈的內層)。這個過程MySQL資料局優化器以幫助我們完成,通常無需手動處理(特殊情況,表的統計資訊不準確)。注意,這裡的“小表”指的是結果集小的表。
2.對於inner join,需要對被驅動表的連線條件建立索引
3.對於inner join,考慮對連線條件和過濾條件(ON、WHERE)建立複合索引
例子1:對於inner join,需要對被驅動表的連線條件建立索引
-- ---------- 構造測試表 -------------------------- -- 建立新表employees_new mysql> create table employees_new like employees; Query OK, 0 rows affected (0.01 sec) mysql> insert into empployees_new select * from employees; Query OK, 300024 rows affected (2.69 sec) Records: 300024 Duplicates: 0 Warnings: 0 -- 建立新表salaries_new mysql> create table salaries_new like salaries; Query OK, 0 rows affected (0.01 sec) mysql> insert into salaries_new select * from salaries; Query OK, 2844047 rows affected (13.00 sec) Records: 2844047 Duplicates: 0 Warnings: 0 -- 刪除主鍵 mysql> alter table employees_new drop primary key; Query OK, 300024 rows affected (1.84 sec) Records: 300024 Duplicates: 0 Warnings: 0 mysql> alter table salaries_new drop primary key; Query OK, 2844047 rows affected (9.58 sec) Records: 2844047 Duplicates: 0 Warnings: 0 -- 表大小 mysql> select table_name,table_rows from information_schema.tables a where a.table_schema = 'employees' and a.table_name in ('employees_new','salaries_new'); +---------------+------------+ | table_name | table_rows | +---------------+------------+ | employees_new | 299389 | | salaries_new | 2837194 | +---------------+------------+
此時測試表ER關係如下:
進行表連線查詢,語句如下:
select e.emp_no,e.first_name,e.last_name,s.salary,s.from_date,s.to_date from employees_new e inner join salaries_new s on e.emp_no = s.emp_no ;
結果為:
-- 1. 被驅動表沒有索引,執行時間:大於800s,(800s未執行完) -- 執行計劃: +----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+----------------------------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+----------------------------------------------------+ | 1 | SIMPLE | e | NULL | ALL | NULL | NULL | NULL | NULL | 299389 | 100.00 | NULL | | 1 | SIMPLE | s | NULL | ALL | NULL | NULL | NULL | NULL | 2837194 | 10.00 | Using where; Using join buffer (Block Nested Loop) | +----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+----------------------------------------------------+ -- 2. 在被驅動表連線條件上建立索引,執行時間: 37s -- 建立索引語句 create index idx_empno on salaries_new(emp_no); -- 執行計劃: +----+-------------+-------+------------+------+---------------+-----------+---------+--------------------+--------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+-----------+---------+--------------------+--------+----------+-------+ | 1 | SIMPLE | e | NULL | ALL | NULL | NULL | NULL | NULL | 299389 | 100.00 | NULL | | 1 | SIMPLE | s | NULL | ref | idx_empno | idx_empno | 4 | employees.e.emp_no | 9 | 100.00 | NULL | +----+-------------+-------+------------+------+---------------+-----------+---------+--------------------+--------+----------+-------+ -- 3. 更進一步,在驅動表連線條件上也建立索引,執行時間: 40s -- 建立索引語句 create index idx_employees_new_empno on employees_new(emp_no); -- 執行計劃: +----+-------------+-------+------------+------+-------------------------+-----------+---------+--------------------+--------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+-------------------------+-----------+---------+--------------------+--------+----------+-------+ | 1 | SIMPLE | e | NULL | ALL | idx_employees_new_empno | NULL | NULL | NULL | 299389 | 100.00 | NULL | | 1 | SIMPLE | s | NULL | ref | idx_empno | idx_empno | 4 | employees.e.emp_no | 9 | 100.00 | NULL | +----+-------------+-------+------------+------+-------------------------+-----------+---------+--------------------+--------+----------+-------+
通過以上測試可見,在被驅動表的連線條件上建立索引是非常有必要的,而在驅動表連線條件上建立索引則不會顯著提高速度。
例子2:對於inner join,考慮對連線條件和過濾條件(ON、WHERE)建立複合索引
進行表連線查詢,語句如下(以下2個SQL在MySQL優化器中解析為相同SQL):
select e.emp_no,e.first_name,e.last_name,s.salary,s.from_date,s.to_date from employees_new e inner join salaries_new s on e.emp_no = s.emp_no and e.first_name = 'Georgi' -- 或者 select e.emp_no,e.first_name,e.last_name,s.salary,s.from_date,s.to_date from employees_new e inner join salaries_new s on e.emp_no = s.emp_no where e.first_name = 'Georgi'
結果為:
-- 1. 未在連線條件和過濾條件上建立複合索引,執行時間: 0.162s -- 執行計劃: +----+-------------+-------+------------+------+-------------------------+-----------+---------+--------------------+--------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+-------------------------+-----------+---------+--------------------+--------+----------+-------------+ | 1 | SIMPLE | e | NULL | ALL | idx_employees_new_empno | NULL | NULL | NULL | 299389 | 10.00 | Using where | | 1 | SIMPLE | s | NULL | ref | idx_empno | idx_empno | 4 | employees.e.emp_no | 9 | 100.00 | NULL | +----+-------------+-------+------------+------+-------------------------+-----------+---------+--------------------+--------+----------+-------------+ -- 2.在連線條件和過濾條件上建立複合索引,執行時間: 0.058s -- 建立索引語句 create index idx_employees_first_name_emp_no on employees_new(first_name,emp_no); create index idx_employees_emp_no_first_name on employees_new(emp_no,first_name); -- 執行計劃: +----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------+---------------------------------+---------+--------------------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------+---------------------------------+---------+--------------------+------+----------+-------+ | 1 | SIMPLE | e | NULL | ref | idx_employees_new_empno,idx_employees_first_name_emp_no,idx_employees_emp_no_first_name | idx_employees_first_name_emp_no | 16 | const | 253 | 100.00 | NULL | | 1 | SIMPLE | s | NULL | ref | idx_empno | idx_empno | 4 | employees.e.emp_no | 9 | 100.00 | NULL | +----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------+---------------------------------+---------+--------------------+------+----------+-------+
通過以上測試可見,表的連線條件上和過濾條件上建立複合索引可以提高查詢速度,從本例子看,速度沒有較大提高,因為對employees_new表全表掃描速度很快,但是在非常大的表中,複合索引能夠有效提高速度。
(5.2)inner join + 排序(group by 或者 order by)優化
常規inner join+排序的SQL語法如下:
SELECT <select_list> FROM <left_table> inner join <right_table> ON <join_condition> WHERE <where_condition>
GROUP BY <group_by_list>
ORDER BY <order_by_list>
優化方法:
1.與inner join一樣,在被驅動表的連線條件上建立索引
2.inner join + 排序往往會在執行計劃裡面伴隨著Using temporary Using filesort關鍵字出現,如果臨時表或者排序的資料量很大,那麼將會導致查詢非常慢,需要特別重視;反之,臨時表或者排序的資料量較小,例如只有幾百條,那麼即使執行計劃有Using temporary Using filesort關鍵字,對查詢速度影響也不大。如果說排序操作消耗了大部分的時間,那麼可以考慮使用索引的有序性來消除排序,接下來對該優化方法進行討論。
group by和order by都會對相關列進行排序,根據SQL是否存在GROUP BY或者ORDER BY關鍵字,分3種情況討論:
SQL語句存在 group by |
SQL語句存在 order by |
優化操作考慮的排序列 | 解釋 | |
情況1 | 是 | 否 | 只需考慮group by相關列排序問題即可 | 如果SQL語句中只含有group by,則只需考慮group by後面的列排序問題即可 |
情況2 | 否 | 是 | 只需考慮order by相關列排序問題即可 | 如果SQL語句中只含有order by,則只需考慮order by後面的列排序問題即可 |
情況3 | 是 | 是 | 只需考慮group by相關列排序問題即可 |
如果SQL語句中同時含有group by和order by,只需考慮group by後面的排序即可。 因為MySQL先執行group by,後執行order by,通常group by之後資料量已經較少了, 後續的order by直接在磁碟上排序即可 |
對於上面3種情況:
1.如果優化考慮的排序列全部來源於驅動表,則可以考慮:在等值謂詞過濾條件上+排序列上建立複合索引,這樣可以使用索引先過濾資料,再使用索引按順序獲取資料。
2.如果優化考慮的排序列全部來源於某個被驅動表,則可以考慮:使用表連線hint(Straight_JOIN)控制連線順序,將排序相關表設定為驅動表,然後按照1建立複合索引;
3.如果優化考慮的排序列來源於多個表,貌似沒有好的解決辦法,有想法的同學也可以留言,一起進步。
例子1:如果優化考慮的排序列全部來源於驅動表,則可以考慮:在等值謂詞過濾條件上+排序列上建立複合索引,這樣可以使用索引先過濾資料,再使用索引按順序獲取資料。
-- 1.驅動表e上存在排序 mysql> explain select e.first_name,sum(salary) from employees_new e inner join salaries_new s on e.emp_no = s.emp_no where e.last_name = 'Aamodt' group by e.first_name; +----+-------------+-------+------------+------+------------------------------------------------------+------------------------------+---------+--------------------+------+----------+-----------------------------------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+------------------------------------------------------+------------------------------+---------+--------------------+------+----------+-----------------------------------------------------------+ | 1 | SIMPLE | e | NULL | ref | idx_employees_new_empno,idx_lastname_empno_firstname | idx_lastname_empno_firstname | 18 | const | 205 | 100.00 | Using where; Using index; Using temporary; Using filesort | | 1 | SIMPLE | s | NULL | ref | idx_empno | idx_empno | 4 | employees.e.emp_no | 9 | 100.00 | NULL | +----+-------------+-------+------------+------+------------------------------------------------------+------------------------------+---------+--------------------+------+----------+-----------------------------------------------------------+ -- 2.在驅動表e上的等值謂詞過濾條件last_name和排序列first_name上建立索引 mysql> create index idx_lastname_firstname on employees_new (last_name,first_name); -- 3.可以看到,排序消除 mysql> explain select e.first_name,sum(salary) from employees_new e inner join salaries_new s on e.emp_no = s.emp_no where e.last_name = 'Aamodt' group by e.first_name; +----+-------------+-------+------------+------+----------------------------------------------------------------------------------+------------------------+---------+--------------------+------+----------+-----------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+----------------------------------------------------------------------------------+------------------------+---------+--------------------+------+----------+-----------------------+ | 1 | SIMPLE | e | NULL | ref | idx_employees_new_empno,idx_employees_new_empno_firstname,idx_lastname_firstname | idx_lastname_firstname | 18 | const | 205 | 100.00 | Using index condition | | 1 | SIMPLE | s | NULL | ref | idx_empno | idx_empno | 4 | employees.e.emp_no | 9 | 100.00 | NULL | +----+-------------+-------+------------+------+----------------------------------------------------------------------------------+------------------------+---------+--------------------+------+----------+-----------------------+
需要說明的是,消除排序只是提供了一種資料優化的方式,消除排序後,其速度並不一定會比之前快,需要具體問題具體分析測試。
例子2:如果優化考慮的排序列全部來源於某個被驅動表,則可以考慮:使用表連線hint(Straight_JOIN)控制連線順序,將排序相關表設定為驅動表,然後按照1建立複合索引;
-- 1. 被驅動表s上存在排序 mysql> explain select s.from_date,sum(salary) from employees_new e inner join salaries_new s on e.emp_no = s.emp_no where e.last_name = 'Aamodt' and s.salary = 40000 group by s.from_date; +----+-------------+-------+------------+------+------------------...-------+------------------------+---------+--------------------+------+----------+---------------------------------+ | id | select_type | table | partitions | type | possible_keys ... | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+------------------...-------+------------------------+---------+--------------------+------+----------+---------------------------------+ | 1 | SIMPLE | e | NULL | ref | idx_employees_new...stname | idx_lastname_firstname | 18 | const | 205 | 100.00 | Using temporary; Using filesort | | 1 | SIMPLE | s | NULL | ref | idx_empno ... | idx_empno | 4 | employees.e.emp_no | 9 | 10.00 | Using where | +----+-------------+-------+------------+------+------------------...-------+------------------------+---------+--------------------+------+----------+---------------------------------+ -- 2. 使用Straight_join改變表的連線順序 mysql> explain select s.from_date,sum(salary) from salaries_new s STRAIGHT_JOIN employees_new e on e.emp_no = s.emp_no where e.last_name = 'Aamodt' and s.salary = 40000 group by s.from_date; +----+-------------+-------+------------+------+-----------------...----------+-------------------------+---------+--------------------+---------+----------+----------------------------------------------+ | id | select_type | table | partitions | type | possible_keys ... | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+-----------------...----------+-------------------------+---------+--------------------+---------+----------+----------------------------------------------+ | 1 | SIMPLE | s | NULL | ALL | idx_empno ... | NULL | NULL | NULL | 2837194 | 10.00 | Using where; Using temporary; Using filesort | | 1 | SIMPLE | e | NULL | ref | idx_employees_ne...firstname | idx_employees_new_empno | 4 | employees.s.emp_no | 1 | 5.00 | Using where | +----+-------------+-------+------------+------+-----------------...----------+-------------------------+---------+--------------------+---------+----------+----------------------------------------------+ -- 3. 在新的驅動表上建立等值謂詞+排序列索引 mysql> create index idx_salary_fromdate on salaries_new(salary,from_date); Query OK, 0 rows affected (5.39 sec) Records: 0 Duplicates: 0 Warnings: 0 -- 4. 可以看到,消除排序 mysql> explain select s.from_date,sum(salary) from salaries_new s STRAIGHT_JOIN employees_new e on e.emp_no = s.emp_no where e.last_name = 'Aamodt' and s.salary = 40000 group by s.from_date; +----+-------------+-------+------------+------+---------------------------------...--+-------------------------+---------+--------------------+--------+----------+-----------------------+ | id | select_type | table | partitions | type | possible_keys ... | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------------------------...--+-------------------------+---------+--------------------+--------+----------+-----------------------+ | 1 | SIMPLE | s | NULL | ref | idx_empno,idx_salary_fromdate ... | idx_salary_fromdate | 4 | const | 199618 | 100.00 | Using index condition | | 1 | SIMPLE | e | NULL | ref | idx_employees_new_empno,idx_empl...e | idx_employees_new_empno | 4 | employees.s.emp_no | 1 | 5.00 | Using where | +----+-------------+-------+------------+------+---------------------------------...--+-------------------------+---------+--------------------+--------+----------+-----------------------+
需要說明的是,大部分情況下,MySQL優化器會自動選擇最優的表連線方式,Straight_join的引入往往會造成大表做驅動表的情況出現,雖然消除了排序,但是又引入了新的麻煩。到底是排序帶來的開銷大,還是NLJ迴圈巢狀不合理帶來的開銷大,需要具體情況具體分析。
(5.3)left join優化
在MySQL中外連線(left join、right join 、full join)會被優化器轉換為left join,因此,外連線只需討論left join即可。常規left join的SQL語法如下:
SELECT <select_list> FROM <left_table> left join <right_table> ON <join_condition> WHERE <where_condition> GROUP BY <group_by_list> ORDER BY <order_by_list>
優化方法:
1.與inner join一樣,在被驅動表的連線條件上建立索引
2.left join的表連線順序都是從左像右的,我們無法改變表連線順序。但是如果右表在where條件中存在謂詞過濾,則MySQL會將left join自動轉換為inner join,其原理圖如下:
例子1:.如果右表在where條件中存在謂詞過濾,則MySQL會將left join自動轉換為inner join
建立測試表:
create table dept ( deptno int, dname varchar(20) ); insert into dept values (10, 'sales'),(20, 'hr'),(30, 'product'),(40, 'develop'); create table emp ( empno int, ename varchar(20), deptno varchar(20) ); insert into emp values (1,'aa',10),(2,'bb',10),(3,'cc',20),(4,'dd',30),(5,'ee',30);
執行left join,檢視其執行計劃,發現並不是左表作為驅動表
mysql> explain select d.dname,e.ename from dept d left join emp e on d.deptno = e.deptno where e.deptno = 30; +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+ | 1 | SIMPLE | e | NULL | ALL | NULL | NULL | NULL | NULL | 5 | 20.00 | Using where | | 1 | SIMPLE | d | NULL | ALL | NULL | NULL | NULL | NULL | 4 | 25.00 | Using where; Using join buffer (Block Nested Loop) | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
通過trace追蹤,發現MySQL對其該語句進行了等價改寫,將外連線改為了內連線。
mysql> set optimizer_trace="enabled=on",end_markers_in_JSON=on; Query OK, 0 rows affected (0.00 sec) mysql> select d.dname,e.ename from dept d left join emp e on d.deptno = e.deptno where e.deptno = 30; +---------+-------+ | dname | ename | +---------+-------+ | product | dd | | product | ee | +---------+-------+ 2 rows in set (0.03 sec) mysql> select * from information_schema.optimizer_trace; | MISSING_BYTES_BEYOND_MAX_MEM_SIZE | INSUFFICIENT_PRIVILEGES | select d.dname,e.ename from dept d left join emp e on d.deptno = e.deptno where e.deptno = 30 | { "steps": [ { "join_preparation": { "select#": 1, "steps": [ { "expanded_query": "/* select#1 */ select `d`.`dname` AS `dname`,`e`.`ename` AS `ename` from (`dept` `d` left join `emp` `e` on((`d`.`deptno` = `e`.`deptno`))) where (`e`.`deptno` = 30)" }, { "transformations_to_nested_joins": { "transformations": [ "outer_join_to_inner_join", "JOIN_condition_to_WHERE", "parenthesis_removal" ] /* transformations */, "expanded_query": "/* select#1 */ select `d`.`dname` AS `dname`,`e`.`ename` AS `ename` from `dept` `d` join `emp` `e` where ((`e`.`deptno` = 30) and (`d`.`deptno` = `e`.`deptno`))" } /* transformations_to_nested_joins */ } ] /* steps */ } /* join_preparation */ }, { "join_optimization": { "select#": 1, "steps": [ { "condition_processing": { "condition": "WHERE", "original_condition": "((`e`.`deptno` = 30) and (`d`.`deptno` = `e`.`deptno`))", "steps": [ { "transformation": "equality_propagation", "resulting_condition": "((`e`.`deptno` = 30) and (`d`.`deptno` = `e`.`deptno`))" }, { "transformation": "constant_propagation", "resulting_condition": "((`e`.`deptno` = 30) and (`d`.`deptno` = `e`.`deptno`))" }, { "transformation": "trivial_condition_removal", "resulting_condition": "((`e`.`deptno` = 30) and (`d`.`deptno` = `e`.`deptno`))" } ] /* steps */ } /* condition_processing */ }, { "substitute_generated_columns": { } /* substitute_generated_columns */ }, { "table_dependencies": [ { "table": "`dept` `d`", "row_may_be_null": false, "map_bit": 0, "depends_on_map_bits": [ ] /* depends_on_map_bits */ }, { "table": "`emp` `e`", "row_may_be_null": true, "map_bit": 1, "depends_on_map_bits": [ ] /* depends_on_map_bits */ } ] /* table_dependencies */ }, { "ref_optimizer_key_uses": [ ] /* ref_optimizer_key_uses */ }, { "rows_estimation": [ { "table": "`dept` `d`", "table_scan": { "rows": 4, "cost": 1 } /* table_scan */ }, { "table": "`emp` `e`", "table_scan": { "rows": 5, "cost": 1 } /* table_scan */ } ] /* rows_estimation */ }, { "considered_execution_plans": [ { "plan_prefix": [ ] /* plan_prefix */, "table": "`dept` `d`", "best_access_path": { "considered_access_paths": [ { "rows_to_scan": 4, "access_type": "scan", "resulting_rows": 4, "cost": 1.8, "chosen": true } ] /* considered_access_paths */ } /* best_access_path */, "condition_filtering_pct": 100, "rows_for_plan": 4, "cost_for_plan": 1.8, "rest_of_plan": [ { "plan_prefix": [ "`dept` `d`" ] /* plan_prefix */, "table": "`emp` `e`", "best_access_path": { "considered_access_paths": [ { "rows_to_scan": 5, "access_type": "scan", "using_join_cache": true, "buffers_needed": 1, "resulting_rows": 1, "cost": 2.6007, "chosen": true } ] /* considered_access_paths */ } /* best_access_path */, "condition_filtering_pct": 100, "rows_for_plan": 4, "cost_for_plan": 4.4007, "chosen": true } ] /* rest_of_plan */ }, { "plan_prefix": [ ] /* plan_prefix */, "table": "`emp` `e`", "best_access_path": { "considered_ac