一句sum千行淚,笛卡爾積多坑人,mysql執行的前後順序

咱們每個人都想要優化SQL語句,以便可以提高性能,可是,若是不瞭解其機制,可能就會事倍功半。我以一個簡單的例子 ,來說解SQL的部分機制。java

今天在公司工做時,面臨這樣一個需求:mysql

根據條件查詢項目的預算金額。sql

查詢要求:數據庫

  1. 項目的id
  2. 項目人員的類型

數據庫表設計

數據庫有這樣的兩張表,一張是項目表project。項目表有些字段不便展現,於是,只作部分截圖:編程

項目表

一張是項目人員表,這張表記錄的是某個項目涉及哪些類型的人員,人員類型(枚舉)以下表所示:編程語言

key值 value值
PERSON_TYPE_SALESMAN 業務員
PERSON_TYPE_SALESMAN_MANAGER 業務部經理
PERSON_TYPE_DESIGNER 設計師
PERSON_TYPE_DESIGNER_MANAGER 設計部經理
PERSON_TYPE_PROJECT_SUPERVISION 工程監理
PERSON_TYPE_ENGINEERING_MANAGER 工程部經理

於是,數據表項目人員(project_person)的的設計爲:函數

項目人員表


查詢條件

  • 條件1:咱們首先查詢項目編號爲167的項目
SELECT SUM(budgetary_amount) FROM zq_project WHERE is_deleted = 0 AND id=167

輸出結果爲 10性能

  • 條件2:關聯項目人員表,查找編號爲167的項目
SELECT
    SUM(zp.budgetary_amount)
FROM
    zq_project zp
LEFT JOIN zq_project_person zpp ON(zpp.is_deleted =  0 AND zpp.project_id = zp.id)
WHERE zp.is_deleted = 0 AND zp.id=167

輸出結果爲 60測試

爲何會這樣呢

爲何會出現上訴狀況,當咱們在作一對多的sum求和時,就出現了笛卡爾積的現象。咱們查找出項目人員表中的項目編號爲167的有多少條記錄優化

SELECT * from zq_project_person zpp WHERE zpp.is_deleted = 0 and zpp.project_id = 167

輸出結果如圖所示:

項目編號爲167的項目人員的記錄

由上圖可知,一共有六條記錄,也就是說,項目表中編號爲167的這條記錄對應着項目人員表中的6條記錄,sum以後要計算6次,才變成60,好比下面的代碼:

SELECT
    zp.id AS projectId,
    zp.budgetary_amount,
    zpp.id AS personId
FROM
    zq_project zp
LEFT JOIN zq_project_person zpp ON(zpp.is_deleted =  0 AND zpp.project_id = zp.id)
WHERE zp.is_deleted = 0 AND zp.id=167;

輸出結果如圖所示:

左鏈接的輸出結果

這就涉及到mysql的執行前後的順序形成笛卡爾積的紊亂

在講解mysql執行的前後順序以前,咱們瞭解一下left join的 on 和 where的區別。

left join 的on和where的區別

on中的是副表的條件,where會將left join轉化爲inner join格式的數據,這是過濾數據用的。

假設有這兩張表,一張是商品表(goods表),一張是商品分類表(goods_category),商品表的外鍵是商品分類表的主鍵。咱們來作left join的測試

商品表和商品分類表的數據

查找語句爲:

SELECT
    *
FROM
    cce_goods cg
LEFT JOIN cce_goods_category cgc ON(cgc.is_deleted =  0 AND cgc.id = cg.goods_category_id)
WHERE
    cg.is_deleted = 0

查找結果如圖所示:

查找結果

你會發現,編號爲1的商品分類的字段屬性is_deleted的值明明是 1 ,而on以後的is_deleted 的值爲 0 ,這應該是篩選不出來了,但仍是能篩選出來呢?這裏就涉及到on的條件了。

  • 首先,left join是並集,那麼又是誰的並集?是主表和副表的並集。這時,主表和副表就有兩種狀況了,一種是主表的外鍵引用副表的主鍵,另外一種就是主表的主鍵是副表的外鍵,那麼,這就得分狀況了。
  • 針對第一種狀況

    • 咱們以商品和商品表爲例子,顯然,商品表是主鍵,引用副表商品分類表的外鍵。
    • 主表和副表進行笛卡爾積(主表的外鍵和副表的主鍵進行匹配)獲得一張臨時表,臨時表中存儲主表和副表的字段屬性。這時,以主表爲主,副表爲輔,即使副表沒有數據,其也還會展現副表的字段。
    • 因此,編號爲1的商品分類副表條件不知足,也就是沒有知足的數據,於是,就把商品分類的字段屬性爲空。
    • 換個角度來看,若是咱們把WHERE cg.is_deleted = 0這個條件去掉,你會發現會有不少數據出來。篩選條件where在left join以後,它的優先級低於left join。
    • 假如,咱們把cgc.is_deleted = 0 改爲爲 cgc.is_deleted = 1,你會發現神奇的一幕,如圖所示:

