計算MySQL表碎片的SQL整理

  • 2019 年 10 月 7 日
  • 筆記

這是學習筆記的第 2111 篇文章

在之前整理過一版MySQL的數據字典,整理了一圈,發現遠比想像的複雜。

當然整理的過程不光是知識梳理的過程,也是轉化為實踐場景的一個過程,通過這樣一個體系,對於整個MySQL對象生命周期管理有了較為深入的認識,這裡我來拋磚引玉,來作為深入學習MySQL數據字典的一個入口,這個問題就是:如何較為準確的計算MySQL碎片情況?

我想碎片的情況在數據庫中是很少有清晰的界定,不過它的的確確會帶來副作用,通過修復碎片情況我們可以提高SQL的執行效率,同時能夠釋放大量的空間。

最近在思考中感悟到:我們所做的很多事情,難點主要都在於查找,比如我告訴你test庫的表test_data存在大量碎片,需要修復一下,這個難度是完全可控的,我們可以很麻利的處理好,但是如果我告訴你需要收集下碎片情況,然後做一下改進,而不告訴你具體的情況,其實難度就會高几個層次。

我們這個場景主要會用到兩個數據字典表:

information_schema.tables

information_schema.INNODB_SYS_TABLESPACES

我們依次來看一下兩個數據字典的輸出信息:

查詢常規的數據字典tables得到的信息基本可以滿足我們的大多數需求。

mysql> select *from information_schema.tables where table_name='tgp_redis_command'G

*************************** 1. row ***************************

TABLE_CATALOG: def

TABLE_SCHEMA: tgp_db

TABLE_NAME: tgp_redis_command

TABLE_TYPE: BASE TABLE

ENGINE: InnoDB

VERSION: 10

ROW_FORMAT: Dynamic

TABLE_ROWS: 477103

AVG_ROW_LENGTH: 111

DATA_LENGTH: 53035008

MAX_DATA_LENGTH: 0

INDEX_LENGTH: 0

DATA_FREE: 5242880

AUTO_INCREMENT: 478096

CREATE_TIME: 2019-08-16 10:54:02

UPDATE_TIME: 2019-09-23 21:12:05

CHECK_TIME: NULL

TABLE_COLLATION: utf8_general_ci

CHECKSUM: NULL

CREATE_OPTIONS:

TABLE_COMMENT: redis命令執行記錄表

1 row in set (0.00 sec)

通過tables字典我們可以得到通過邏輯計算出來的預估表大小,包括數據和索引的空間情況,還有平均行長度來作為校驗。

但是在這裡我們總是會感覺有些隔靴搔癢,因為我們通過計算得到了邏輯大小,但是我們還是無從得知物理文件的大小,如果逐個去通過du方式計算,這個成本是很高的,而且如果有很多的表,這種模式的效率和代價是不大合理的,所幸MySQL 5.7版本中的innodb_sys_tablespaces這個數據字典做了擴容,有了新的字段FILE_SIZE,可以完美的解決我們的疑慮,使用innodb_sys_tablespaces得到的結果如下:

mysql> select *from INNODB_SYS_TABLESPACES where name like 'tgp_db/tgp_redis_command'G

*************************** 1. row ***************************

SPACE: 818

NAME: tgp_db/tgp_redis_command

FLAG: 33

FILE_FORMAT: Barracuda

ROW_FORMAT: Dynamic

PAGE_SIZE: 16384

ZIP_PAGE_SIZE: 0

SPACE_TYPE: Single

FS_BLOCK_SIZE: 4096

FILE_SIZE: 62914560

ALLOCATED_SIZE: 62918656

1 row in set (0.00 sec)

比如常規來說我們要得到表tgp_redis_command的物理文件大小(即.ibd文件),可以通過INNODB_SYS_TABLESPACES 來查詢得到,這是一個緩存中刷新得到的實時的值,遠比我們通過du等方式計算要快捷方便許多。

可以做一個簡單的計算,表裡的數據量為:

mysql> select count(*) from tgp_redis_command;

+———-+

| count(*) |

+———-+

| 478093 |

+———-+

1 row in set (0.06 sec)

