如何提高 200 多万条记录的查询性能
How can I improve query performance for 200+ million records
背景
我有一个 MySQL 测试环境,其中 table 包含超过 2 亿行。在此 table 上必须执行两种类型的查询;
- 某些行是否存在。
给定一个
client_id
和一个 sgtin
的列表,最多可以容纳
50.000 项,我需要知道 table. 中存在哪些 sgtin
- Select 那些行。
给定一个
client_id
和一个 sgtin
的列表,最多可以容纳
50.000 个项目,我需要获取整行。 (商店、GTIN...)
单个 'client_id' 的 table 可以增长到 200+ 百万条记录。
测试环境
至强 E3-1545M / 32GB 内存 / 固态硬盘。 InnoDB 缓冲池 24GB。
(生产将是具有 192GB RAM 的更大服务器)
Table
CREATE TABLE `sgtins` (
`client_id` INT UNSIGNED NOT NULL,
`sgtin` varchar(255) NOT NULL,
`store` varchar(255) NOT NULL,
`gtin` varchar(255) NOT NULL,
`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX (`client_id`, `store`, `sgtin`),
INDEX (`client_id`),
PRIMARY KEY (`client_id`,`sgtin`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
测试
首先,我生成了分布在 10 'client_id' 秒内的随机 sgtin 值,以用 2 亿行填充 table。
我创建了一个基准工具来执行我尝试过的各种查询。我还使用解释计划找出最佳性能。对于每次测试,该工具将从我用来填充数据库的数据中读取新的随机数据。确保每个查询都是不同的。
为此 post 我将使用 28 sgtin
s。
温度table
CREATE TEMPORARY TABLE sgtins_tmp_table (`sgtin` varchar(255) primary key)
engine=MEMORY;
存在查询
我使用此查询来查明 sgtin
是否存在。这也是我发现的最快的查询。对于 50K sgtin
s,此查询将花费 3 到 9 秒。
-- cost = 17 for 28 sgtins loaded in the temp table.
SELECT sgtin
FROM sgtins_tmp_table
WHERE EXISTS
(SELECT sgtin FROM sgtins
WHERE sgtins.client_id = 4
AND sgtins.sgtin = sgtins_tmp_table.sgtin);
Select 查询
-- cost = 50.60 for 28 sgtins loaded in the temp table. 50K not usable.
SELECT sgtins.sgtin, sgtins.store, sgtins.timestamp
FROM sgtins_tmp_table, sgtins
WHERE sgtins.client_id = 4
AND sgtins_tmp_table.sgtin = sgtins.sgtin;
-- cost = 64 for 28 sgtins loaded in the temp table.
SELECT sgtins.sgtin, sgtins.store, sgtins.timestamp
FROM sgtins
WHERE sgtins.client_id = 4
AND sgtins.sgtin IN ( SELECT sgtins_tmp_table.sgtin
FROM sgtins_tmp_table);
-- cost = 50.60 for 28 sgtins loaded in the temp table.
SELECT sgtins_tmp_table.epc, sgtins.store
FROM sgtins_tmp_table, sgtins
WHERE exists (SELECT organization_id, sgtin FROM sgtins WHERE client_id = 4 AND sgtins.sgtin = sgtins_tmp_table.sgtin)
AND sgtins.client_id = 4
AND sgtins_tmp_table.sgtin = sgtins.sgtin;
总结
现有查询可用,但选择速度较慢。我该怎么办?欢迎任何建议:)
我建议重写你的 EXISTS
SQL 因为相关的子查询往往在大多数时候优化得很差。
建议的查询将改为使用 INNER JOIN
。
SELECT filter.sgtin
FROM (SELECT '<value>' AS sgtin UNION ALL SELECT '<value>' ..) AS filter
INNER JOIN sgtins ON filter.sgtin = sgtins.sgtin WHERE sgtins.client_id = 4
这很可能比使用临时 table 更快。
但是您正在处理 50K 个值,因此我可以直接从临时 table 生成具有动态 SQL 的所需派生 table SQL。
也像我在聊天中建议的那样。
根据数据选择性,制作索引 (sgtins, client_id)
很可能更有意义,这并不是很清楚。
因为该索引可能会使您的相关子查询更快。
查询
# Maybe also needed to be changed with 50 K
# SET SESSION max_allowed_packet = ??;
# needed for GROUP_CONCAT as if defualts to only 1024
SET SESSION group_concat_max_len = @@max_allowed_packet;
SET @UNION_SQL = NULL;
SELECT
CONCAT(
'SELECT '
, GROUP_CONCAT(
CONCAT("'", sgtins_tmp_table.sgtin,"'", ' AS sgtin')
SEPARATOR ' UNION ALL SELECT '
)
)
FROM
sgtins_tmp_table
INTO
@UNION_SQL;
SET @SQL = CONCAT("
SELECT filter.sgtin
FROM (",@UNION_SQL,") AS filter
INNER JOIN sgtins ON filter.sgtin = sgtins.sgtin WHERE sgtins.client_id = 4
");
PREPARE q FROM @SQL;
EXECUTE q;
见demo
因评论而编辑
更理想的方法是使用固定的 table 索引并使用 CONNECTION_ID()
分隔搜索值。
CREATE TABLE sgtins_filter (
connection_id INT
, sgtin varchar(255) NOT NULL
, INDEX(connection_id, sgtin)
);
然后您可以简单地在 table 和
之间加入
SELECT sgtins_filter.sgtin
FROM sgtins_filter
INNER JOIN sgtins
ON
sgtins_filter.sgtin = sgtins.sgtin
AND
sgtins_filter.connection_id = CONNECTION_ID()
AND
sgtins.client_id = 4;
见demo
我会这样写你的 exists
查询:
SELECT stt.sgtin
FROM sgtins_tmp_table stt
WHERE EXISTS (SELECT 1
FROM sgtins s
WHERE s.client_id = 4 AND
s.sgtin = stt.sgtin
);
对于此查询,您需要 sgtins(sgtin, client_id)
上的索引。
假设 200M 行并且每个客户端不超过 50K 个 sgtin,那么必须有超过 4K 个客户端?
只对 10 个客户进行基准测试是有风险的。在某些情况下,优化器会在使用索引和进行 table 扫描之间切换;可能就是这种情况。
所以,请说明最终目标;我不想建议你如何使基准测试 运行 更快,只是让 'real' 案例不符合建议。
另外,stgins 列表是静态的吗?您通过建议 pre-building a MEMORY
table 暗示了这一点。但这似乎并不常见。也许 'real' 案例每次都会得到一组不同的 sgtins。
那么,我来回答这个问题:
- 200M 行
- Table 大于 24GB
- innodb_buffer_pool_size = 24G
- 数以千计的不同
client_id
值。 (只有 10 个,优化器试图忽略索引并进行 table 扫描。)
- 每个 client_id
有数千个 stgin
值
(client_id, stgin)
对是独一无二的
- 每个查询可能有不同的stgins列表;也就是说,不能假定来自 运行 运行
的相同列表
- 想要优化
SELECT stgin FROM t WHERE client_id = 1234 AND stgin IN (..long list..)
- 想要优化
SELECT * FROM t WHERE client_id = 1234 AND stgin IN (..long list..)
不管EXPLAIN
提供的数字是多少,以下是两个查询的最优解:
WHERE client_id = 1234 AND stgin IN (..long list..)`
PRIMARY KEY(client_id, stgin) -- in this order.
为什么?
- 优化器很乐意关注
client_id = constant
并跳过 stgins 列表。
- 通过
client_id
第一个,SELECT
的所有activity将集中在一小部分table。这很重要,因为它将要触摸的块数限制为小于 buffer_pool_size.
- 从技术上讲,独立的
INDEX(client_id, stgin)
对于 SELECT stgin...
会更快,但我不推荐它,因为它太多余了,不会节省太多性能。
成本分析评论:
- 它不考虑块是否被缓存。对于 HDD 驱动器,这可以产生巨大的 (10x) 差异。
- 它没有过多考虑索引与数据,也没有考虑索引 + 数据(如 non-covering 二级索引)
- 它对值的分布一无所知。 (除非使用 MariaDB 或 MySQL 8.0,它们有直方图)
背景
我有一个 MySQL 测试环境,其中 table 包含超过 2 亿行。在此 table 上必须执行两种类型的查询;
- 某些行是否存在。
给定一个
client_id
和一个sgtin
的列表,最多可以容纳 50.000 项,我需要知道 table. 中存在哪些 - Select 那些行。
给定一个
client_id
和一个sgtin
的列表,最多可以容纳 50.000 个项目,我需要获取整行。 (商店、GTIN...)
sgtin
单个 'client_id' 的 table 可以增长到 200+ 百万条记录。
测试环境
至强 E3-1545M / 32GB 内存 / 固态硬盘。 InnoDB 缓冲池 24GB。 (生产将是具有 192GB RAM 的更大服务器)
Table
CREATE TABLE `sgtins` (
`client_id` INT UNSIGNED NOT NULL,
`sgtin` varchar(255) NOT NULL,
`store` varchar(255) NOT NULL,
`gtin` varchar(255) NOT NULL,
`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX (`client_id`, `store`, `sgtin`),
INDEX (`client_id`),
PRIMARY KEY (`client_id`,`sgtin`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
测试
首先,我生成了分布在 10 'client_id' 秒内的随机 sgtin 值,以用 2 亿行填充 table。
我创建了一个基准工具来执行我尝试过的各种查询。我还使用解释计划找出最佳性能。对于每次测试,该工具将从我用来填充数据库的数据中读取新的随机数据。确保每个查询都是不同的。
为此 post 我将使用 28 sgtin
s。
温度table
CREATE TEMPORARY TABLE sgtins_tmp_table (`sgtin` varchar(255) primary key)
engine=MEMORY;
存在查询
我使用此查询来查明 sgtin
是否存在。这也是我发现的最快的查询。对于 50K sgtin
s,此查询将花费 3 到 9 秒。
-- cost = 17 for 28 sgtins loaded in the temp table.
SELECT sgtin
FROM sgtins_tmp_table
WHERE EXISTS
(SELECT sgtin FROM sgtins
WHERE sgtins.client_id = 4
AND sgtins.sgtin = sgtins_tmp_table.sgtin);
Select 查询
-- cost = 50.60 for 28 sgtins loaded in the temp table. 50K not usable.
SELECT sgtins.sgtin, sgtins.store, sgtins.timestamp
FROM sgtins_tmp_table, sgtins
WHERE sgtins.client_id = 4
AND sgtins_tmp_table.sgtin = sgtins.sgtin;
-- cost = 64 for 28 sgtins loaded in the temp table.
SELECT sgtins.sgtin, sgtins.store, sgtins.timestamp
FROM sgtins
WHERE sgtins.client_id = 4
AND sgtins.sgtin IN ( SELECT sgtins_tmp_table.sgtin
FROM sgtins_tmp_table);
-- cost = 50.60 for 28 sgtins loaded in the temp table.
SELECT sgtins_tmp_table.epc, sgtins.store
FROM sgtins_tmp_table, sgtins
WHERE exists (SELECT organization_id, sgtin FROM sgtins WHERE client_id = 4 AND sgtins.sgtin = sgtins_tmp_table.sgtin)
AND sgtins.client_id = 4
AND sgtins_tmp_table.sgtin = sgtins.sgtin;
总结
现有查询可用,但选择速度较慢。我该怎么办?欢迎任何建议:)
我建议重写你的 EXISTS
SQL 因为相关的子查询往往在大多数时候优化得很差。
建议的查询将改为使用 INNER JOIN
。
SELECT filter.sgtin
FROM (SELECT '<value>' AS sgtin UNION ALL SELECT '<value>' ..) AS filter
INNER JOIN sgtins ON filter.sgtin = sgtins.sgtin WHERE sgtins.client_id = 4
这很可能比使用临时 table 更快。
但是您正在处理 50K 个值,因此我可以直接从临时 table 生成具有动态 SQL 的所需派生 table SQL。
也像我在聊天中建议的那样。
根据数据选择性,制作索引 (sgtins, client_id)
很可能更有意义,这并不是很清楚。
因为该索引可能会使您的相关子查询更快。
查询
# Maybe also needed to be changed with 50 K
# SET SESSION max_allowed_packet = ??;
# needed for GROUP_CONCAT as if defualts to only 1024
SET SESSION group_concat_max_len = @@max_allowed_packet;
SET @UNION_SQL = NULL;
SELECT
CONCAT(
'SELECT '
, GROUP_CONCAT(
CONCAT("'", sgtins_tmp_table.sgtin,"'", ' AS sgtin')
SEPARATOR ' UNION ALL SELECT '
)
)
FROM
sgtins_tmp_table
INTO
@UNION_SQL;
SET @SQL = CONCAT("
SELECT filter.sgtin
FROM (",@UNION_SQL,") AS filter
INNER JOIN sgtins ON filter.sgtin = sgtins.sgtin WHERE sgtins.client_id = 4
");
PREPARE q FROM @SQL;
EXECUTE q;
见demo
因评论而编辑
更理想的方法是使用固定的 table 索引并使用 CONNECTION_ID()
分隔搜索值。
CREATE TABLE sgtins_filter (
connection_id INT
, sgtin varchar(255) NOT NULL
, INDEX(connection_id, sgtin)
);
然后您可以简单地在 table 和
之间加入SELECT sgtins_filter.sgtin
FROM sgtins_filter
INNER JOIN sgtins
ON
sgtins_filter.sgtin = sgtins.sgtin
AND
sgtins_filter.connection_id = CONNECTION_ID()
AND
sgtins.client_id = 4;
见demo
我会这样写你的 exists
查询:
SELECT stt.sgtin
FROM sgtins_tmp_table stt
WHERE EXISTS (SELECT 1
FROM sgtins s
WHERE s.client_id = 4 AND
s.sgtin = stt.sgtin
);
对于此查询,您需要 sgtins(sgtin, client_id)
上的索引。
假设 200M 行并且每个客户端不超过 50K 个 sgtin,那么必须有超过 4K 个客户端?
只对 10 个客户进行基准测试是有风险的。在某些情况下,优化器会在使用索引和进行 table 扫描之间切换;可能就是这种情况。
所以,请说明最终目标;我不想建议你如何使基准测试 运行 更快,只是让 'real' 案例不符合建议。
另外,stgins 列表是静态的吗?您通过建议 pre-building a MEMORY
table 暗示了这一点。但这似乎并不常见。也许 'real' 案例每次都会得到一组不同的 sgtins。
那么,我来回答这个问题:
- 200M 行
- Table 大于 24GB
- innodb_buffer_pool_size = 24G
- 数以千计的不同
client_id
值。 (只有 10 个,优化器试图忽略索引并进行 table 扫描。) - 每个 client_id 有数千个
(client_id, stgin)
对是独一无二的- 每个查询可能有不同的stgins列表;也就是说,不能假定来自 运行 运行 的相同列表
- 想要优化
SELECT stgin FROM t WHERE client_id = 1234 AND stgin IN (..long list..)
- 想要优化
SELECT * FROM t WHERE client_id = 1234 AND stgin IN (..long list..)
stgin
值
不管EXPLAIN
提供的数字是多少,以下是两个查询的最优解:
WHERE client_id = 1234 AND stgin IN (..long list..)`
PRIMARY KEY(client_id, stgin) -- in this order.
为什么?
- 优化器很乐意关注
client_id = constant
并跳过 stgins 列表。 - 通过
client_id
第一个,SELECT
的所有activity将集中在一小部分table。这很重要,因为它将要触摸的块数限制为小于 buffer_pool_size. - 从技术上讲,独立的
INDEX(client_id, stgin)
对于SELECT stgin...
会更快,但我不推荐它,因为它太多余了,不会节省太多性能。
成本分析评论:
- 它不考虑块是否被缓存。对于 HDD 驱动器,这可以产生巨大的 (10x) 差异。
- 它没有过多考虑索引与数据,也没有考虑索引 + 数据(如 non-covering 二级索引)
- 它对值的分布一无所知。 (除非使用 MariaDB 或 MySQL 8.0,它们有直方图)