日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

count 浅析

發布時間:2023/12/16 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 count 浅析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

作者:張政俊,中歐基金DBA

上次打了慢sql日志,發現有很多包含count邏輯的sql,周末抽空來梳理下mysql里的count。

一. count(*)的實現與執行

在mysql中,不同的存儲引擎,count(*)的實現方式是不同的

  • Myisam:

    Myisam會把表的行數存在磁盤上,每當執行count(*)的時候,直接返回就行了,所以速度非常快。

  • Innodb:

    Innodb執行count(*)的時候,需要一條一條把數據從存儲引擎里讀出來,然后累計計數。

既然myisam的count這么快,為什么innodb不能基于myisam的原理也去把行數存起來呢?

主要還是因為 MVCC。

1. Innodb 引擎下的 count

MVCC限制了innodb存儲引擎不可以記錄行數。

假設同一時間有多個查詢會話,test表共有100條數據:

session Asession Bsession C
begin;

select count(*) from test;


insert into test 插入一行


begin;


insert into test 插入一行
select count(*) from test;共100select count(*) from test;共101select count(*) from test;共102

可以看到在最后時刻,每個session拿到的總行數是不一樣的。

mysql5.6后默認的隔離級別是RR(目前生產也是使用的RR),它是通過多版本并發機制實現的。在count的時候,每一行記錄都要判斷自己是否對這個會話可見,所以innodb只能把數據一行一行地讀出來依次判斷,如果判斷為當前session可見行,那就把它加到統計的總行數上。

2. count(*) 執行計劃

先模擬點數據,看看 count(*) 時mysql自身給出的執行計劃

建表:

CREATE TABLE `count_test` (`id` bigint NOT NULL AUTO_INCREMENT,`var_col` varchar(300) NOT NULL,`int_col` int(11) NOT NULL,`insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,PRIMARY KEY (`id`),KEY `var_col` (`var_col`,`int_col`),KEY `create_time` (`insert_time`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

存儲過程插入數據:

CREATE PROCEDURE insert_person()begindeclare i integer default 1;while i<=200000 doinsert into count_test values(i, concat('var_col',i), i, date_sub(NOW(), interval i second));set i=i+1;end while; endcall insert_count_test();

查看執行計劃:

select count(*) from count_test; image

可以看到,使用了 create_time 的普通索引。這里很多人會有疑惑,執行計劃為什么不走主鍵呢?

3. count(*) 的內部優化

innodb是索引組織表,主鍵索引的葉子結點存放的是完整數據,普通索引葉子結點存放的是主鍵值。因此,普通索引要比主鍵索引小得多(除非全表所有列設為一個聯合索引)。

在執行count(*)的時候,遍歷哪個索引樹得到的結果都是一樣的,所以mysql優化器會去尋找最小的那顆樹來遍歷。

數據庫系統設計的原則之一,就是在保證邏輯正確的前提下,盡量減少掃描的數據量。

4. rows 的計算

還有個地方可以很快地返回全表行數:

執行計劃或者?show table status?命令會輸出當前表的行數信息(rows),

這個語句結果返回非常非常快,不像是一張張表 count(*) 出來的。那這個rows是不是就是表的真實行數呢?

實際上它類似索引統計值,是通過采樣來估算的,官方文檔說誤差有可能達到40%以上,所以這個不具備使用條件。

采樣統計:innodb默認選擇N個數據頁,統計這些頁上的不同值,得到一個平均值后,再乘以這個索引的頁面數

5. 小結

  • Mysiam快,但是不支持事務,而且加上where條件判斷后,就沒了快的優勢;

  • show table status 命令雖然返回很快,但是值不準確;

  • innodb直接count(*)會遍歷全表,性能較差;

count(*) 函數調用時,是先要把表中數據加載到內存緩沖區,然后掃描全表獲得行的總記錄數。如果使用主鍵的話,innodb 先要讀取所有20萬數據到數據緩沖區,而且主鍵葉子結點存有所有字段的數據,這個操作需要消耗很多I/O。

而輔助索引,只保存index的值,不包含其他字段數據,I/O消耗要少很多,所以執行速度會更快。

二. Mysql 中各類的count

1. count(主鍵id)

innodb引擎會遍歷全表,把每一行id都取出來,返回給server層,逐條累加。

2. count(1)

innodb引擎會遍歷整張表,但是不取值,server層對于返回的每一行放一個數字“1”進去,逐行累加。

3.count(字段)

  • 如果字段是not null ,一行行從記錄里讀出這個字段,逐行累加;

  • 如果允許為null,取值的時候需要判斷,不為null的,才累加;

  • 4. 小結

    因為count(*)是特殊優化過的,幾個count性能排序如下:

    count(*) 大于等于 count(1) 大于 count(主鍵id) 遠大于 count(字段)

    三. 優化count統計方案

    我們再回過頭來看下,有沒有好一點的方案能解決innodb下count(*)慢的情況呢?優化思想應該還是通過存下該數據,需要的時候,可以快速響應。

    方案一:緩存

    想要快,用緩存。

    比如用redis,當表中有數據插入式時,redis計數就加1,刪除數據的時候,redis減1。

    這種方案也是存在風險的:

    1. 持久化風險:

    redis是存在內存中,你可以使用rdb或者aof去持久化,如果剛插入條數據redis在內存中加1了,但是這是redis重啟了,重新啟動后redis加載的備份文件中沒有新加的1,那這時候數據就不一致了。

    當然上面的情況可以通過其他方式處理,就是需要額外考慮性能和成本,比如redis重啟后,先去數據庫里count一把,把它寫回redis中。

    2. 邏輯風險:

    session Asession B
    插入一條記錄

    讀取redis的計數,發現是100
    redis計數加1

    在并發系統里,無法精準控制不同線程的執行時間,如上圖,即使redis正常工作,個別情況下計數的邏輯還有有點不夠精準的。

    方案二:數據庫中保存

    新建一張專門計數的表,專門存放表的計數數據。

    這樣即使數據庫重啟起碼數據不會丟(redo log保證)。

    session Asession B
    begin;
    計數表記錄加1

    begin;

    讀取計數表,查到100條

    commit;
    插入一條數據
    commit;

    session B 是獨立的事務,因為session A沒提交,所以計數表記錄加1這個操作是對B不可見的。

    通過數據庫事務的特性,把執行時序的問題給解決掉。

    方案三:其他數據庫

    其他數據庫的話首推 clickhouse,之前測試ch時發現執行count(*)速度非常快,截一張當時的PPT:

    image

    當然異構數據庫最大的問題就是要解決增量同步。mysql 同步至 CH,目前大多數的方案是使用python工具,該方案還不成熟,相信隨著時間推移會有更好的方案,屆時很多 OLAP 或者 count(*) 業務都可以在 clickhouse 上進行。

    小結

    如果對行數這種實時性、響應性要求很高,而數據庫本身也已無法滿足,這時候才應該考慮去持久化計數。各種方案都是有利有弊,找到合適自己的才是最好的。

    四. 關于查詢成本

    在測試count性能時,想到了select操作會涉及查詢成本,于是特意把之前寫的有關查詢成本的內容貼了過來,希望可以幫到大家,也給自己做個知識點回顧。

    執行計劃

    再額外看下mysql的查詢成本,以一條sql為例:

    SELECT* FROMcount_test WHEREvar_col > 'var_co1123456' AND insert_time < '2020-10-26 10:10:12' image

    這條sql不出意外掃了全表,可能是由于用了 select * 需要回表,開銷較大。接下來改成索引覆蓋的形式。

    索引覆蓋:
    SELECTinsert_time FROMcount_test WHEREvar_col > 'var_co1123456' AND insert_time < '2020-10-26 10:10:12' image

    執行計劃顯示還是用了全表。

    索引覆蓋+強制索引:

    使用 force index ,讓它強制使用時間索引:

    image

    執行計劃用到了時間索引。

    查詢成本核算

    核算公式:

    cost = rows*0.2 + data_length/(1024*16)
    1. 全表查詢成本
    image

    199644 * 0.2 + 9977856 / (1024 * 16) = 40,537.8

    代入公式可以算出,全表的成本約為 40537.8

    2. 各索引查詢成本

    通過 optimizer_trace 方式查看:

    SET optimizer_trace="enabled=on";SELECT insert_time FROM count_test WHERE var_col > 'var_co1123456' AND insert_time < '2020-10-26 10:10:12';SELECT * FROM information_schema.OPTIMIZER_TRACE;SET optimizer_trace="enabled=off";

    然后看下走索引的預估成本:


    optimizer_trace 下全表查詢的預估成本:

    40540 和我們之前計算的 40537.8 差不多,這個值要遠小于走索引的成本。

    所以 mysql 在執行此 sql 的時候會使用全表掃描,都是基于執行成本來判斷的。

    全文完。

    Enjoy MySQL :)

    總結

    以上是生活随笔為你收集整理的count 浅析的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。