mysql 學習 - 連接的原理


本篇已收錄在 MySQL 是怎樣運行的 學習筆記系列

連接簡介

連接的本質

mysql> CREATE TABLE t1 (m1 int, n1 char(1));
Query OK, 0 rows affected (0.02 sec)

mysql> CREATE TABLE t2 (m2 int, n2 char(1));
Query OK, 0 rows affected (0.02 sec)

mysql> INSERT INTO t1 VALUES(1, 'a'), (2, 'b'), (3, 'c');
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> INSERT INTO t2 VALUES(2, 'b'), (3, 'c'), (4, 'd');
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

連接的本質就是把各個連接表中的記錄都取出來依次匹配的組合加入結果集並返回給用戶。所以我們把t1和t2兩個表連接起來的過程如下圖所示:


連接查詢的結果集中包含一個表中的每一條記錄與另一個表中的每一條記錄相互匹配的組合,像這樣的結果集就可以稱之為笛卡爾積。因為表t1中有3條記錄,表t2中也有3條記錄,所以這兩個表連接之后的笛卡爾積就有3×3=9行記錄。在MySQL中,連接查詢的語法也很隨意,只要在FROM語句后邊跟多個表名就好了,比如我們把t1表和t2表連接起來的查詢語句可以寫成這樣:

多表的過濾條件

下邊這個查詢語句:

SELECT * FROM t1, t2 WHERE t1.m1 > 1 AND t1.m1 = t2.m2 AND t2.n2 < 'd';

那么這個連接查詢的大致執行過程如下:

首先確定第一個需要查詢的表,這個表稱之為驅動表。只需要選取代價最小的那種訪問方法去執行單表查詢語句就好了(就是說從const、ref、ref_or_null、range、index、all這些執行方法中選取代價最小的去執行查詢)


因為是根據t1表中的記錄去找t2表中的記錄,所以t2表也可以被稱之為被驅動表。上一步驟從驅動表中得到了2條記錄,所以需要查詢2次t2表。此時涉及兩個表的列的過濾條件t1.m1 = t2.m2就派上用場了:
  • 當t1.m1 = 2時,過濾條件t1.m1 = t2.m2就相當於t2.m2 = 2,所以此時t2表相當於有了t2.m2 = 2、t2.n2 < 'd'這兩個過濾條件,然后到t2表中執行單表查詢。
  • 當t1.m1 = 3時,過濾條件t1.m1 = t2.m2就相當於t2.m2 = 3,所以此時t2表相當於有了t2.m2 = 3、t2.n2 < 'd'這兩個過濾條件,然后到t2表中執行單表查詢。

所以整個連接查詢的執行過程就如下圖所示:

這個兩表連接查詢共需要查詢1次t1表,2次t2表。當然這是在特定的過濾條件下的結果,如果我們把t1.m1 > 1這個條件去掉,那么從t1表中查出的記錄就有3條,就需要查詢3次t2表了。也就是說在兩表連接查詢中,驅動表只需要訪問一次,被驅動表可能被訪問多次。

內連接和外連接

CREATE TABLE student (
    number INT NOT NULL AUTO_INCREMENT COMMENT '學號',
    name VARCHAR(5) COMMENT '姓名',
    major VARCHAR(30) COMMENT '專業',
    PRIMARY KEY (number)
) Engine=InnoDB CHARSET=utf8 COMMENT '學生信息表';

CREATE TABLE score (
    number INT COMMENT '學號',
    subject VARCHAR(30) COMMENT '科目',
    score TINYINT COMMENT '成績',
    PRIMARY KEY (number, subject)
) Engine=InnoDB CHARSET=utf8 COMMENT '學生成績表';

現在我們想把每個學生的考試成績都查詢出來就需要進行兩表連接了(因為score中沒有姓名信息,所以不能單純只查詢score表)。連接過程就是從student表中取出記錄,在score表中查找number相同的成績記錄,所以過濾條件就是student.number = socre.number,整個查詢語句就是這樣:

