<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          MySQL千萬(wàn)大表優(yōu)化實(shí)踐

          共 7775字,需瀏覽 16分鐘

           ·

          2020-09-17 12:09

          點(diǎn)擊上方藍(lán)色字體,選擇“設(shè)為星標(biāo)

          回復(fù)”資源“獲取更多資源

          大數(shù)據(jù)技術(shù)與架構(gòu)
          點(diǎn)擊右側(cè)關(guān)注,大數(shù)據(jù)開(kāi)發(fā)領(lǐng)域最強(qiáng)公眾號(hào)!

          暴走大數(shù)據(jù)
          點(diǎn)擊右側(cè)關(guān)注,暴走大數(shù)據(jù)!


          前段時(shí)間筆者遇到一個(gè)復(fù)雜的慢查詢,今天有空便進(jìn)行了整理,以便日后回顧。舉一個(gè)相似的業(yè)務(wù)場(chǎng)景的例子。以文章評(píng)論為例,查詢20191201~20191231日期間發(fā)表的經(jīng)濟(jì)科技類別的文章,同時(shí)需要顯示這些文章的熱評(píng)數(shù)目

          涉及到的四張表結(jié)構(gòu)如下所示

          文章表結(jié)構(gòu)和索引信息如下,文章表中存儲(chǔ)了200萬(wàn)數(shù)據(jù)

          評(píng)論表結(jié)構(gòu)和索引信息如下,評(píng)論表存儲(chǔ)了1000萬(wàn)數(shù)據(jù)

          文章分類表結(jié)構(gòu)如下,這張表數(shù)據(jù)比較少,僅僅存儲(chǔ)了300條數(shù)據(jù)

          用戶表結(jié)構(gòu)如下,該表存儲(chǔ)了100萬(wàn)數(shù)據(jù)

          其中涉及到的慢查詢語(yǔ)句如下所示,這個(gè)查詢語(yǔ)句性能非常慢,執(zhí)行時(shí)間接近60s

          SELECT
          tb_article.`title`,
          tb_user.`name`,
          count( 1 ) AS `total`
          FROM
          tb_article
          LEFT JOIN tb_cmt ON tb_article.`id` = tb_cmt.`article_id`
          INNER JOIN tb_user on tb_article.`userid` = tb_user.`id`
          WHERE
          tb_article.`type` IN (
          SELECT code
          FROM tb_category
          WHERE code like '12%' or code like '13%'
          )
          AND tb_cmt.`upvote` > 100
          AND tb_cmt.`len` BETWEEN 10 AND 30
          AND tb_article.`create_time` BETWEEN '2019-12-01 00:00:00' AND '2019-12-31 23:59:59'
          GROUP BY
          tb_cmt.`article_id`

          使用explain分析慢查詢的執(zhí)行流程

          Mysql執(zhí)行流程如下,首先mysql以tb_category作為驅(qū)動(dòng)表,看到這,有沒(méi)有感到很奇怪,tb_category在整個(gè)查詢中只是作為一個(gè)子查詢存在,tb_category怎么成為驅(qū)動(dòng)表了呢?如果讀者了解mysql的in子查詢?cè)淼脑捑秃芎美斫饬耍琺ysql會(huì)將in查詢改寫為semi-join關(guān)聯(lián)查詢,explain涉及到的start temporary和end temporary用于semi-join的去重。我們可以使用explain extended和show warnings查看mysql改寫的的查詢語(yǔ)句,mysql改寫后的查詢語(yǔ)句如下所示

          Mysql為什么選擇tb_category作為驅(qū)動(dòng)表呢?原因是tb_category的表最小,只有300條數(shù)據(jù),mysql查詢優(yōu)化器通常情況下都會(huì)以小表作為驅(qū)動(dòng)表。

          隨后,tb_category和tb_article進(jìn)行關(guān)聯(lián)計(jì)算,關(guān)聯(lián)計(jì)算的列是tb_article的type列,mysql使用了tb_article表上的type_time_idx的索引,這個(gè)過(guò)程mysql使用了Batched Key Access進(jìn)行了優(yōu)化以達(dá)到減少索引回表查找的IO次數(shù),隨后關(guān)聯(lián)tb_cmt表,這次關(guān)聯(lián)中,mysql使用了tb_cmt的article_id_idx字段。經(jīng)過(guò)上述關(guān)聯(lián),mysql生成了一個(gè)結(jié)果集,mysql再在結(jié)果集上對(duì)upvote,type和len字段進(jìn)行where條件篩選,最后進(jìn)行了一次group by操作。

          優(yōu)化的核心思路仍然是減少掃描的行數(shù),從上述的explain結(jié)果上看,掃描的rows行數(shù)好像不是很多,但是tb_category,tb_article,tb_cmt,tb_user四張表關(guān)聯(lián)之后生成的結(jié)果集非常的龐大,筆者使用如下代碼進(jìn)行以一次計(jì)算

          SELECT
          count(*)
          FROM
          tb_article
          LEFT JOIN tb_cmt ON tb_article.`id` = tb_cmt.`article_id`
          INNER JOIN tb_user on tb_article.`userid` = tb_user.`id`
          WHERE
          tb_article.`type` IN (
          SELECT code
          FROM tb_category
          WHERE code like '12%' or code like '13%'
          )

          結(jié)果如下

          四張表的關(guān)聯(lián)結(jié)果集有611萬(wàn)數(shù)據(jù)

          如果讀者了解Mysql關(guān)聯(lián)查詢?cè)淼脑挘x者便會(huì)知道m(xù)ysql的關(guān)聯(lián)查詢之后,如果再進(jìn)行條件篩選是無(wú)法使用非驅(qū)動(dòng)表索引的(換一句話講,mysql關(guān)聯(lián)查詢只會(huì)使用驅(qū)動(dòng)表的索引進(jìn)行條件篩選),也就是說(shuō)下面幾個(gè)條件都是無(wú)法使用索引的

          在611萬(wàn)結(jié)果集上進(jìn)行upvote,len,create_time條件篩選和group by操作性能可想而知很慢了。筆者希望在執(zhí)行關(guān)聯(lián)查詢的時(shí)候可以盡量多的使用索引,比如upvote_len_idx,create_time_idx索引,所以驅(qū)動(dòng)表一定不能是tb_category。和1000萬(wàn)數(shù)據(jù)量的tb_cmt表相比,筆者更希望以只有200萬(wàn)數(shù)據(jù)量的tb_article表作為驅(qū)動(dòng)表。

          步驟一:避免semi-join

          如果筆者希望以tb_article作為驅(qū)動(dòng)表,那么一定要避免in的關(guān)聯(lián)子查詢,因?yàn)閙ysql在執(zhí)行in關(guān)聯(lián)子查詢的時(shí)候,會(huì)將其轉(zhuǎn)化為semi-join,因?yàn)閠b_category數(shù)據(jù)量少,mysql查詢優(yōu)化器會(huì)使用tb_category作為驅(qū)動(dòng)表。

          避免semi-join的關(guān)鍵是避免in子查詢,筆者將上述查詢語(yǔ)句拆分為兩個(gè)查詢語(yǔ)句,在應(yīng)用服務(wù)層首先執(zhí)行如下語(yǔ)句選出經(jīng)濟(jì),科技類型文章的編碼

          SELECT code
          FROM tb_category
          WHERE code like '12%' or code like '13%'

          然后再將上述結(jié)果代入到原來(lái)查詢中,查詢語(yǔ)句修改如下

          SELECT
          tb_article.`title`,
          tb_user.`name`,
          count( 1 ) AS `total`
          FROM
          tb_article
          LEFT JOIN tb_cmt ON tb_article.`id` = tb_cmt.`article_id`
          INNER JOIN tb_user on tb_article.`userid` = tb_user.`id`
          WHERE
          tb_article.`type` IN (
          '1213331',
          '1374609',
          '1389750',
          '1204526',
          '1382565',
          '1239054',
          '1321189',
          '1292666'
          )
          AND tb_cmt.`upvote` > 100
          AND tb_cmt.`len` BETWEEN 10 AND 30
          AND tb_article.`create_time` BETWEEN '2019-12-01 00:00:00' AND '2019-12-31 23:59:59'
          GROUP BY
          tb_cmt.`article_id`

          優(yōu)化之后查詢耗時(shí)18s,性能有了非常大的提升,我們?cè)倏匆幌聝?yōu)化后的explain結(jié)果

          我們看到,mysql以tb_article作為驅(qū)動(dòng)表,并且查詢不再涉及semi-join,達(dá)到了當(dāng)前步驟的優(yōu)化目的

          步驟二:盡力使用索引

          當(dāng)前的查詢語(yǔ)句以tb_article作為驅(qū)動(dòng)表,同時(shí)使用了tb_article上的type_time_idx索引過(guò)濾tb_article表,然后關(guān)聯(lián)tb_cmt表,這個(gè)關(guān)聯(lián)過(guò)程只會(huì)使用tb_cmt一個(gè)索引article_id,而tb_cmt存儲(chǔ)有1000萬(wàn)數(shù)據(jù),即使使用了article_id這個(gè)索引,最終會(huì)生成一個(gè)134萬(wàn)的結(jié)果集,在134萬(wàn)的結(jié)果集上進(jìn)行如下條件過(guò)濾和group by mysql的性能仍然會(huì)非常差。

          tb_cmt.`upvote` > 100
          tb_cmt.`len` BETWEEN 10 AND 30
          GROUP BY
          tb_cmt.`article_id`

          筆者希望tb_article僅僅和熱門評(píng)論進(jìn)行關(guān)聯(lián),掃描的數(shù)據(jù)就大大減少。利用這個(gè)思路筆者重新編寫sql語(yǔ)句如下

          select
          tb_article.`title`,
          tb_user.`name`,
          count( 1 ) AS `total`
          from tb_article
          LEFT JOIN (
          SELECT article_id FROM tb_cmt
          WHERE tb_cmt.upvote > 100
          AND tb_cmt.len BETWEEN 10 AND 30
          ) t
          on t.article_id=tb_article.id
          INNER JOIN tb_user ON tb_article.userid = tb_article.userid
          AND tb_article.create_time BETWEEN '2019-12-01 00:00:00' AND '2019-12-31 23:59:59'
          AND tb_article.type IN(
          '1213331',
          '1374609',
          '1389750',
          '1204526',
          '1382565',
          '1239054',
          '1321189',
          '1292666'
          )
          GROUP BY article_id

          為了使用tb_cmt上的upvote_len_idx索引,筆者延遲了tb_cmt關(guān)聯(lián),先對(duì)tb_cmt進(jìn)行了篩選。雖然這個(gè)查詢會(huì)生成一個(gè)臨時(shí)表t,但是臨時(shí)表t比較小,數(shù)據(jù)量不足10萬(wàn),所以這個(gè)臨時(shí)表也不會(huì)造成太大的性能負(fù)擔(dān)。但是tb_cmt的子查詢卻無(wú)法使用upvote_len_idx索引,我們還得對(duì)范圍查詢進(jìn)行優(yōu)化

          步驟三:范圍查詢優(yōu)化

          筆者讓tb_article和篩選過(guò)的評(píng)論表即熱評(píng)表t進(jìn)行關(guān)聯(lián),但是發(fā)現(xiàn)評(píng)論的子查詢表仍然不使用upvote_len_idx索引,原因是tb_cmt.upvote > 100是一個(gè)范圍查詢,而tb_cmt.len BETWEEN 10 AND 30也是一個(gè)范圍查詢,mysql不支持松散索引掃描,無(wú)法在同一個(gè)索引上使用兩個(gè)范圍查詢。優(yōu)化思路是將兩個(gè)范圍查詢優(yōu)化為一個(gè)范圍查詢,將tb_cmt.len BETWEEN 10 AND 30優(yōu)化為散列值,同時(shí)刪除原來(lái)的upvote_len_idx,創(chuàng)建len_upvote_idx索引,目的是將需要范圍掃描的upvote字段置為組合索引的尾部。

          優(yōu)化之后代碼如下所示

          SELECT
          tb_article.`title`,
          tb_user.`name`,
          count( 1 ) AS `total`
          from tb_article
          LEFT JOIN (
          SELECT article_id FROM tb_cmt
          WHERE tb_cmt.upvote > 100
          AND tb_cmt.len in (10 ,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29, 30 )
          ) t
          on t.article_id=tb_article.id
          INNER JOIN tb_user ON tb_user.id = tb_article.userid
          AND tb_article.create_time BETWEEN '2019-12-01 00:00:00' AND '2019-12-31 23:59:59'
          AND tb_article.type IN(
          '1213331',
          '1374609',
          '1389750',
          '1204526',
          '1382565',
          '1239054',
          '1321189',
          '1292666'
          )
          GROUP BY article_id

          在這一步優(yōu)化之后,筆者再次執(zhí)行查詢,發(fā)現(xiàn)性能變得更差了,原本18秒可以運(yùn)行結(jié)束的查詢,現(xiàn)在需要40s。原因是什么呢?因?yàn)閠表的生成過(guò)程完全走在索引上,所以t表的生成過(guò)程不是性能瓶頸所在,所以筆者猜測(cè)是引入的t表和tb_article表左關(guān)聯(lián)時(shí)候性能太差的原因,于是筆者注釋掉生成t表的子查詢以驗(yàn)證筆者的猜想,注釋后的代碼如下所示

          SELECT
          tb_article.`title`,
          tb_user.`name`,
          count( 1 ) AS `total`
          from tb_article
          -- LEFT JOIN (
          -- SELECT article_id FROM tb_cmt
          -- WHERE tb_cmt.upvote > 100
          -- AND tb_cmt.len in (10 ,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29, 30 )
          -- ) t
          -- on t.article_id=tb_article.id
          INNER JOIN tb_user ON tb_user.id = tb_article.userid
          AND tb_article.create_time BETWEEN '2019-12-01 00:00:00' AND '2019-12-31 23:59:59'
          AND tb_article.type IN(
          '1213331',
          '1374609',
          '1389750',
          '1204526',
          '1382565',
          '1239054',
          '1321189',
          '1292666'
          )
          GROUP BY tb_article.id

          上述查詢耗時(shí)5.26秒,驗(yàn)證了筆者的上述猜想,但是筆者也沒(méi)有太好的辦法解決這個(gè)問(wèn)題,筆者在嘗試group by優(yōu)化時(shí)意外找到了優(yōu)化方案

          步驟四 group by優(yōu)化

          仔細(xì)觀察這個(gè)sql語(yǔ)句,我們可以發(fā)現(xiàn)GROUP BY這個(gè)操作既可以放在臨時(shí)表t中,又可以放在關(guān)聯(lián)后的結(jié)果集上進(jìn)行,我們?nèi)绾芜x擇呢?group by無(wú)法使用索引,只能使用臨時(shí)表,所以我們應(yīng)該讓需要被group by的數(shù)據(jù)盡量的少,而tb_article和tb_cmt是左關(guān)聯(lián),所以應(yīng)該將group by操作放在tb_cmt子查詢內(nèi)部進(jìn)行。除此之外,group by 優(yōu)化還有一個(gè)小技巧,mysql在執(zhí)行g(shù)roup by的時(shí)候,默認(rèn)會(huì)進(jìn)行排序,在當(dāng)前業(yè)務(wù)中,筆者并不需要進(jìn)行排序,于是筆者在group by 末尾追加order by null ,最終優(yōu)化的sql結(jié)果為

          SELECT
          tb_article.`title`,
          tb_user.`name`,
          `total`
          from tb_article
          LEFT JOIN (
          SELECT article_id ,count( 1 ) AS `total` FROM tb_cmt
          WHERE tb_cmt.upvote > 100
          AND tb_cmt.len in (10 ,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29, 30 )
          GROUP BY article_id
          ORDER BY null
          ) t
          on t.article_id=tb_article.id
          INNER JOIN tb_user ON tb_user.id = tb_article.userid
          AND tb_article.create_time BETWEEN '2019-12-01 00:00:00' AND '2019-12-31 23:59:59'
          AND tb_article.type IN(
          '1213331',
          '1374609',
          '1389750',
          '1204526',
          '1382565',
          '1239054',
          '1321189',
          '1292666'
          )

          整個(gè)查詢耗時(shí)1.3秒,和原查詢耗時(shí)60秒相比,已經(jīng)有了近60倍性能提升。我們?cè)倏匆幌翬xplain分析

          可以看到在將group by放在子查詢內(nèi)部的時(shí)候,生成的臨時(shí)表t好像出現(xiàn)了一個(gè)索引,正是這個(gè)key加速了tb_article和臨時(shí)表t的關(guān)聯(lián)查詢。

          版權(quán)聲明:

          本文為大數(shù)據(jù)技術(shù)與架構(gòu)整理,原作者獨(dú)家授權(quán)。未經(jīng)原作者允許轉(zhuǎn)載追究侵權(quán)責(zé)任。
          編輯|冷眼丶
          微信公眾號(hào)|import_bigdata


          歡迎點(diǎn)贊+收藏+轉(zhuǎn)發(fā)朋友圈素質(zhì)三連


          文章不錯(cuò)?點(diǎn)個(gè)【在看】吧!??

          瀏覽 37
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  xxxx黄色 | 大香蕉一人在线 | 国产精品久久久久久爽爽爽麻豆色哟哟 | 青青久久伊人春色 | 看操年轻小媳妇逼毛片视频 |