如何提高 200 多万条记录的查询性能

How can I improve query performance for 200+ million records

背景

我有一个 MySQL 测试环境,其中 table 包含超过 2 亿行。在此 table 上必须执行两种类型的查询;

  1. 某些行是否存在。 给定一个 client_id 和一个 sgtin 的列表,最多可以容纳 50.000 项,我需要知道 table.
  2. 中存在哪些 sgtin
  3. 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 sgtins。 温度table

CREATE TEMPORARY TABLE sgtins_tmp_table (`sgtin` varchar(255) primary key)
 engine=MEMORY;

存在查询

我使用此查询来查明 sgtin 是否存在。这也是我发现的最快的查询。对于 50K sgtins,此查询将花费 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,它们有直方图)