物理文件的大小,和innodb_sys_tablespaces的結果是完全一致的。

# ll *redis*

-rw-r—– 1 mysql mysql 9176 Aug 16 10:54 tgp_redis_command.frm

-rw-r—– 1 mysql mysql 62914560 Sep 23 21:14 tgp_redis_command.ibd

所以表的大小邏輯計算為data_length+index_length=53035008+0,大約是50M左右,而物理文件大小是60M左右,那麼碎片率大約是(60-50)/60約等於16.7%

我們做一下數據的truncate操作,發現物理文件的大小很快收縮了。

mysql> select *from INNODB_SYS_TABLESPACES where name like 'tgp_db/tgp_redis_command'G

*************************** 1. row ***************************

SPACE: 818

NAME: tgp_db/tgp_redis_command

FLAG: 33

FILE_FORMAT: Barracuda

ROW_FORMAT: Dynamic

PAGE_SIZE: 16384

ZIP_PAGE_SIZE: 0

SPACE_TYPE: Single

FS_BLOCK_SIZE: 4096

FILE_SIZE: 98304

ALLOCATED_SIZE: 102400

1 row in set (0.00 sec)

mysql> select *from information_schema.tables where table_name='tgp_redis_command'G

*************************** 1. row ***************************

TABLE_CATALOG: def

TABLE_SCHEMA: tgp_db

TABLE_NAME: tgp_redis_command

TABLE_TYPE: BASE TABLE

ENGINE: InnoDB

VERSION: 10

ROW_FORMAT: Dynamic

TABLE_ROWS: 0

AVG_ROW_LENGTH: 0

DATA_LENGTH: 16384

MAX_DATA_LENGTH: 0

INDEX_LENGTH: 0

DATA_FREE: 0

AUTO_INCREMENT: 1

CREATE_TIME: 2019-08-16 10:54:02

UPDATE_TIME: 2019-09-24 09:51:22

CHECK_TIME: NULL

TABLE_COLLATION: utf8_general_ci

CHECKSUM: NULL

CREATE_OPTIONS:

TABLE_COMMENT: redis命令執行記錄表

1 row in set (0.00 sec)

[root@hb30-dba-mysql-tgp-124-34 tgp_db]# ll *redis*

-rw-r—– 1 mysql mysql 9176 Aug 16 10:54 tgp_redis_command.frm

-rw-r—– 1 mysql mysql 98304 Sep 24 09:55 tgp_redis_command.ibd

當然這種計算方式是不夠完整的,而且不夠清晰,我們可以寫一個簡單的SQL來做下統計,就是把那些需要修復的表列出來即可。

SQL如下:

SELECT

t.table_schema,

t.table_name,

t.table_rows,

t.data_length+

t.index_length data_size,

t.index_length index_size,

t.avg_row_length,

t.avg_row_length * t.table_rows logic_size,

s.FILE_SIZE,

truncate(s.FILE_SIZE/ (t.data_length+ t.index_length)*1.1*2 ,0)tab_frag

FROM

information_schema.tables t,

information_schema.INNODB_SYS_TABLESPACES s

WHERE

t.table_type = 'BASE TABLE'

and concat(t.table_schema,'/',t.table_name)=s.name

and t.table_schema not in ('sys','information_schema','mysql','test')

— and t.table_schema in('tgp_db','test')

and s.FILE_SIZE >102400000

and (t.data_length+ t.index_length)*1.1*2 < s.FILE_SIZE

order by s.FILE_SIZE;

以如下的輸出為例,我們可以看到整個碎片率極高,基本就是邏輯大小為100M,實際大小為500M,類似這種情況。

其中對於邏輯大小的計算做了一些取捨,默認在MySQL中變化的數據在10%以外是會重新去統計計算的,所以我們可以把基數調整的稍大一些為1.1,然後以這個為基線,如果碎片率超過了200%則計入統計結果中。

通過這種方式我們可以很快的分析出那些要具體修復的表,而整個性能的分析也可以更加清晰。

稍後,把它包裝為一個批量異步任務,通過異步任務來得到儘可能完整的碎片表列表,然後集中去處理就好了。