本週SQL優化實戰分享

分享一下本週SQL優化的兩個場景。
如果能對讀者有一定的啓發,共同探討,不勝榮幸。

版本信息:mysql,5.7.19
引擎: innodb


場景1


我們有一張常口表,裏面的數據由各種數據源合併而來,所以人員可能有多個手機號其中還包括座機號。這點在這篇文章裏也分享過。https://juejin.cn/post/7234355976458518586
現在人員詳情頁面需展示同手機號的人員列表,同手機號是包含,而非等同關係。

在人員列表裏手機號頁面有做展示,那麼點擊跳轉人員詳情的時候,是可以把手機號通過URL帶過來的,但前端說參數過多,不好控制,所以只傳遞了人員ID參數。

所以後端查詢的時候先得通過主鍵ID把手機號查出來。之所以不一次性通過join帶出手機號再關聯同手機號人員,是關聯與被關聯人員手機號都可能存在多個。


select * 還是select 指定字段


原通過主鍵查詢手機號的SQL,是直接用的mybatis生成器自動生成的SQL。

<select id="selectPhoneByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap">
    select
    phone
    from t_person_info
    where ID = #{id,jdbcType=VARCHAR}
  </select>

Base_Column_List可想而知是全部字段,類似於select *,這本身沒什麼,但其中有一部份字段長度在幾百,全部加起來也算是個大字段,全部提取對效率還是有一定的影響,所以改爲select phone 查詢手機一個字段。

select
    phone
    from t_person_info
    where ID = #{id,jdbcType=VARCHAR}

這一點的優化相對比較雞肋,都在1-2ms之間看不出明顯差別,但把limit放大的時候,還是能看出差距。

表數據70萬左右。

select * form table limit 10000

select phone form table limit 10000

174ms vs 7ms

確實是聊勝於無。
但是到底是select * 還是select 指定字段,確實還是存在着一些爭議。

一般情況下,表字段少,且不存在大字段,用select * 確實能減少許多麻煩,加減字段不用改sql,多個查詢子功能可以共用等。
而且,頁面查詢多是分頁,不太可能一下子查詢10000條這種情況。

佔用內存,不必要的IO,增加網絡負擔,拒絕覆蓋索引,確實也是select *的問題。

我覺得需要根據具體情況,自行判斷,沒必要太過教條。


全文檢索


