【问题标题】:MySql composite indexMySql 复合索引
【发布时间】:2015-02-20 15:10:12
【问题描述】:

我们使用 MySql 作为我们的数据库

以下查询在 mysql 表(大约 2500 万条记录)上运行。我在这里粘贴了两个查询。查询运行速度太慢,我想知道更好的复合索引是否可以改善这种情况。

你知道什么是最好的复合索引吗?

并建议我这些查询是否需要复合索引

第一个查询

    EXPLAIN SELECT log_type,
       count(DISTINCT subscriber_id) AS distinct_count,
       count(*) as total_count
FROM stats.campaign_logs
WHERE domain = 'xxx'
  AND campaign_id='12345'
  AND log_type IN ('EMAIL_SENT', 'EMAIL_CLICKED', 'EMAIL_OPENED', 'UNSUBSCRIBED')
  AND log_time BETWEEN CONVERT_TZ('2015-02-12 00:00:00','+05:30','+00:00')
                   AND CONVERT_TZ('2015-02-19 23:59:58','+05:30','+00:00')
GROUP BY log_type

上述查询的解释

+----+-------------+---------------+-------------+--------------------------------------------------------------+--------------------------------+---------+------+-------+------------------------------------------------------------------------------+
| id | select_type | table         | type        | possible_keys                                                | key                            | key_len | ref  | rows  | Extra                                                                        |
+----+-------------+---------------+-------------+--------------------------------------------------------------+--------------------------------+---------+------+-------+------------------------------------------------------------------------------+
|  1 | SIMPLE      | campaign_logs | index_merge | campaign_id_index,domain_index,log_type_index,log_time_index | campaign_id_index,domain_index | 153,153 | NULL | 35683 | Using intersect(campaign_id_index,domain_index); Using where; Using filesort |
+----+-------------+---------------+-------------+--------------------------------------------------------------+--------------------------------+---------+------+-------+------------------------------------------------------------------------------+

第二个查询

SELECT campaign_id
     , subscriber_id
     , campaign_name
     , log_time
     , log_type
     , message
     , UNIX_TIMESTAMP(log_time) AS time 
  FROM campaign_logs 
 WHERE domain = 'xxx'  
   AND log_type = 'EMAIL_OPENED'  
 ORDER  
    BY log_time DESC 
 LIMIT 20;

上述查询的解释

+----+-------------+---------------+-------------+-----------------------------+-----------------------------+---------+------+--------+---------------------------------------------------------------------------+
| id | select_type | table         | type        | possible_keys               | key                         | key_len | ref  | rows   | Extra                                                                     |
+----+-------------+---------------+-------------+-----------------------------+-----------------------------+---------+------+--------+---------------------------------------------------------------------------+
|  1 | SIMPLE      | campaign_logs | index_merge | domain_index,log_type_index | domain_index,log_type_index | 153,153 | NULL | 118392 | Using intersect(domain_index,log_type_index); Using where; Using filesort |
+----+-------------+---------------+-------------+-----------------------------+-----------------------------+---------+------+--------+---------------------------------------------------------------------------+

第三个查询

EXPLAIN SELECT *, UNIX_TIMESTAMP(log_time) AS time FROM stats.campaign_logs WHERE domain = 'xxx' AND log_type <> 'EMAIL_SLEEP' AND  subscriber_id = '123' ORDER BY log_time DESC LIMIT 100

上述查询的解释

+----+-------------+---------------+------+-------------------------------------------------+---------------------+---------+-------+------+-----------------------------+
| id | select_type | table         | type | possible_keys                                   | key                 | key_len | ref   | rows | Extra                       |
+----+-------------+---------------+------+-------------------------------------------------+---------------------+---------+-------+------+-----------------------------+
|  1 | SIMPLE      | campaign_logs | ref  | subscriber_id_index,domain_index,log_type_index | subscriber_id_index | 153     | const |   35 | Using where; Using filesort |
+----+-------------+---------------+------+-------------------------------------------------+---------------------+---------+-------+------+-----------------------------+

如果您需要我可以在此处提供的任何其他详细信息

更新(2016/4/22): 现在我们想在现有表中再添加一列,即节点 ID。一个活动可以有多个节点。无论我们在活动中生成什么报告,我们现在也需要关于单个节点的那些报告。

例如

SELECT log_type,
           count(DISTINCT subscriber_id) AS distinct_count,
           count(*) as total_count
    FROM stats.campaign_logs
    WHERE domain = 'xxx',
      AND campaign_id='12345',
      AND node_id = '34567',
      AND log_type IN ('EMAIL_SENT', 'EMAIL_CLICKED', 'EMAIL_OPENED', 'UNSUBSCRIBED')
      AND log_time BETWEEN CONVERT_TZ('2015-02-12 00:00:00','+05:30','+00:00')
                       AND CONVERT_TZ('2015-02-19 23:59:58','+05:30','+00:00')
    GROUP BY log_type

