MySQL选择不同的优化

时间:2016-03-21 20:20:26

标签: mysql query-optimization distinct

假设我在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分钟内返回112620行2.45秒

6 个答案:

答案 0 :(得分:1)

如果您的数据没有太多数据,这不是非常具体,但我希望您仍然会觉得它很有用。

索引和RAM

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

分区

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

更好的按键

对于前一点并且因为它更清洁,我强烈建议将时间戳的bigint类型更改为真实的日期/时间mysql类型,如@Huy Nguyen建议的那样。作为结束语,他关于alarm_statusalarm_severity的评论很好,如果这只能取一组定义的值,你应该切换到int类型,这样可以更有效地使用它们在密钥和分区中。

更新

关于您的更新,我并不是非常精通宽松的索引扫描优化,但在value_valid, time_stamp上添加密钥似乎减少了使用的行数(来自explain命令)并且系统地选择的密钥(而不是已定义的主键)。我的数据集非常少,因此值得尝试一下您的数据。要使用您已定义的主键来对数字进行通话,我会在示例查询中key_len: 250, rows:242,使用我的附加键:key_len:9, rows:106

答案 1 :(得分:0)

您应该在

上添加索引
events.time_stamp 

也可能对

上的索引有用
events.alarm_severity

答案 2 :(得分:0)

同时添加

  

INCLUDE(events.pv_name)

到索引所以它不会进行表扫描

答案 3 :(得分:0)

一些可能的提示,按理论上的改进:

  1. 尝试MYSQL在SELECT之前锁定表,然后解锁 表。我相信锁定表会加速它,因为它没有 必须担心在选择时对表的更新,以及为此 可以更有效地获取数据。

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

  2. 也可以帮助制作这些索引:time_stamp,value, alarm_severity。

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

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

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

  6. time_stamp真的需要成为big_int吗?它可能更多 高效只是为了使用时间戳数据类型。

  7. 是否必须是ROW_FORMAT = COMPRESSED?这听起来像是 将其减慢以在查询数据时解压缩数据。

  8. 因此建议的表结构可能如下:

    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;
    

    它确实降低了磁盘空间与速度的关系。您还可以将数据集分解为单独的表,一个用于特定值或某个alarm_severity,因此每个查询都可以在较小的表上。

答案 4 :(得分:0)

绩效改进

&#34;索引扫描&#34;反对PRIMARY,所以它实际上是一个表扫描,这是最慢的方式。

你需要

INDEX(time_stamp)

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

警告:如果无法切换到新索引,您可能需要使用&#34;准备&#34;在SP。

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

可以交换PRIMARY KEY中字段的顺序,但这可能会影响其他查询,而ALTER会花费很长时间。< / p>

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

但是这是一个很大但是没有成功,因为valueTEXT。如果value可以更改为VARCHAR(191),那么它就可以了。更好的是ENUM。 (不,&#34;前缀索引&#34;不够聪明。)

<强>辩驳

是的,索引应该适合RAM。但通常你没有选择权。

PARTITIONing 很少有用。在这种情况下,我不认为

  

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

做吧!假设这是一个非常大的表,那会大大缩小表的大小,使其更快 - 特别是如果它现在受到I / O限制。

不同大小的

PARTITIONs - 这很好,但是有一个&#34;问题&#34;当你需要4个星期到1个月(或其他)。它有效地阻止了整合期间的活动。并且,由于其他(性能)原因需要不超过大约50个分区,因此最终将需要卷起来。

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

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

对于InnoDB,请在适当的情况下使用BEGIN ... COMMIT进行交易完整性。请勿使用LOCK TABLES

valuealarm_severity上的各个索引对此查询无效。 (但time_stamp很有用。)

&#34;将varchar(40)更改为char(40)&#34; - 不!几乎没有CHAR更好的情况。而不是在这种情况下。

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

答案 5 :(得分:0)

加速大型表上的大型查询的另一种方法是构建和维护一个&#34; Summary表&#34;。

让我们说你通常想看看&#34;小时&#34; (而不是几天或几个月等)。此查询的摘要表(以及许多其他查询)类似于

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

相关问题