【问题标题】:How to optimize datetime comparisons in mysql in where clause如何在where子句中优化mysql中的日期时间比较
【发布时间】:2022-01-09 08:50:51
【问题描述】:

上下文

我有一张大桌子,里面装满了由外部来源更新的“文档”。当我注意到更新比我上一个接触点更新时,我需要处理这些文档。不过我遇到了一些严重的性能问题。

示例代码

select count(*) from documents;

在 1 分 15.24 秒内返回 212,494,397 个文档。

select count(*) from documents where COALESCE( updated_at > last_indexed_at, TRUE);

实际查询在 14 分 36.23 秒内获得了 55,988,860 个。

select count(*) from documents where COALESCE( updated_at > last_indexed_at, TRUE) limit 1;

同样需要大约 15 分钟。 (这让我很惊讶)

问题

如何执行updated_at > last_indexed_at 更多 合理的时间?

详情

我很确定我的查询在某种程度上是不可搜索的。不幸的是,我找不到这个查询阻止它在行独立的基础上执行的原因。

select count(*) 
from documents 
where last_indexed_at is null or updated_at > last_indexed_at; 

并没有做得更好。

也没有

select count( distinct( id ) ) 
from documents 
where last_indexed_at is null or updated_at > last_indexed_at limit 1;

也没有

select count( distinct( id ) ) 
from documents limit 1;

编辑:跟进请求的数据

这个问题只涉及rails项目中的一个表(谢天谢地),所以我们很方便地为表定义了rails。

/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `documents` (
  `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `document_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `document_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `locale` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `allowed_ids` text COLLATE utf8mb4_unicode_ci NOT NULL,
  `fields` mediumtext COLLATE utf8mb4_unicode_ci,
  `created_at` datetime(6) NOT NULL,
  `updated_at` datetime(6) NOT NULL,
  `last_indexed_at` datetime(6) DEFAULT NULL,
  `deleted_at` datetime(6) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_documents_on_document_type` (`document_type`),
  KEY `index_documents_on_locale` (`locale`),
  KEY `index_documents_on_last_indexed_at` (`last_indexed_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

SELECT VERSION(); 找我5.7.27-30-log

而且可能是最重要的,

explain select count(*) from documents where COALESCE( updated_at > last_indexed_at, TRUE);

完全了解我

+----+-------------+-----------+------------+------+---------------+------+---------+------+-----------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows      | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+-----------+----------+-------------+
|  1 | SIMPLE      | documents | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 208793754 |   100.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+-----------+----------+-------------+

【问题讨论】:

  • explain select count(*) from documents; 显示什么?实际上每个解释说明了什么?在问题中也发布show create table documents
  • 你可以运行EXPLAIN select count(*) from documents USE INDEX (PRIMARY) 看看会发生什么
  • edit你的问题告诉我们你的MySql版本。另外,请read this 和 [edit????] 您的问题告诉我们有关您的表和查询计划的更多信息。
  • 还有什么我可以补充的吗?

标签: mysql sql optimization query-optimization


【解决方案1】:

添加一个覆盖 INDEX

如果您有 INDEX(last_indexed_at, updated_at),则 15 分钟的查询可能会运行得更快一些。 (列的顺序无关紧要在这种情况下。)

假设这两列都是表中的列。如果是这样,那么查询必须读取每一行。 (我不知道“sargable”这个词是否涵盖了这种情况。)

我建议的INDEX 会更快,因为它是“覆盖”的。通过只读取索引,可以减少 I/O。

重复 15 分钟可能是因为innodb_buffer_pool_size 不够大,无法容纳整张桌子。所以,它是 I/O 绑定的。我的INDEX 会更小,因此(希望)足够小以适合缓冲池。所以,第二次运行会更快,甚至更快。

慢或

OR 通常是一个可怕的减速。但我认为这并不重要。

如果您要将 last_indexed_at 初始化为某个旧日期(例如“2000-01-01”),则可以去掉 COALESCEOR

另一种清理方法是

SELECT  SUM(last_indexed_at IS NULL) +
        SUM(updated_at > last_indexed_at) AS "Need indexing"
    FROM t;

我仍然需要索引。 SUM(boolean expression) 将表达式视为 0(false 或 NULL)或 1(TRUE)。

同时,我认为COUNT(DISTINCT id)COUNT(*) 没有任何不同。并且SUMs 这对也应该给你价值。

再次,我依赖于“覆盖”作为诀窍。

“超过..”技巧

在某些情况下,您并不需要确切的数字,尤其是当它“超过某个阈值”时。

SELECT 1 FROM tbl WHERE ... LIMIT 1000,1;

如果返回“1”,则至少有 1000 行。如果返回为空(没有返回行),则不是。

这仍然需要触及多达 1000 行(希望在索引中),但这比触及一百万行要好。

【讨论】:

    【解决方案2】:

    如果您使用的是最新的 MySQL 版本(5.7+),您可以在包含搜索表达式的表中添加 generated column,然后为该列建立索引。

    ALTER TABLE t 
     ADD COLUMN needs_indexing TINYINT 
      GENERATED ALWAYS AS 
         (CASE WHEN last_indexed_at IS NULL THEN 1
               WHEN updated_at > last_indexed_at THEN 1
               ELSE 0 END) VIRTUAL;
    ALTER TABLE t 
      ADD INDEX needs_indexing (needs_indexing);
    

    这会将驱动器空间用于索引,但不在您的表中。

    然后您可以通过SELECT SUM(needs_indexing) FROM t 获取符合您条件的项目数。

    但是:您不必计算所有项目就知道您需要重新索引某些项目。正如您所发现的,在大型 InnoDB 表上执行 COUNT(*) 非常昂贵。你可以这样做:

    SELECT EXISTS (SELECT 1 FROM t WHERE needs_indexing = 1) something_needs_indexing;
    

    你会很快从这个查询中得到 1 或 0。 1 表示您至少有一行符合您的条件。

    当然,您的索引工作也可以做到

    SELECT * FROM t WHERE needs_indexing=1 LIMIT 1;
    

    或任何有意义的东西。这也会很快。

    【讨论】:

      【解决方案3】:

      哦! MySQL 5.7 引入了Generated Columns——它为我们提供了一种索引表达式的方法! ?

      如果你这样做:

      ALTER TABLE documents
        ADD COLUMN dirty BOOL GENERATED ALWAYS AS (COALESCE(updated_at > last_indexed_at, TRUE)) STORED,
        ADD INDEX index_documents_on_dirty(dirty);
      

      ...并将查询更改为:

      SELECT COUNT(*) FROM documents WHERE dirty;
      

      ...你得到什么结果?

      希望我们将评估COALESCE(updated_at > last_indexed_at, TRUE) 的工作从读取时间转移到写入时间。

      【讨论】:

        猜你喜欢
        • 2021-10-09
        • 2017-09-16
        • 1970-01-01
        • 1970-01-01
        • 2012-01-02
        • 1970-01-01
        • 2017-11-22
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多