clipboard.png

你會發現,這是商品分類的字段屬性是有值的,由於,副表的條件知足了,能拿到副表中的字段屬性值。
若是咱們把left join 改爲inner join ,而cgc.is_deleted = 0 不變,這又不同了,如代碼所示:

SELECT
    *
FROM
    cce_goods cg
INNER JOIN cce_goods_category cgc ON(cgc.is_deleted =  0 AND cgc.id = cg.goods_category_id)
WHERE
    cg.is_deleted = 0

這樣,上面的兩條數據也沒了,由於,inner join 是主表和副表的交集,主表和副表的條件是平行條件,具備一樣的權重,也就是說同時知足主副表的條件,才能出現數據。

再假如,咱們cgc.is_deleted = 0放到外面,如代碼所示:

SELECT
    *
FROM
    cce_goods cg
INNER JOIN cce_goods_category cgc ON(cgc.id = cg.goods_category_id)
WHERE
    cg.is_deleted = 0 AND cgc.is_deleted =  0

這樣,也就把left join 隱性成了 inner join了,主表和副表的條件也是平行條件,具備一樣的權重。

  • 針對第二種狀況
    一、 以項目和項目人員來看,項目是主表,項目人員是副表,目前有三條沒被刪除的記錄,如圖所示:

    沒被刪除的三條項目人員記錄

    二、 咱們來執行如下的查詢語句,如代碼所示:

SELECT
    zp.id AS projectId,
    zp.budgetary_amount,
    zpp.id AS personId
FROM
    zq_project zp
LEFT JOIN zq_project_person zpp ON(zpp.is_deleted =  0 AND zpp.project_id = zp.id)
WHERE zp.is_deleted = 0 AND zp.id=167;

目前只有三條記錄,其餘的五條記錄沒有展現,這是爲何呢?這個只能意會,沒法言傳。就好比java中的對象,類Project對象是類ProjectPerson的成員屬性,咱們能在ProjectPerson對象裏填充Project對象,但沒法在Project對象中填充ProjectPerson的對象是同樣的道理。

上面也提到了mysql執行的前後順序了,在下面,詳細介紹mysql執行的前後順序。


mysql執行的前後順序

mysql在執行的過程會有必定的前後順序的,它是按照什麼順序來的呢?

任何一種開發語言,不論是面向結構的c語言,仍是面向對象的JAVA語言,或者,結構化查詢語言sql,其都有一個入口,C語言是main,java是public static void main(String[] args){...},SQL語言好比mysql,其入口是From,而後根據各個優先級。依次往下進行。

  1. from
  2. join
  3. on
  4. where
  5. group by(開始使用select中的別名,後面的語句中均可以使用)
  6. avg,sum.... 複合函數
  7. having
  8. select
  9. distinct
  10. order by

以項目表爲主表,以項目人員表和項目進程表爲副表,查找出項目名和項目的預算金額

SELECT DISTINCT
    zp.id AS projectId,
    SUM(zp.budgetary_amount) AS totalBugAmo,
    zp.`name` AS projectName
FROM
    zq_project zp
LEFT JOIN zq_project_person zper ON (
    zper.is_deleted = 0
    AND zper.project_id = zp.id
)
LEFT JOIN zq_project_process zpro ON (
    zpro.is_deleted = 0
    AND zpro.project_id = zp.id
)
WHERE
    zp.is_deleted = 0
GROUP BY
    zp.id
HAVING
    totalBugAmo <= 12000
ORDER BY
    totalBugAmo DESC

執行結果如圖所示:

項目名和項目的預算金額