上表的查詢結果中並沒有全部的學生信息, 因為有一些學生缺考了. 我們想將缺考的學生信息同時也查出來, mysql 提供了內連接和外連接幫我們實現:

  • 對於內連接的兩個表,驅動表中的記錄在被驅動表中找不到匹配的記錄,該記錄不會加入到最后的結果集,我們上邊提到的連接都是所謂的內連接。
  • 對於外連接的兩個表,驅動表中的記錄即使在被驅動表中沒有匹配的記錄,也仍然需要加入到結果集。在 MySQL中,根據選取驅動表的不同,外連接仍然可以細分為2種:左外連接和右外連接
  • 可是這樣仍然存在問題,即使對於外連接來說,有時候我們也並不想把驅動表的全部記錄都加入到最后的結果集。這時就需要兩種過濾條件來幫助解決.

    where 和 on 子句

    WHERE子句中的過濾條件就是我們平時見的那種,不論是內連接還是外連接,凡是不符合WHERE子句中的過濾條件的記錄都不會被加入最后的結果集。

    對於外連接的驅動表的記錄來說,如果無法在被驅動表中找到匹配ON子句中的過濾條件的記錄,那么該記錄仍然會被加入到結果集中,對應的被驅動表記錄的各個字段使用NULL值填充。

    一般情況下,我們都把只涉及單表的過濾條件放到 WHERE 子句中,把涉及兩表的過濾條件都放到 ON 子句中,我們也一般把放到ON子句中的過濾條件也稱之為連接條件。

    左(外)連接的語法

    左(外)連接的語法還是挺簡單的,比如我們要把t1表和t2表進行左外連接查詢可以這么寫:

    SELECT * FROM t1 LEFT [OUTER] JOIN t2 ON 連接條件 [WHERE 普通過濾條件];

    對於左(外)連接和右(外)連接來說,必須使用ON子句來指出連接條件。

    右(外)連接的語法

    右(外)連接和左(外)連接的原理是一樣一樣的,語法也只是把LEFT換成RIGHT而已:

    SELECT * FROM t1 RIGHT [OUTER] JOIN t2 ON 連接條件 [WHERE 普通過濾條件];

    內連接的語法

    內連接和外連接的根本區別就是在驅動表中的記錄不符合ON子句中的連接條件時不會把該記錄加入到最后的結果集:

    SELECT * FROM t1 [INNER | CROSS] JOIN t2 [ON 連接條件] [WHERE 普通過濾條件];

    還有一種省略的寫法:

    SELECT * FROM t1, t2;

    連接的原理

    真正的重點來了, 在理解了mysql 使用了什么算法來實現的連接原理以后, 才能真正將連接寫得好.

    循環嵌套連接算法

    用偽代碼表示一下這個過程就是這樣:

    for each row in t1 {   #此處表示遍歷滿足對t1單表查詢結果集中的每一條記錄
        
        for each row in t2 {   #此處表示對於某條t1表的記錄來說,遍歷滿足對t2單表查詢結果集中的每一條記錄
        
            for each row in t3 {   #此處表示對於某條t1和t2表的記錄組合來說,對t3表進行單表查詢
                if row satisfies join conditions, send to client
            }
        }
    }
    

    這個過程就像是一個嵌套的循環,所以這種驅動表只訪問一次,但被驅動表卻可能被多次訪問,訪問次數取決於對驅動表執行單表查詢后的結果集中的記錄條數的連接執行方式稱之為嵌套循環連接(Nested-Loop Join),這是最簡單,也是最笨拙的一種連接查詢算法。

    使用索引加快連接速度

    當兩個表連接查詢時, 被驅動表肯定是拿着驅動表單表最優查詢的結果, 進行多次查詢. 如果每次查詢剛剛好都查詢在被驅動表所建立的索引上, 效率可能會提升不少. (主要是看這個索引對於數據庫的訪問方法是否高效)

    基於塊的循環嵌套連接

    掃描一個表的過程其實是先把這個表從磁盤上加載到內存中,然后從內存中比較匹配條件是否滿足。實際生產環境兩個表的數據量都非常大的情況, 內存里可能並不能完全存放的下表中所有的記錄,所以在掃描表前邊記錄的時候后邊的記錄可能還在磁盤上,等掃描到后邊記錄的時候可能內存不足,所以需要把前邊的記錄從內存中釋放掉。

    采用嵌套循環連接算法的兩表連接過程中,被驅動表可是要被訪問好多次的,如果這個被驅動表中的數據特別多而且不能使用索引進行訪問,那就相當於要從磁盤上讀好幾次這個表,這個I/O代價就非常大了,所以我們得想辦法:盡量減少訪問被驅動表的次數

    為了解決被驅動表數據量很大, 每次使用驅動表的結果逐條去與被驅動表中所有數據進行比對, 則可能驅動表的結果集有多少條數據, 就要從磁盤中取出被驅動表的數據多少次. 這樣做顯然是太慢了. mysql 提出當拿出一部分被驅動表的數據后, 就整體與驅動表的結果集都比對一次, 這樣會減少被驅動表的數據取出的次數. 所以這一部分數據先要放在一個叫做 join buffer的區域. 默認大小是 256kb,

    另外需要注意的是,驅動表的記錄並不是所有列都會被放到join buffer中,只有查詢列表中的列和過濾條件中的列才會被放到join buffer中,所以再次提醒我們,最好不要把*作為查詢列表,只需要把我們關心的列放到查詢列表就好了,這樣還可以在join buffer中放置更多的記錄


    免責聲明!

    本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



     
    粵ICP備18138465號   © 2018-2025 CODEPRJ.COM