CREATE TABLE `camp_logs` (
  `domain` varchar(50) DEFAULT NULL,
  `campaign_id` varchar(50) DEFAULT NULL,
  `subscriber_id` varchar(50) DEFAULT NULL,
  `message` varchar(21000) DEFAULT NULL,
  `log_time` datetime DEFAULT NULL,
  `log_type` varchar(50) DEFAULT NULL,
  `level` varchar(50) DEFAULT NULL,
  `campaign_name` varchar(500) DEFAULT NULL,
  KEY `subscriber_id_index` (`subscriber_id`),
  KEY `log_type_index` (`log_type`),
  KEY `log_time_index` (`log_time`),
  KEY `campid_domain_logtype_logtime_subid_index` (`campaign_id`,`domain`,`log_type`,`log_time`,`subscriber_id`),
  KEY `domain_logtype_logtime_index` (`domain`,`log_type`,`log_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |

尺寸问题。

由于我们有两个复合索引,索引文件快速增加。以下是表格当前的统计数据。 数据大小:30 GB 索引大小:35 GB

对于关于 node_id 的报告,我们想要更新我们现有的复合索引

来自

KEY `campid_domain_logtype_logtime_subid_index` (`campaign_id`,`domain`,`log_type`,`log_time`,`subscriber_id`),

到

KEY `campid_domain_logtype_logtime_subid_nodeid_index` (`campaign_id`,`domain`,`log_type`,`log_time`,`subscriber_id`,`node_id`)

您能否为活动和节点级别报告推荐合适的复合索引。

谢谢

【问题讨论】:

  • DATE('2015-02-12 00:00:00') AND DATE('2015-02-19 23:59:58') 和'2015-02-12 00:00:00' AND '2015-02-19 23:59:58' 的功能区别是什么?
  • 尝试简化您的查询。第一个在内部查询和外部查询中都有GROUP BY。没有索引可以帮助你。

标签: mysql sql


【解决方案1】:

这是您的第一个查询:

SELECT A.log_type, count(*) as distinct_count, sum(A.total_count) as total_count
from (SELECT log_type, count(subscriber_id) as total_count
      FROM stats.campaign_logs
      WHERE domain = 'xxx' AND campaign_id = '12345' AND
            log_type IN ('EMAIL_SENT', 'EMAIL_CLICKED', 'EMAIL_OPENED', 'UNSUBSCRIBED') AND
             DATE(CONVERT_TZ(log_time,'+00:00','+05:30')) BETWEEN DATE('2015-02-12 00:00:00') AND DATE('2015-02-19 23:59:58')
      GROUP BY subscriber_id,log_type) A
GROUP BY A.log_type;

最好写成:

      SELECT log_type, count(DISTINCT subscriber_id) as total_count
      FROM stats.campaign_logs
      WHERE domain = 'xxx' AND campaign_id = '12345' AND
            log_type IN ('EMAIL_SENT', 'EMAIL_CLICKED', 'EMAIL_OPENED', 'UNSUBSCRIBED') AND
             DATE(CONVERT_TZ(log_time, '+00:00', '+05:30')) BETWEEN DATE('2015-02-12 00:00:00') AND DATE('2015-02-19 23:59:58')
      GROUP BY log_type;

这方面的最佳索引可能是:campaign_logs(domain, campaign_id, log_type, log_time, subscriber_id)。这是查询的覆盖索引。前三个键应该用于where 过滤。

【讨论】:

  • 您的重写丢失了 distinct_count,可能是COUNT(DISTINCT subscriber_id, log_type)。覆盖指数看起来不错。
  • @RickJames。 . .原始查询计算每种日志类型的不同订阅者数量。这就是嵌套 group by 在这种情况下的工作原理。
  • @Gordon Linoff 感谢您的评论,我更新了我的第一个查询并粘贴了一个查询,总共 3 个查询,您能否建议所有查询的最佳复合索引组合
【解决方案2】:

重写第一个查询如下:

SELECT log_type,
       count(DISTINCT subscriber_id) AS distinct_count,
       count(*) as total_count
FROM stats.campaign_logs
WHERE domain = 'xxx'
  AND campaign_id='12345'
  AND log_type IN ('EMAIL_SENT', 'EMAIL_CLICKED', 'EMAIL_OPENED', 'UNSUBSCRIBED')
  AND DATE(CONVERT_TZ(log_time,'+00:00','+05:30'))
      BETWEEN DATE('2015-02-12 00:00:00') AND DATE('2015-02-19 23:59:58')
GROUP BY log_type

它应该产生相同的结果,但它没有内部查询和单个GROUP BY。

该表已经拥有它需要的所有索引。

最后一个条件 (DATE(...)) 不能使用任何索引,因为它必须使用 log_time 为每一行计算一个值。重写它以将log_time 的裸值与一些计算值进行比较(将CONVERT_TZ() 应用于执行反向转换的区间边界)。

这样,它使用索引的全部功能将索引列log_time 与一些常量值进行比较:

  AND log_time BETWEEN CONVERT_TZ('2015-02-12 00:00:00','+05:30','+00:00')
                   AND CONVERT_TZ('2015-02-19 23:59:58','+05:30','+00:00')

domain 和 log_type 列上的多列索引(按此顺序)有助于加快这两个查询(它们在 Extra 返回的 Extra 列中都有 Using intersect(domain_index,log_type_index))。

如果您创建这样的索引,则删除索引domain_index。列domain 和log_type(按此顺序)上的索引也可以仅用作domain 上的索引。 MySQL 可以使用它来代替domain_index。拥有两个相同的索引会使写入操作变慢并且没有任何好处。

【讨论】:

  • 感谢您的评论,我更新了我的第一个查询并粘贴了一个查询,总共 3 个查询,您能否建议所有查询的最佳组合索引组合。以上第三个查询很快。
【解决方案3】:

对于查询 1,@Gordon Linoff 的索引非常好(至少在重写 SELECT 之后):

INDEX(domain, campaign_id, log_type, log_time, subscriber_id)
INDEX(campaign_id, domain, log_type, log_time, subscriber_id) -- equally good.

对于查询 2:“index_merge”表示您可能会从“复合索引”中受益。第二个查询最好由以下任一方法处理,(我认为)它将仅用 EXPLAIN 估计的 20 次读取而不是 118K 来计算结果集。

INDEX(domain, log_type, log_time)
INDEX(log_type, domain, log_time)

请记住,当您添加索引时,您应该删除多余的索引。例如INDEX(domain, ...) 使KEY domain_index (domain) 变得多余,因此可以删除后者。

总的来说,我会推荐

DROP INDEX(campaign_id_index),
ADD INDEX(campaign_id, domain, log_type, log_time, subscriber_id),
DROP INDEX(domain),
ADD INDEX(domain, log_type, log_time)
PRIMARY KEY(id, log_time) -- if you also add PARTITIONing; see below

其他建议:

  • InnoDB 必须有一个主键。 (为您提供了一个6字节隐藏的。)推荐ADD COLUMN id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY。

  • 考虑将 log_type 从庞大的 VARCHAR 更改为 ENUM。

  • 如果subscriber_id 确实是一个数字,则考虑INT UNSIGNED。
  • 您最终是否需要清除“旧”记录? PARTITION BY RANGE(TO_DAYS(log_time)) 可能是最好的方法。见http://mysql.rjweb.org/doc.php/partitionmaint。 (请注意,PK 需要是 (id, log_time)。)
  • “分区修剪”不会发生,因为 log_time 隐藏在一对函数中。使用 @axiac 的措辞。
  • innodb_buffer_pool_size 应设置为可用 RAM 的 70% 左右。

【讨论】:

  • 如果他只运行 1 个查询,那些 DROP 索引将是有意义的。问题是谁只运行一个查询或 2 个查询?
  • @Mihai ,关键是INDEX(domain, &lt;other cols&gt;) 可以处理所有将受益于INDEX(domain) 的WHERE 子句。因此,几乎没有理由同时拥有两个索引。
  • @Rick 感谢您的评论,我更新了我的第一个查询并粘贴了一个查询,总共 3 个查询,您能否建议所有查询的最佳复合索引组合
【解决方案4】:

对于 3 查询,我看到很少共享索引。相反,我会为这些索引投票(假设您添加了id ... AUTO_INCREMENT)

PRIMARY KEY(id)
INDEX(campaign_id, domain, log_time)
INDEX(subscriber_id, domain)
INDEX(domain, log_type, log_time)
INDEX(log_time)

您仍应考虑其余建议(ENUM、INT 等)。这些将缩小数据和索引的磁盘占用空间。更小 -> 更多可缓存 -> 更少 I/O -> 更快。​​

INDEX(log_time) 不一定会在任何查询中使用,但我保留了它,以防优化器决定以ORDER BY 而不是WHERE 为目标。我没有足够的信息来预测;此外,我怀疑优化器可能一次选择一个索引,另一次选择另一个索引。

这 3 个“复合”索引实际上可以按任意顺序包含前两列。我选择将它们混合在一起,以便第一列在它们之间有所不同,从而可能对查询 #4 有所帮助。

这个答案更像是“艺术”而不是“科学”;我认为它和它会得到的一样好。

【讨论】:

    猜你喜欢
    • 2020-06-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多