如何预测 mysql 查询的 IO 计数?
how to predict the IO count of mysql query?
因为 InnoDB 在 B+ 树中组织其数据。树的高度会影响 IO 次数,这可能是导致 DB 变慢的主要原因之一。
所以我的问题是如何预测或计算B+树的高度(例如根据页数可以通过行大小、页大小和行号来计算),从而做出决定是否分片给不同的master
https://www.percona.com/blog/2009/04/28/the_depth_of_a_b_tree/
设 N 为 table.
中的行数
令 B 为适合一个 B 树节点的键数。
树的深度是(log N) / (log B)
。
来自博客:
Let’s put some numbers in there. Say you have a billion rows, and you can currently fit 64 keys in a node. Then the depth of the tree is (log 109)/ log 64 ≈ 30/6 = 5. Now you rebuild the tree with keys half the size and you get log 109 / log 128 ≈ 30/7 = 4.3. Assuming the top 3 levels of the tree are in memory, then you go from 2 disk seeks on average to 1.3 disk seeks on average, for a 35% speedup.
我还要补充一点,通常你不必针对I/O成本进行优化,因为你经常使用的数据应该在InnoDB缓冲池中,因此不会产生任何I/O 阅读它的成本。您应该充分调整缓冲池的大小,以使其适用于大多数读取。
计算更简单
快速而粗略的答案是以 100 为底的对数,四舍五入。也就是说,BTree 中的每个节点大约有 100 个叶节点。在某些圈子里,这称为扇出。
1K 行:2 个级别
1M 行:3 个级别
十亿:5级
兆:6 级
这些数字适用于 "average" 行或索引。当然,对于扇出,您可以使用大约 2 或 1000 的极端值。
精确深度
您可以从一些 information_schema:
中找到实际深度
对于 Oracle 的 MySQL:
$where = "WHERE ( ( database_name = ? AND table_name = ? )
OR ( database_name = LOWER(?) AND table_name = LOWER(?) ) )";
$sql = "SELECT last_update,
n_rows,
'Data & PK' AS 'Type',
clustered_index_size * 16384 AS Bytes,
ROUND(clustered_index_size * 16384 / n_rows) AS 'Bytes/row',
clustered_index_size AS Pages,
ROUND(n_rows / clustered_index_size) AS 'Rows/page'
FROM mysql.innodb_table_stats
$where
UNION
SELECT last_update,
n_rows,
'Secondary Indexes' AS 'BTrees',
sum_of_other_index_sizes * 16384 AS Bytes,
ROUND(sum_of_other_index_sizes * 16384 / n_rows) AS 'Bytes/row',
sum_of_other_index_sizes AS Pages,
ROUND(n_rows / sum_of_other_index_sizes) AS 'Rows/page'
FROM mysql.innodb_table_stats
$where
AND sum_of_other_index_sizes > 0
";
对于 Percona:
/* to enable stats:
percona < 5.5: set global userstat_running = 1;
5.5: set global userstat = 1; */
$sql = "SELECT i.INDEX_NAME as Index_Name,
IF(ROWS_READ IS NULL, 'Unused',
IF(ROWS_READ > 2e9, 'Overflow', ROWS_READ)) as Rows_Read
FROM (
SELECT DISTINCT TABLE_SCHEMA, TABLE_NAME, INDEX_NAME
FROM information_schema.STATISTICS
) i
LEFT JOIN information_schema.INDEX_STATISTICS s
ON i.TABLE_SCHEMA = s.TABLE_SCHEMA
AND i.TABLE_NAME = s.TABLE_NAME
AND i.INDEX_NAME = s.INDEX_NAME
WHERE i.TABLE_SCHEMA = ?
AND i.TABLE_NAME = ?
ORDER BY IF(i.INDEX_NAME = 'PRIMARY', 0, 1), i.INDEX_NAME";
(那些提供的不仅仅是深度。)
PRIMARY
指数据的 BTree。 "n_diff_pfx03" 之类的名称指的是 BTree 的第 3 层; table 的最大此类数字表示总深度。
行宽
关于估计一行的宽度,请参阅 Bill 的回答。这是另一种方法:
- 查找每列的大小(INT=4 字节,对 VAR 使用平均值)
- 求和。
- 乘以 2 到 3(以允许 InnoDB 的开销)
- 分成16K得到叶个节点的平均数。
非叶节点,加上索引叶节点,比较棘手,因为您需要准确理解在此类节点中什么代表了 "row"。
(因此,我的简单化“每个节点 100 行”。)
但是谁在乎呢?
这是另一种似乎很有效的简化方法。由于磁盘命中是查询中最大的性能项,因此需要"count the disk hits"作为判断查询性能的第一顺序。
但是看看 buffer_pool 中的块缓存。父节点最近被触摸的可能性是子节点的 100 倍。
所以,简化为"assume"所有非叶子节点都被缓存,所有叶子节点都需要从磁盘中获取。因此,depth 并不像接触了多少叶节点块那么重要。这降低了您的“35% 加速”——CPU 当然有 35% 的加速,但 I/O[=102 几乎没有加速=].而I/O是重要的组成部分。
请注意,如果您获取按时间顺序存储的 table 的最新 20 行,它们将在最后 1 个(或可能是 2 个)块中找到。如果它们由 UUID 存储,则更有可能达到 20 个块——更多的磁盘命中,因此速度更慢。
二级索引
PRIMARY KEY
与数据聚类。这意味着 PK 的外观需要向下钻取一个 BTree。但是二级索引是由第二个 BTree 实现的——向下钻取它以找到 PK,然后通过 PK 向下钻取。当"counting the disk hits"时,需要同时考虑两个BTrees。并考虑随机性(例如,对于 UUID)或不(按日期排序)。
写
- 找到块(可能缓存)
- 更新它
- 如有必要,处理分块
- 在 buffer_pool
中将块标记为 "dirty"
- 最终将其写回磁盘。
步骤 1 可能 涉及读取 I/O;第 5 步可能涉及写入 I/O -- 但您不会等待它完成。
索引更新
UNIQUE
索引必须在完成 INSERT
之前检查。这涉及潜在缓存读取 I/O.
对于非唯一索引,在 "Change buffer" 中创建一个条目。 (它存在于 buffer_pool 中。)最终它与磁盘上的适当块合并。也就是说,当 INSERTing
行时不等待 I/O(至少不等待更新非唯一索引)。
推论:UNIQUE
索引的成本更高。但是真的需要超过2个这样的索引(包括PK)吗?
因为 InnoDB 在 B+ 树中组织其数据。树的高度会影响 IO 次数,这可能是导致 DB 变慢的主要原因之一。
所以我的问题是如何预测或计算B+树的高度(例如根据页数可以通过行大小、页大小和行号来计算),从而做出决定是否分片给不同的master
https://www.percona.com/blog/2009/04/28/the_depth_of_a_b_tree/
设 N 为 table.
中的行数令 B 为适合一个 B 树节点的键数。
树的深度是(log N) / (log B)
。
来自博客:
Let’s put some numbers in there. Say you have a billion rows, and you can currently fit 64 keys in a node. Then the depth of the tree is (log 109)/ log 64 ≈ 30/6 = 5. Now you rebuild the tree with keys half the size and you get log 109 / log 128 ≈ 30/7 = 4.3. Assuming the top 3 levels of the tree are in memory, then you go from 2 disk seeks on average to 1.3 disk seeks on average, for a 35% speedup.
我还要补充一点,通常你不必针对I/O成本进行优化,因为你经常使用的数据应该在InnoDB缓冲池中,因此不会产生任何I/O 阅读它的成本。您应该充分调整缓冲池的大小,以使其适用于大多数读取。
计算更简单
快速而粗略的答案是以 100 为底的对数,四舍五入。也就是说,BTree 中的每个节点大约有 100 个叶节点。在某些圈子里,这称为扇出。
1K 行:2 个级别 1M 行:3 个级别 十亿:5级 兆:6 级
这些数字适用于 "average" 行或索引。当然,对于扇出,您可以使用大约 2 或 1000 的极端值。
精确深度
您可以从一些 information_schema:
中找到实际深度对于 Oracle 的 MySQL:
$where = "WHERE ( ( database_name = ? AND table_name = ? )
OR ( database_name = LOWER(?) AND table_name = LOWER(?) ) )";
$sql = "SELECT last_update,
n_rows,
'Data & PK' AS 'Type',
clustered_index_size * 16384 AS Bytes,
ROUND(clustered_index_size * 16384 / n_rows) AS 'Bytes/row',
clustered_index_size AS Pages,
ROUND(n_rows / clustered_index_size) AS 'Rows/page'
FROM mysql.innodb_table_stats
$where
UNION
SELECT last_update,
n_rows,
'Secondary Indexes' AS 'BTrees',
sum_of_other_index_sizes * 16384 AS Bytes,
ROUND(sum_of_other_index_sizes * 16384 / n_rows) AS 'Bytes/row',
sum_of_other_index_sizes AS Pages,
ROUND(n_rows / sum_of_other_index_sizes) AS 'Rows/page'
FROM mysql.innodb_table_stats
$where
AND sum_of_other_index_sizes > 0
";
对于 Percona:
/* to enable stats:
percona < 5.5: set global userstat_running = 1;
5.5: set global userstat = 1; */
$sql = "SELECT i.INDEX_NAME as Index_Name,
IF(ROWS_READ IS NULL, 'Unused',
IF(ROWS_READ > 2e9, 'Overflow', ROWS_READ)) as Rows_Read
FROM (
SELECT DISTINCT TABLE_SCHEMA, TABLE_NAME, INDEX_NAME
FROM information_schema.STATISTICS
) i
LEFT JOIN information_schema.INDEX_STATISTICS s
ON i.TABLE_SCHEMA = s.TABLE_SCHEMA
AND i.TABLE_NAME = s.TABLE_NAME
AND i.INDEX_NAME = s.INDEX_NAME
WHERE i.TABLE_SCHEMA = ?
AND i.TABLE_NAME = ?
ORDER BY IF(i.INDEX_NAME = 'PRIMARY', 0, 1), i.INDEX_NAME";
(那些提供的不仅仅是深度。)
PRIMARY
指数据的 BTree。 "n_diff_pfx03" 之类的名称指的是 BTree 的第 3 层; table 的最大此类数字表示总深度。
行宽
关于估计一行的宽度,请参阅 Bill 的回答。这是另一种方法:
- 查找每列的大小(INT=4 字节,对 VAR 使用平均值)
- 求和。
- 乘以 2 到 3(以允许 InnoDB 的开销)
- 分成16K得到叶个节点的平均数。
非叶节点,加上索引叶节点,比较棘手,因为您需要准确理解在此类节点中什么代表了 "row"。
(因此,我的简单化“每个节点 100 行”。)
但是谁在乎呢?
这是另一种似乎很有效的简化方法。由于磁盘命中是查询中最大的性能项,因此需要"count the disk hits"作为判断查询性能的第一顺序。
但是看看 buffer_pool 中的块缓存。父节点最近被触摸的可能性是子节点的 100 倍。
所以,简化为"assume"所有非叶子节点都被缓存,所有叶子节点都需要从磁盘中获取。因此,depth 并不像接触了多少叶节点块那么重要。这降低了您的“35% 加速”——CPU 当然有 35% 的加速,但 I/O[=102 几乎没有加速=].而I/O是重要的组成部分。
请注意,如果您获取按时间顺序存储的 table 的最新 20 行,它们将在最后 1 个(或可能是 2 个)块中找到。如果它们由 UUID 存储,则更有可能达到 20 个块——更多的磁盘命中,因此速度更慢。
二级索引
PRIMARY KEY
与数据聚类。这意味着 PK 的外观需要向下钻取一个 BTree。但是二级索引是由第二个 BTree 实现的——向下钻取它以找到 PK,然后通过 PK 向下钻取。当"counting the disk hits"时,需要同时考虑两个BTrees。并考虑随机性(例如,对于 UUID)或不(按日期排序)。
写
- 找到块(可能缓存)
- 更新它
- 如有必要,处理分块
- 在 buffer_pool 中将块标记为 "dirty"
- 最终将其写回磁盘。
步骤 1 可能 涉及读取 I/O;第 5 步可能涉及写入 I/O -- 但您不会等待它完成。
索引更新
UNIQUE
索引必须在完成 INSERT
之前检查。这涉及潜在缓存读取 I/O.
对于非唯一索引,在 "Change buffer" 中创建一个条目。 (它存在于 buffer_pool 中。)最终它与磁盘上的适当块合并。也就是说,当 INSERTing
行时不等待 I/O(至少不等待更新非唯一索引)。
推论:UNIQUE
索引的成本更高。但是真的需要超过2个这样的索引(包括PK)吗?