執行順序如圖所示
MySQL語句的知心順序

  1. 第一步驟, 以from爲入口進入查詢語句中,肯定主表是zq_project,而後從主表中取數據源
  2. LEFT JOIN zq_project_person zper ON (。。。)此時生成一張虛擬表vt1,根據虛擬表vt1中的on以後的篩選條件匹配數據,生成虛擬表vt2
  3. LEFT JOIN zq_project_process zpro ON(。。。)在vt2的基礎上生成vt3和vt4,
  4. where篩選器,過濾掉已被邏輯刪除的項目,生成虛擬表vt5,
  5. 在group by這裏出現了分水嶺,以後就可使用select中的別名了。這個爲何要分組呢?好比,項目人員表中相同項目編號的人員不止一個,這個要以項目id來對其進行分組統計,但此時的分組統計,是有問題的,由於,項目的預算金額是在項目表中的,而相同的項目編號的人員不止一個,那麼,就出現了人員項目重複統計的現象。下面再細分析。生成虛擬表vt6
  6. 因此,分組以後再sum等這些複合函數,因而,就出現了同一個項目的項目預算相加。這就出現了數據的累加錯誤。生成虛擬表vt7
  7. having是對虛擬表vt7進行數據過濾的,也就是說,它服務的對象是複合函數。生成虛擬表vt8
  8. select是將vt8的根據咱們寫出的條件篩選出來數據,好比咱們只想要項目的id、項目的預算金額、項目的名字等,生成虛擬表vt9
  9. 使用distinct 對虛擬表vt9進行去重,生成虛擬表vt10
  10. 最後再排序,生成咱們最後想要的表。

聚合函數的笛卡爾積錯誤

在講解這個問題前,咱們先看這張圖:

項目表、項目人員表、項目進程表

咱們的查語句是:

SELECT
    zp.id AS projectId,
    zp.budgetary_amount AS bugAmo,
    zp.`name` AS projectName
FROM
    zq_project zp
LEFT JOIN zq_project_person zper ON (
    zper.is_deleted = 0
    AND zper.project_id = zp.id
)
LEFT JOIN zq_project_process zpro ON (
    zpro.is_deleted = 0
    AND zpro.project_id = zp.id
)
WHERE
    zp.is_deleted = 0 AND zp.id=167

查詢結果的截圖爲:

clipboard.png

你會發現,數據多了,爲何會多?以項目編號爲167的爲研究點,此時,當left join項目人員表時,根據排列組合而來,$C(1,1)*C(2,1)$=2,多生成一張有兩條記錄的虛擬表。此時,再left join項目進程表時,根據排列組合而來,$2* C(3,1)$=6,就會出現,這時就會出現6條數據的虛擬表,這時,咱們再sum的話,就會計算6次,從而得出項目編號爲167的預算金額是60,而不是10。

上面就出現了分組以後的項目編號爲167的預算金額爲90的了,一對多的關係若是sum,是會出現笛卡爾積的錯誤的。

由於,咱們須要使用disdict去重,因而,咱們重寫代碼後爲:

SELECT
    vt1.projectId,
    SUM(vt1.bugAmo),
    vt1.projectName
FROM
    (
        SELECT DISTINCT
            zp.id AS projectId,
            zp.budgetary_amount AS bugAmo,
            zp.`name` AS projectName
        FROM
            zq_project zp
        LEFT JOIN zq_project_person zper ON (
            zper.is_deleted = 0
            AND zper.project_id = zp.id
        )
        LEFT JOIN zq_project_process zpro ON (
            zpro.is_deleted = 0
            AND zpro.project_id = zp.id
        )
        WHERE
            zp.is_deleted = 0
        AND zp.id = 167
    ) AS vt1

此時,將其去重後的數據做爲虛擬表,放置在from裏面,咱們拿到的數據就是正確的,如圖所示:

去重後的數據

若是,咱們想要查找所有項目的統計金額,也能夠重寫代碼。

重寫代碼的思想:咱們先將查詢結果去重,獲得去重後的虛擬表;再過濾虛擬表的數據,從虛擬表中統計數據,因而乎獲得:

SELECT
    SUM(vt1.bugAmo) AS toalBugAmo
FROM
    (
        SELECT DISTINCT
            zp.id AS projectId,
            zp.budgetary_amount AS bugAmo,
            zp.`name` AS projectName
        FROM
            zq_project zp
        LEFT JOIN zq_project_person zper ON (
            zper.is_deleted = 0
            AND zper.project_id = zp.id
        )
        LEFT JOIN zq_project_process zpro ON (
            zpro.is_deleted = 0
            AND zpro.project_id = zp.id
        )
        WHERE
            zp.is_deleted = 0
    ) AS vt1
GROUP BY
    vt1.projectId
HAVING
    toalBugAmo <= 12000
ORDER BY
    toalBugAmo DESC

這個執行結果爲:

修改後的執行結果

結尾

任何一門語言,只要掌握住了,它的機制是怎麼運行的,你也就學會了如何優化,提高該語言的性能等。只要你真正掌握住了一門變成語言,你掌握其餘的編程語言,學起來就很是地快。

相關文章
相關標籤/搜索