【问题标题】:MySQL select distinct optimizationMySQL select distinct 优化
【发布时间】:2016-03-21 20:20:26
【问题描述】:

假设我在 MySQL 中有下表:

CREATE TABLE `events` (
  `pv_name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
  `time_stamp` bigint(20) unsigned NOT NULL,
  `event_type` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
  `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
  `value_type` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `value_count` bigint(20) DEFAULT NULL,
  `alarm_status` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `alarm_severity` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`pv_name`,`time_stamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED;

有什么方法可以通过索引或其他方式改进以下查询?

SELECT DISTINCT events.pv_name
FROM events
WHERE events.time_stamp > t0_in AND events.time_stamp < t1_in
AND (events.value IS NULL OR events.alarm_severity = 'INVALID');

t0_int1_in 是传递给定义查询的存储过程的参数。

使用 EXPLAIN 运行查询给出:

+----+-------------+--------+-------+---------------+---------+---------+------+----------+-------------+
| id | select_type | table  | type  | possible_keys | key     | key_len | ref  | rows     | Extra       |
+----+-------------+--------+-------+---------------+---------+---------+------+----------+-------------+
|  1 | SIMPLE      | events | index | PRIMARY       | PRIMARY | 250     | NULL | 12724016 | Using where |
+----+-------------+--------+-------+---------------+---------+---------+------+----------+-------------+

在数据库上运行查询在 1 分 50.93 秒内返回 102620 行。

更新

为简单起见,假设表格如下:

CREATE TABLE `events` (
  `pv_name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
  `time_stamp` bigint(20) unsigned NOT NULL,
  `value_valid` tinyint(1) NOT NULL,
  PRIMARY KEY (`pv_name`,`time_stamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED;

是否可以添加适当的索引,以便以下或等效查询使用松散的索引扫描优化?

SELECT DISTINCT events.pv_name
FROM events
WHERE events.time_stamp > t0_in AND events.time_stamp < t1_in
AND events.value_valid = 0);

更新

如果我在time_stamp 上添加索引,我会得到:

mysql> EXPLAIN SELECT DISTINCT events.pv_name FROM events WHERE events.time_stamp > 0 AND events.time_stamp < 11426224880000000000 AND (events.value IS NULL OR events.alarm_severity = 'INVALID');
+----+-------------+--------+-------+--------------------+---------+---------+------+----------+-------------+
| id | select_type | table  | type  | possible_keys      | key     | key_len | ref  | rows     | Extra       |
+----+-------------+--------+-------+--------------------+---------+---------+------+----------+-------------+
|  1 | SIMPLE      | events | index | PRIMARY,time_stamp | PRIMARY | 250     | NULL | 13261211 | Using where |
+----+-------------+--------+-------+--------------------+---------+---------+------+----------+-------------+

在数据库上运行此查询在 30.44 秒内返回 11511 行。

mysql> EXPLAIN SELECT DISTINCT events.pv_name FROM events FORCE INDEX (time_stamp) WHERE events.time_stamp > 0 AND events.time_stamp < 11426224880000000000 AND (events.value IS NULL OR events.alarm_severity = 'INVALID');
+----+-------------+--------+-------+--------------------+------------+---------+------+---------+-----------------------------------------------------+
| id | select_type | table  | type  | possible_keys      | key        | key_len | ref  | rows    | Extra                                               |
+----+-------------+--------+-------+--------------------+------------+---------+------+---------+-----------------------------------------------------+
|  1 | SIMPLE      | events | range | PRIMARY,time_stamp | time_stamp | 8       | NULL | 6630605 | Using index condition; Using where; Using temporary |
+----+-------------+--------+-------+--------------------+------------+---------+------+---------+-----------------------------------------------------+

在数据库上运行此查询在 2 分 20.41 秒内返回 11511 行。

更新

根据我已将表格更改为的建议:

CREATE TABLE `events` (
  `pv_name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
  `time_stamp` bigint(20) unsigned NOT NULL,
  `event_type` enum('add','init','update','disconnect','remove') COLLATE utf8mb4_unicode_ci NOT NULL,
  `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
  `value_type` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `value_count` bigint(20) DEFAULT NULL,
  `alarm_status` enum('NO_ALARM','READ','WRITE','HIHI','HIGH','LOLO','LOW','STATE','COS','COMM','TIMEOUT','HWLIMIT','CALC','SCAN','LINK','SOFT','BAD_SUB','UDF','DISABLE','SIMM','READ_ACCESS','WRITE_ACCESS') COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `alarm_severity` enum('NO_ALARM','MINOR','MAJOR','INVALID') COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`pv_name`,`time_stamp`),
  KEY `event_type` (`event_type`,`time_stamp`),
  KEY `alarm_severity` (`alarm_severity`,`time_stamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED;

查询到:

SELECT DISTINCT events.pv_name
FROM events
WHERE events.time_stamp > 0 AND events.time_stamp < 1426224880000000000
AND alarm_severity = 'INVALID'
UNION
SELECT DISTINCT events.pv_name
FROM events
WHERE events.time_stamp > 0 AND events.time_stamp < 1426224880000000000
AND event_type = 'add'
UNION
SELECT DISTINCT events.pv_name
FROM events
WHERE events.time_stamp > 0 AND events.time_stamp < 1426224880000000000
AND event_type = 'disconnect'
UNION
SELECT DISTINCT events.pv_name
FROM events
WHERE events.time_stamp > 0 AND events.time_stamp < 1426224880000000000
AND event_type = 'remove';

对查询运行解释给出:

+----+--------------+----------------+-------+-----------------------------------+----------------+---------+------+--------+-------------------------------------------+
| id | select_type  | table          | type  | possible_keys                     | key            | key_len | ref  | rows   | Extra                                     |
+----+--------------+----------------+-------+-----------------------------------+----------------+---------+------+--------+-------------------------------------------+
|  1 | PRIMARY      | events         | range | PRIMARY,event_type,alarm_severity | alarm_severity | 10      | NULL | 101670 | Using where; Using index; Using temporary |
|  2 | UNION        | events         | range | PRIMARY,event_type,alarm_severity | event_type     | 9       | NULL | 994652 | Using where; Using index; Using temporary |
|  3 | UNION        | events         | range | PRIMARY,event_type,alarm_severity | event_type     | 9       | NULL |  73660 | Using where; Using index; Using temporary |
|  4 | UNION        | events         | range | PRIMARY,event_type,alarm_severity | event_type     | 9       | NULL | 136348 | Using where; Using index; Using temporary |
| NULL | UNION RESULT | <union1,2,3,4> | ALL   | NULL                              | NULL           | NULL    | NULL |   NULL | Using temporary                           |
+----+--------------+----------------+-------+-----------------------------------+----------------+---------+------+--------+-------------------------------------------+

在数据库上运行查询在 1 分 2.45 秒内返回 112620 行。

【问题讨论】:

  • 你能提供一个带有一点数据的sqlfiddle吗?
  • 整张桌子有多大?
  • 该表目前大约有 12,000,000 行,并将稳步增长。
  • @Loufylouf:我不太熟悉 sqlfiddle。表中没有大量行是否具有代表性?
  • 这将比尝试手动执行此操作要好,并且解释仍然可以工作,因此它不会那么重要,但仍然有用。

标签: mysql query-optimization distinct


【解决方案1】:

没有太多关于您的数据的数据,这不会很具体,但我希望您仍然会发现它有用。

索引和内存

为了保持最佳性能,您应该始终确保您的索引可以放入您的 RAM。情况可能经常如此,但是当表开始达到数百万行时,就值得一看。您可以在 SO question 上找到很多关于如何操作的信息。它为什么如此重要 ?好吧,我不知道它在内部是如何工作的,但是索引很有可能会存储在硬盘上,这将是非常棒的。或者它也可以刷新索引的第一部分,然后将剩余的部分加载到 RAM 等中。无论如何,它会很长,如果你可以简单地避免它(通过增加引擎可以使用的 RAM),那么就这样做。

分区

您已经使用了主键,这是一件好事,但您也可以使用分区。这个想法非常简单,而不是将其存储在单个表中,它会自动等效于仅包含一些值范围的子表(它比这更复杂,但我们现在说值范围) .使用 SELECT、UPDATE 或 DELETE 时,这一切对您来说都是透明的,因此您的请求不涉及重构。我建议看看这个非常简洁的演示文稿about partitions。该文档在这方面也非常棒。例如,您将看到可以使用不同大小的分区。例如,如果您根据时间戳进行分区,并且您知道最近的数据比旧数据更频繁地被访问,您可以在过去 7 天创建 7 个分区,然后在前 4 周创建 4 个分区,然后再创建 12 个过去 12 个月的分区等。但这需要您进行一些分析。

更好的键

对于前一点,也因为它更干净,我强烈建议您将时间戳的bigint 类型更改为@Huy Nguyen 建议的真实日期/时间mysql 类型。作为尾注,他关于alarm_statusalarm_severity 的评论很好,如果这只能取一组定义的值,你应该切换到 int 类型,这样可以让你在键和分区中更有效地使用它们.

更新

关于您的更新,我并不精通松散索引扫描优化,但在 value_valid, time_stamp 上添加一个键似乎可以减少使用的行数(来自解释命令)并且系统地选择了键(而不是已经定义的主键)。我的数据集相当少,因此值得尝试您的数据。在我的示例查询中,仅使用您定义的主键来谈论数字:key_len: 250, rows:242,使用我的附加键:key_len:9, rows:106

【讨论】:

  • 谢谢。 time_stamp 以纳秒为单位编码 GPS 时间,所以我认为它必须是一个 bigint。我可以将event_typevalue_typealarm_statusalarm_severity 更改为枚举类型。我确实计划以您描述的方式添加分区。我有点希望有某种方法可以制定查询以使用松散的索引扫描优化,但也许这是不可能的?
  • 我正在更改其他内容,所以我不确定,但似乎将“innodb 缓冲池大小”增加到 8 GB 有助于将查询时间缩短到 18 秒左右。跨度>
  • 太好了,几乎好一个数量级。关于你的时间戳,你真的需要纳秒精度吗? MySQL 似乎能够将日期/时间值存储到microsecond,如果你不经常使用它,你总是可以将纳秒部分存储在一个单独的列中。
  • 不幸的是,我确实需要纳秒级精度。我正在记录其他时间可能相同的事件。
【解决方案2】:

你应该在

上添加一个索引
events.time_stamp 

并且在

上的索引也可能很有用
events.alarm_severity

【讨论】:

    【解决方案3】:

    同时添加

    包含(events.pv_name)

    到索引,所以它不做表扫描

    【讨论】:

    • 对不起,我不确定我是否理解。 events.pv_name 已经在主键中。
    • 这不是 MySQL 的特性。
    【解决方案4】:

    一些可能的提示,按理论改进的顺序排列:

    1. 尝试 MYSQL 在 SELECT 之前锁定表,然后 UNLOCK 表。我相信锁定表确实可以加快速度,因为它不会 必须担心表选择时的更新,因此 可以更有效地抓取数据。

      我认为在 BEGIN/COMMIT 事务序列中使用它在某些情况下可能有助于加快速度,但通常使用 INSERT/UPDATES 而不是 SELECTS。

    2. 创建这些索引也可能会有所帮助:time_stamp、value、 警报严重性。

    3. 如果可能,请将 alarm_severity 从 varchar(40) 更改为 char(40)。 CHAR 比 VARCHAR 搜索速度更快,但占用更多空间。 或将 alarm_severity 更改为整数而不是字符串,以便 可以更快地被索引。或者添加一个附加字段 整数对应物,如alarm_severity_code,会更快 索引和搜索。

    4. 您为 alarm_severity 创建的索引可以限制为仅 10 字符左右。我相信这将使搜索更快 (取决于您的数据集),但仍允许最多 40 个字符 场地。如果这些值类似于'INVALID',那么 10 应该是好的 足以索引。

    5. 也许添加一个可索引的“has_value”字段,而不是搜索 对于值为 NULL 的值,因为值不可索引。这需要 在您添加/编辑记录时分配一个值。

    6. time_stamp 真的需要是 big_int 吗?可能更多 仅使用时间戳数据类型就很有效。

    7. 是否必须为 ROW_FORMAT=COMPRESSED?听起来会 在查询数据时放慢速度以解压缩数据。

    所以建议的表结构可能是这样的:

    CREATE TABLE IF NOT EXISTS `events` (
      `pv_name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
      `time_stamp` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
      `event_type` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
      `has_value` int(11) NOT NULL DEFAULT '0',
      `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
      `value_type` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
      `value_count` bigint(20) DEFAULT NULL,
      `alarm_status` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
      `alarm_severity` char(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
      PRIMARY KEY (`pv_name`,`time_stamp`),
      KEY `time_stamp` (`time_stamp`),
      KEY `alarm_severity` (`alarm_severity`(10)),
      KEY `has_value` (`has_value`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED;
    

    磁盘空间对速度的影响确实很大。您还可以将数据集分解为单独的表,一个用于特定值或特定警报严重性,因此每个查询都可以在一个较小的表上。

    【讨论】:

    • 谢谢。 time_stamp 以纳秒为单位编码 GPS 时间,所以我认为它必须是一个 bigint。我可能可以将event_typevalue_typealarm_statusalarm_severity 更改为枚举类型。我正在考虑添加一个布尔值 is_valid 列进行搜索,而不是检查 valuealarm_severity 列。我一直在尝试制定查询和索引以使用松散的索引扫描优化,但也许这是不可能的?
    • 抱歉,我不得不对此投反对票。有太多的错误信息。我已经在我的回答的反驳部分回复了他们中的大多数。
    【解决方案5】:

    性能提升

    “索引扫描”是针对PRIMARY,所以它实际上是一个表扫描,这是可能的最慢的方式。

    你需要

    INDEX(time_stamp)
    

    PRIMARY KEY(pv_name, time_stamp) 没有用处,因为 前导 字段 (pv_name) 对 WHEREGROUP BYORDER BY 没有帮助。

    警告:如果切换到新索引失败,可能需要在 SP 中使用“prepare”。

    alarm_severity 上的索引将无济于事,因为它隐藏在 OR 中。

    可以交换PRIMARY KEY 中的字段顺序,但这可能会损害其他查询,并且执行ALTER 需要很长时间。

    Cookbook on creating indexes.

    更好的改进(除了它不起作用)

    由于ORWHERE这部分无法优化:

    AND (events.value IS NULL OR
         events.alarm_severity = 'INVALID')
    

    有一个希望:把OR变成UNION

          ( SELECT  DISTINCT events.pv_name
                FROM  events
                WHERE  events.time_stamp > t0_in
                  AND  events.time_stamp < t1_in
                  AND  events.value IS NULL 
          )
        UNION  DISTINCT 
          ( SELECT  DISTINCT events.pv_name
                FROM  events
                WHERE  events.time_stamp > t0_in
                  AND  events.time_stamp < t1_in
                  AND  events.alarm_severity = 'INVALID' 
          );
    

    并添加

    INDEX(alarm_severity, time_stamp) -- in that order
    INDEX(value , time_stamp) -- in that order
    

    但是 - 这是一个很大的 BUT - 因为valueTEXT,所以这是行不通的。如果value 可以更改为VARCHAR(191),那么它会起作用。更好的是ENUM。 (不,“前缀索引”不够聪明。)

    反驳

    是的,索引应该适合 RAM。但通常你别无选择。

    PARTITIONing很少有用。我不认为它是有益的在这种情况下

    我大概可以将 event_type、value_type、alarm_status 和 alarm_severity 更改为枚举类型。

    去做吧!假设这是一个非常大的表,这将大大缩小表的大小,从而使其更快——尤其是如果它现在是 I/O 绑定的。

    PARTITIONs 大小不同——这很好,但是当您需要将 4 周转换为 1 个月(或其他任何时间)时,就会出现“问题”。它有效地阻止了合并期间的活动。而且,由于其他(性能)原因需要不超过大约 50 个分区,因此汇总最终将是“必要的”。

    innodb_buffer_pool_size 应设置为可用 RAM 的大约 70%。这是最重要的可调参数。

    纳秒——查看数据;我怀疑你有重复。当然,这应该足够精确,但是提供时钟的算法是什么?这可能是允许重复。 (我不太担心它的 8 个字节。)

    对于 InnoDB,在适合事务完整性的情况下使用 BEGIN...COMMIT。不要使用LOCK TABLES

    valuealarm_severity 上的单个索引对于此查询没有用处。 (但time_stamp 很有用。)

    “将 varchar(40) 更改为 char(40)”——不!几乎没有CHAR 更好的情况。而不是在这种情况下。

    KEY alarm_severity (alarm_severity(10)) -- 前缀索引几乎没有用处。特别是当它是VARCHAR 并且值通常很短时。

    【讨论】:

    • 我已更新问题以报告向time_stamp 添加索引。不幸的是,它似乎运行速度较慢。由于它正在寻找 pv_name 的不同值,我猜应该使用 pv_name 上的索引?
    • 索引用于过滤和/或排序,而不是用于值。
    • 由于它变慢了,我怀疑优化器没有做明显的事情并使用新的索引。从简单地执行SELECT 更改为使用CONCAT 构造SELECT 并缝合t0_int1_in 值。然后使用prepare。或者,您可以将USE INDEX(time_stamp) 添加到SELECT
    • 我使用了force index,解释说它正在使用它。
    • 啊。它仍在扫描半张桌子。 (也就是说,time_stamp 并不是一个过滤器。)因此,使用索引不是有效的。但是,如果 PK开始 带有 time_stamp,它会有点用处。 带有两个新索引的UNION 会有所帮助。 (同样,基数可能会妨碍您提供很多帮助。)您期望有多少行?
    【解决方案6】:

    加快对大型表的大型查询的另一种方法是构建和维护“汇总表”。

    假设您通常希望查看“小时”(而不是几天或几个月等)。此查询(以及许多其他查询)的汇总表类似于

    CREATE TABLE foo (
        hr MEDIUMINT UNSIGNED NOT NULL,  -- derived from time_stamp; see below
        alarm_severity ...  -- preferably an ENUM, not VARCHAR
        event_type ...
        pv_name ...
        ct INT UNSIGNED -- if you want to know how many
        PRIMARY KEY(hr, alarm_severity, event_type)
    ) ENGINE=InnoDB;
    

    每个小时结束后:

    INSERT INTO foo
        SELECT FLOOR(time_stamp / 3600e9),
               alarm_severity, event_type, pv_name,
               COUNT(*)
            FROM events
            WHERE time_stamp >= ...  -- start of previous hour
              AND time_stamp < ...   -- and end
            GROUP BY 1,2,3,4;
    

    那么原来的查询就变成了

    SELECT  DISTINCT pv_name
        FROM  foo
        WHERE  hr >= t0_in / 3600e9
          AND  hr <  t1_in / 3600e9
          AND ( alarm_severity = 'INVALID'
           OR   event_type IN ('add', 'disconnect', 'remove')
              );
    

    最后的SELECT 将很容易不到 1 秒。但它要求数据一旦插入就不能改变等等。

    您之前拥有AND value IS NULL。这可以添加到INSERT..SELECT 中,或者您可能需要value_is_null 作为foo 及其PK 中的真/假标志。

    More on Summary tables.

    【讨论】:

      猜你喜欢
      • 2016-08-14
      • 1970-01-01
      • 1970-01-01
      • 2020-05-28
      • 1970-01-01
      • 2011-11-16
      • 2015-01-28
      • 2012-09-26
      相关资源
      最近更新 更多