寻求分页查询在大 table 上变得越来越慢

Seek paginated query gets progressively slower on a big table

我有一大堆 table 事件。 (目前有 530 万行)。我主要需要以线性方式从头到尾遍历此 table。大多数情况下没有随机搜索。目前的数据包括大约 5 天的这些事件。

由于 table 的大小,我需要对结果进行分页,互联网告诉我 "seek pagination" 是最好的方法。

不过,此方法在前 3 天的遍历效果很好且速度很快,此后 mysql 真正开始变慢。我发现它一定是 io-bound,因为我的 cpu 使用率实际上随着减速开始而下降。

我相信这与我所做的 2 列排序和文件排序的使用有关,也许 Mysql 需要读取所有行来对我的结果进行排序或其他。正确建立索引可能是一个正确的解决方法,但我一直无法找到解决我问题的索引。

这个数据库的复杂之处在于 ID 和时间戳的顺序并不完美。该软件要求数据按时间戳排序。但是在向这个数据库中添加数据时,有些事件是在实际发生后 1 分钟添加的,因此自动递增的 id 不是按时间顺序排列的。

截至目前,减速非常严重,以至于我的 5 天遍历从未完成。它只会越来越慢...

我试过以多种方式为 table 编制索引,但 mysql 似乎不想使用这些索引,并且 EXPLAIN 一直显示 "filesort"。不过,在 where 语句上使用了索引。

我目前使用的解决方法是首先进行完整的 table 遍历并将所有行 ID 和时间戳加载到内存中。我对软件 python 端的行进行排序,然后在我遍历时(仅通过 ID)从 mysql 以较小的块加载完整数据。这工作正常,但由于对相同数据的总共 2 次遍历,效率非常低。

table 的架构:

CREATE TABLE `events` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `server` varchar(45) DEFAULT NULL,
  `software` varchar(45) DEFAULT NULL,
  `timestamp` bigint(20) DEFAULT NULL,
  `data` text,
  `event_type` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index3` (`timestamp`,`server`,`software`,`id`),
  KEY `index_ts` (`timestamp`)
) ENGINE=InnoDB AUTO_INCREMENT=7410472 DEFAULT CHARSET=latin1;

查询(一个可能的行):

SELECT software,
       server,
       timestamp,
       id,
       event_type,
       data
FROM   events
WHERE  ( server = 'a58b'
         AND ( software IS NULL
                OR software IN ( 'ASD', 'WASD' ) ) )
       AND ( timestamp, id ) > ( 100, 100 )
       AND timestamp <= 200
ORDER  BY timestamp ASC,
          id ASC
LIMIT  100; 

查询基于https://blog.jooq.org/2013/10/26/faster-sql-paging-with-jooq-using-the-seek-method/(以及其他一些具有相同想法的帖子)。我相信它叫做 "seek pagination with seek predicate"。基本要点是我有一个开始时间戳和结束时间戳,我需要使用我指定的服务器上的软件获取所有事件,或者仅获取特定于服务器的事件(软件 = NULL)。奇怪的 ( )-stuff 是由于 python 根据给定的参数构造查询。如果它们可能会产生一些影响,我就让它们可见。

我排除了在宇宙热寂之前完成的遍历。

第一次改变

AND ( timestamp, id ) > ( 100, 100 )

AND (timestamp > 100 OR timestamp = 100 AND id > 100)

官方文档中建议的优化方式:Row Constructor Expression Optimization

现在引擎将能够使用 (timestamp) 上的索引。根据 serversoftware 列的基数,这可能已经足够快了。

(server, timestamp, id) 上的索引应该会进一步提高性能。

如果仍然不够快,我建议对

进行 UNION 优化
AND (software IS NULL OR software IN ('ASD', 'WASD'))

那就是:

(
    SELECT software, server, timestamp, id, event_type, data
    FROM events
    WHERE server = 'a58b'
      AND software IS NULL
      AND (timestamp > 100 OR timestamp = 100 AND id > 100)
      AND timestamp <= 200
    ORDER BY timestamp ASC, id ASC
    LIMIT 100
) UNION ALL (
    SELECT software, server, timestamp, id, event_type, data
    FROM events
    WHERE server = 'a58b'
      AND software = 'ASD'
      AND (timestamp > 100 OR timestamp = 100 AND id > 100)
      AND timestamp <= 200
    ORDER BY timestamp ASC, id ASC
    LIMIT 100
) UNION ALL (
    SELECT software, server, timestamp, id, event_type, data
    FROM events
    WHERE server = 'a58b'
      AND software = 'WASD'
      AND (timestamp > 100 OR timestamp = 100 AND id > 100)
      AND timestamp <= 200
    ORDER BY timestamp ASC, id ASC
    LIMIT 100
)
ORDER BY timestamp ASC, id ASC
LIMIT 100

您需要在 (server, software, timestamp, id) 上为此查询创建索引。

存在多种并发症。

快速修复是

INDEX(software, timestamp, id)   -- in this order

连同

    WHERE  server = 'a58b'
      AND  timestamp BETWEEN 100 AND 200
      AND ( software IS NULL
                OR software IN ( 'ASD', 'WASD' ) ) )
      AND ( timestamp, id ) > ( 100, 100 )
    ORDER  BY timestamp ASC,
              id ASC
    LIMIT  100; 

请注意,server 需要在索引中 第一个 ,而不是在您正在对 (timestamp) 进行范围操作之后。此外,我打破了 timestamp BETWEEN ... 以使优化器清楚 ORDER BY 的下一列可能会使用索引。

你说 "pagination",所以我假设你也有一个 OFFSET?把它加回去,这样我们就可以讨论其含义。我在 "remembering where you left off" 上的 blog 而不是使用 OFFSET 可能(或可能不)实用。