拿到手機號以後,根據手機號去查詢關聯人員。
因爲是包含關係,所以同事一開始用的是like模糊匹配。

 select p.id, p.id as pid,p.name,p.idcard,p.phone,count( w.EVENT_NO ) AS count 
      from t_person_info p 
      left join t_other w on w.pid = p.ID
        where
            <foreach collection="phones" item="phone" separator="or" open="(" close=")">
                p.phone like concat("%",#{phone},"%")
            </foreach>
         and p.id != #{id}
        group by p.id

這裏的!=有可能會導致索引失效,這時候可以在sql去掉,然後在代碼中過濾掉當前人員。

因爲where條件中有 p.id != #{id},執行計劃倒是從從ALL上升到了range。 耗時1.5秒。

將phone加上全文索引。 where 條件改爲

match(p.phone) against (#{phones} IN boolean MODE) and p.id != #{id}

每個手機號需要全匹配,所以這裏使用布爾模式,
因爲手機號有多個,需要做到or,
又因爲涉及到座機號,其中帶的-可能會被mysql識別爲邏輯運算符。

具體參照我寫的這篇文章 https://juejin.cn/post/7234355976458518586


布爾模式的邏輯運算符


  1. +
    select * from t_user where match(phone) AGAINST('a +b' in boolean mode)
    其中 + 會被識別成邏輯運算符,而不是將a +b作爲一個整體,以下同理。
    'a +b' 指'a'和'b'必須同時出現才滿足搜索條件。
  2. -
    select * from t_user where match(phone) AGAINST('0797 -12345' in boolean mode)
    0797 -123450797必須包含,但不包含12345才能滿足搜索條件。
    以下查詢排除了包含0797-12345的記錄。

    注意-前後空格 0797 -12345才表示包含0797 同時不包含12345.
    0797-12345等於0797 - 12345,它並不等於0797 -12345
    有圖爲證:

  3. > <
    提高/降低該條匹配數據的權重值。不管使用>還是 <,其權重值均大於沒使用其中任何一個的。
    select * from t_user where match(phone) AGAINST('0797(>94649 <12345)' in boolean mode)
    表示匹配0797,同時包含94649的列往前排,包含12345的往後排
    select * from t_user where match(phone) AGAINST('a > b' in NATURAL LANGUAGE mode)
  4. ()
    相當於表達式分組,參考上一個例子。
  5. *
    通配符,只能在字符串後面使用
  6. "
    完全匹配,被雙引號包起來的單詞必須整個被匹配。
    select * from t_user where match(phone) AGAINST('"0797-1789"' in boolean mode)
    "0797-1789"中不可再分。其它包含0797-1234等記錄就不再匹配。
  7. 空格表示 or

這裏使用6,7來解決上述的兩種問題。
如下SQL,與以下4個手機號其中一個全區配的人員都將被篩選出來。

#{phone}參數應爲"135****6" "136****9" "1387****2" "0791-123"格式 。

耗時從1.5秒降到了2毫秒。


場景2

還是常口表,列表查詢。


排序


每個用戶呢會關聯一些事件,無需理會什麼是事件,反正這張表中的每條記錄與事件表形成一對多的關聯關係。
事件實時進入。然後再用戶列表展示的時候需要根據關聯的事件數來進行排序。

實時join關聯事件表,耗時4.9秒。
sql執行計劃 extra爲 Using temporary; Using filesort 產生了臨時表和IO文件排序。當然快不起來。

這還是在沒有查詢條件,以及沒有深度分頁的情況下。

那麼很明顯,需要在用戶表建一個冗餘字段,保存用戶所關聯的事件數,再對這個字段建立索引。

但這會犧牲一定的實時性。
以及需要定時任務去統計用戶的關聯事件數。

然後需要跟產品溝通,因爲我們的產品是2B的,還需要跟客戶進行溝通。

結合我們的業務場景,經過我們的努力溝通,客戶認爲犧牲適當的實時性,換來頁面的響應效率,是值得的。

然後耗時降到了3毫秒。

一旦 where having order by 裏的字段是通過max,min,count等計算出來的虛擬字段,那麼肯定會產生 Using temporary; Using filesort 臨時表和IO文件排序。
要想辦法消滅,不管從業務還是技術上。

適當的建立冗餘字段,或者寬表。

但阿里巴巴java開發手冊,禁止3張表以上的關聯,畢竟只是比較理想的狀態。

幸福的公司都是 相似 的;不幸的公司我看也有相似不幸。
不外乎難搞的產品,多變的客戶,睿(s)智(13)的老闆。


深度分頁


上面小節同樣的sql,首頁查詢只需耗時2ms,但是到了700000以後,耗時達到了2.6秒。

這就是著名的mysql深度分頁的問題。
通過執行計劃,可以明顯的看出,mysql會將前 700015條數據取出來,然後丟掉前700000條,只取後15條數據。
前面讀取的700000條數據是不必要耗時操作。

解決深度分頁的方式有幾種。 看具體情況,沒有通用的辦法。


利用覆蓋索引


或者叫利用不回表。
這裏爲了便利,用主鍵索引id來演示,innodb下,主鍵索引爲聚簇索引,本身就是回表啦,相當於普通索引省掉了回表操作。

如此查詢只需200毫秒左右。

但是,這裏不合適把需要展示的字段全部建成一個覆蓋索引。


利用覆蓋索引延遲關聯


先通過覆蓋索引把id拿到,再把這15條數據去關聯一次拿到其它字段不就好了嗎?

select p.id ,p.name,p.idcard,p.phone
from t_person_info p
inner join (select id from t_person_info order by EVENTCOUNT desc limit 700000,15) p2 on p.id = p2.id

如此同樣只需要200毫秒左右。


其它方式


其它方式,通過記錄上次的位置,通過子查詢,都只適用於id爲自增主鍵的情況。

不適用我的這個業務場景。

類似於 這樣的SQL

select id ,name,idcard,phone,EVENTCOUNT from t_person_info where id <=(select id from t_person_info order by EVENTCOUNT limit 700000, 1) limit 15;

由於歷史友商等原因,我們的數據ID有部份是UUID,它是不連續的,且人員關聯事件數EVENTCOUNT也不連續,大量的人員集中在某一個數量上,這都使得此種方式不可取。


分頁插件


在做列表展示時肯定需要分頁,分頁就需要查詢總數。
分頁插件pagehelper默認會生成一個查詢總數的方法。

假如mapper查詢方法爲selectList(),那麼查詢總數的方法名爲selectList_COUNT()。
對應的SQL爲SELECT count(0) FROM 原sql

在一些比較比較簡單的SQL的時候,分頁的SQL還是會進行重寫,比較去掉多餘的select字段,不必要的排序等。

但當SQL比較複雜的時候,那就是直接在原SQL上包一層select count(0)。

這個時候我們就可以自已去實現這個selectList_COUNT()這個方法,讓它執行效率更高的自定義SQL.


完。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章