1. 程式人生 > >MySQL表連線及其優化

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過程:

  1. 從驅動表按順序資料,然後到被驅動表中逐行進行比較
  2. 如果條件滿足,則取出該行資料(注意取出的是2個表連線之後的資料),如果條件不滿足,則丟棄資料,然後繼續向下比較,直到遍歷完被驅動表的所有行
  3. 一致迴圈上面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過程:

  1. 從左表按順序資料,然後到右表中逐行進行比較
  2. 如果條件滿足,則取出該行資料(注意取出的是2個表連線之後的資料),如果條件不滿足,則丟棄資料,然後繼續向下比較,直到遍歷完被驅動表的所有行,如果遍歷完右表所有的行都沒有與左表匹配的資料,則返回左表的行,右表的記錄用NULL填充。
  3. 一致迴圈上面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