1

我在运行 MySQL 5.0.77 的应用程序中有一个快速增长的大型日志表。我正在尝试找到根据消息类型优化在过去 X 天内对实例进行计数的查询的最佳方法:

CREATE TABLE `counters` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `kind` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `created_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_counters_on_kind` (`kind`),
  KEY `index_counters_on_created_at` (`created_at`)
) ENGINE=InnoDB AUTO_INCREMENT=302 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

对于这个测试集,表中有 668521 行。我要优化的查询是:

SELECT kind, COUNT(id) FROM counters WHERE created_at >= ? GROUP BY kind;

目前,该查询需要 3-5 秒,估计如下:

+----+-------------+----------+-------+----------------------------------+------------------------+---------+------+---------+-------------+
| id | select_type | table    | type  | possible_keys                    | key                    | key_len | ref  | rows    | Extra       |
+----+-------------+----------+-------+----------------------------------+------------------------+---------+------+---------+-------------+
|  1 | SIMPLE      | counters | index | index_counters_on_created_at_idx | index_counters_on_kind | 258     | NULL | 1185531 | Using where | 
+----+-------------+----------+-------+----------------------------------+------------------------+---------+------+---------+-------------+
1 row in set (0.00 sec)

删除 created_at 索引后,它看起来像这样:

+----+-------------+----------+-------+---------------+------------------------+---------+------+---------+-------------+
| id | select_type | table    | type  | possible_keys | key                    | key_len | ref  | rows    | Extra       |
+----+-------------+----------+-------+---------------+------------------------+---------+------+---------+-------------+
|  1 | SIMPLE      | counters | index | NULL          | index_counters_on_kind | 258     | NULL | 1185531 | Using where | 
+----+-------------+----------+-------+---------------+------------------------+---------+------+---------+-------------+
1 row in set (0.00 sec)

(是的,由于某种原因,行估计大于表中的行数。)

因此,显然,该索引没有意义。

真的没有更好的方法来做到这一点吗?我尝试将该列作为时间戳,但结果却变慢了。

编辑:我发现将查询更改为使用间隔而不是特定日期最终会使用索引,将行估计减少到上述查询的 20% 左右:

SELECT kind, COUNT(id) FROM counters WHERE created_at >= 
    (NOW() - INTERVAL 7 DAY) GROUP BY kind;

我不完全确定为什么会发生这种情况,但我相当有信心,如果我理解了它,那么这个问题通常会更有意义。

4

2 回答 2

0

在阅读了有关该问题的最新编辑后,问题似乎是该WHERE子句中使用的参数被 MySQL 解释为字符串而不是datetime值。这可以解释为什么index_counters_on_created_at优化器没有选择索引,而是会导致扫描将created_at值转换为字符串表示,然后进行比较。我认为,这可以通过datetimewhere子句中显式转换来防止:

where `created_at` >= convert({specific_date}, datetime)

我原来的评论仍然适用于优化部分。

这里真正的性能杀手是kind列。因为在执行GROUP BY数据库引擎时,首先需要确定kind列中的所有不同值,这会导致表或索引扫描。这就是为什么估计的行数大于表中的总行数的原因,在一次传递中它将确定kind列中的不同值,在第二次传递中它将确定哪些行满足create_at >= ?条件。更糟糕的kind是,该列varchar (255)太大而无法高效,再加上它使用utf8字符集和utf8_unicode_ci排序规则,这增加了确定该列中唯一值所需的比较的复杂性。

如果您将kind列的类型更改为int. 因为整数比较比 unicode 字符比较更高效、更简单。为存储和kind的消息创建一个目录表也会有所帮助。然后对种类目录表的连接和首先按日期过滤的日志表的子查询进行分组:kind_iddescription

select k.kind_id, count(*)
from
    kind_catalog k
    inner join (
        select kind_id
        from counters
        where create_at >= ?
    ) c on k.kind_id = c.kind_id
group by k.kind_id

这将首先过滤counters表,create_at >= ?并可以从该列的索引中受益。然后它将它加入到kind_catalog表中,如果 SQL 优化器很好,它将扫描较小的kind_catalog表以进行分组,而不是counters表。

于 2011-10-19T22:51:18.390 回答
0

为什么不使用级联索引?

CREATE INDEX idx_counters_created_kind ON counters(created_at, kind);

应该进行仅索引扫描(在 Extras 中提到“使用索引”,因为 COUNT(ID) 无论如何都不是 NULL)。

参考:

于 2011-10-20T07:08:31.647 回答