如何预测 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 的回答。这是另一种方法:

  1. 查找每列的大小(INT=4 字节,对 VAR 使用平均值)
  2. 求和。
  3. 乘以 2 到 3(以允许 InnoDB 的开销)
  4. 分成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)或不(按日期排序)。

  1. 找到块(可能缓存)
  2. 更新它
  3. 如有必要,处理分块
  4. 在 buffer_pool
  5. 中将块标记为 "dirty"
  6. 最终将其写回磁盘。

步骤 1 可能 涉及读取 I/O;第 5 步可能涉及写入 I/O -- 但您不会等待它完成。

索引更新

UNIQUE 索引必须在完成 INSERT 之前检查。这涉及潜在缓存读取 I/O.

对于非唯一索引,在 "Change buffer" 中创建一个条目。 (它存在于 buffer_pool 中。)最终它与磁盘上的适当块合并。也就是说,当 INSERTing 行时不等待 I/O(至少不等待更新非唯一索引)。

推论:UNIQUE 索引的成本更高。但是真的需要超过2个这样的索引(包括PK)吗?