具有多个分组或排序的mysql查询优化
Posted
技术标签:
【中文标题】具有多个分组或排序的mysql查询优化【英文标题】:mysql query optimization with multiple groupings or order by 【发布时间】:2013-09-13 09:39:13 【问题描述】:更新:表和索引定义
desc activities;x
+----------------+--------------+------+-----+---------+
| Field | Type | Null | Key | Default |
+----------------+--------------+------+-----+---------+
| id | int(11) | NO | PRI | NULL |
| trackable_id | int(11) | YES | MUL | NULL |
| trackable_type | varchar(255) | YES | | NULL |
| owner_id | int(11) | YES | MUL | NULL |
| owner_type | varchar(255) | YES | | NULL |
| key | varchar(255) | YES | | NULL |
| parameters | text | YES | | NULL |
| recipient_id | int(11) | YES | MUL | NULL |
| recipient_type | varchar(255) | YES | | NULL |
| created_at | datetime | NO | | NULL |
| updated_at | datetime | NO | | NULL |
+----------------+--------------+------+-----+---------+
show indexes from activities;
+------------+------------+-----------------------------------------------------+--------------+----------------+-----------+-------------+----------+--------+------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type |
+------------+------------+-----------------------------------------------------+--------------+----------------+-----------+-------------+----------+--------+------+------------+
| activities | 0 | PRIMARY | 1 | id | A | 7263 | NULL | NULL | | BTREE |
| activities | 1 | index_activities_on_trackable_id_and_trackable_type | 1 | trackable_id | A | 7263 | NULL | NULL | YES | BTREE |
| activities | 1 | index_activities_on_trackable_id_and_trackable_type | 2 | trackable_type | A | 7263 | NULL | NULL | YES | BTREE |
| activities | 1 | index_activities_on_owner_id_and_owner_type | 1 | owner_id | A | 7263 | NULL | NULL | YES | BTREE |
| activities | 1 | index_activities_on_owner_id_and_owner_type | 2 | owner_type | A | 7263 | NULL | NULL | YES | BTREE |
| activities | 1 | index_activities_on_recipient_id_and_recipient_type | 1 | recipient_id | A | 2421 | NULL | NULL | YES | BTREE |
| activities | 1 | index_activities_on_recipient_id_and_recipient_type | 2 | recipient_type | A | 3631 | NULL | NULL | YES | BTREE |
+------------+------------+-----------------------------------------------------+--------------+----------------+-----------+-------------+----------+--------+------+------------+
select count(id) from activities;
+-----------+
| count(id) |
+-----------+
| 7117 |
+-----------+
这是我当前查询的样子:
SELECT act.*, group_concat(act.owner_id order by act.created_at desc) as owner_ids
FROM (select * from activities order by created_at desc) as act
INNER JOIN users on users.id = act.owner_id
WHERE (users.city_id = 1 and act.owner_type = 'User')
GROUP BY trackable_type, recipient_id, recipient_type
order by act.created_at desc
limit 20 offset 0;
解释一下
我经常使用这个查询,包括索引等。有什么方法可以优化这个查询吗?
【问题讨论】:
我认为在选择整个数据时没有使用(select * from activities order by created_at desc)
subselect,而是直接按表名加入
我需要在 group_concat 之前使用那个子查询,否则结果不是我想要的顺序。
不,你没有。这就是为什么您在 group_concat 函数中有另一个 order by。
如果您希望我们帮助优化查询,您需要向我们展示表和索引定义,以及每个表的行数。也许您的表格定义不佳。也许索引没有正确创建。也许您认为您在该列上没有索引。没有看到表和索引定义,我们无法判断。我们还需要行计数,因为这会极大地影响查询优化。如果您知道如何处理EXPLAIN
或获取执行计划,请将结果也放入问题中。
首先,请阅读mysql Extensions to GROUP BY
:选择“隐藏”列而不进行聚合的能力是 MySQL 特有的“功能”,会导致不确定的结果(在其他 RDBMS 中完全无效)。也许您的意思是SELECT trackable_type, recipient_id, recipient_type, GROUP_BY(owner_id ORDER BY created_at DESC) ...
,这更有意义?但如果是这样,很难看出您打算最外层的 ORDER BY
完成什么(同样,created_at
是一个“隐藏”列,会导致不确定的结果)。
【参考方案1】:
MySQL 有时工作起来很奇怪,所以我会试一试。我假设 ID 是用户表上的主键。
SELECT
act.trackable_type, act.recipient_id, act.recipient_type,
max(act.created_at) as max_created_at,
group_concat(act.owner_id order by act.created_at DESC) as owner_ids
FROM activities act
WHERE act.owner_id in (select id from users where city_id = 1)
AND act.owner_Type = 'User'
GROUP BY trackable_type, recipient_id, recipient_type
ORDER BY max_created_at
LIMIT 20
【讨论】:
这似乎是所有提供的解决方案中最快的。谢谢:) 不错!很高兴能帮上忙。【参考方案2】:我认为你根本不需要offset 0
,看起来你也可以不用子查询。如果不使用users
表中的字段,可以使用in
(或exists
)明确:
select
a.trackable_type, a.recipient_id, a.recipient_type,
max(a.created_at) as max_created_at,
group_concat(a.owner_id order by a.created_at desc) as owner_ids
from activities as a
where
a.owner_type = 'User' and
a.owner_id in (select u.id from users as u where u.city_id = 1)
group by a.trackable_type, a.recipient_id, a.recipient_type
order by max_created_at desc
limit 20;
对我来说,如果您在activities
上的owner_type, owner_id
上创建索引(您的索引owner_id, owner_type
不适用于您的查询)并在@987654330 上的city_id
上创建索引,那么对我来说,您的查询肯定会获得性能提升@。
【讨论】:
@HassanJaveed 这样您就可以在查询中添加偏移量。不知道你为什么接受 nimdil 的回答,一般是我的回答,但一天后添加了【参考方案3】:首先我会开始使查询更具可读性:-)
您不需要带有 ORDER BY 的派生表,而是使用列列表而不是 ACT。*。
SELECT ACT.TRACKABLE_TYPE, ACT.RECIPIENT_ID, ACT.RECIPIENT_TYPE, MAX(ACT.CREATED_AT) AS max_created,
GROUP_CONCAT(ACT.OWNER_ID ORDER BY ACT.CREATED_AT DESC) AS OWNER_IDS
FROM ACTIVITIES AS ACT
JOIN USERS ON USERS.ID = ACT.OWNER_ID
WHERE (USERS.CITY_ID = 1 AND ACT.OWNER_TYPE = 'USER')
GROUP BY ACT.TRACKABLE_TYPE, ACT.RECIPIENT_ID, ACT.RECIPIENT_TYPE
ORDER BY max_created DESC
LIMIT 20 OFFSET 0;
将用户的 WHERE 条件移动到派生表中可能会有所帮助:
SELECT ACT.TRACKABLE_TYPE, ACT.RECIPIENT_ID, ACT.RECIPIENT_TYPE, MAX(ACT.CREATED_AT) AS max_created,
GROUP_CONCAT(ACT.OWNER_ID ORDER BY ACT.CREATED_AT DESC) AS OWNER_IDS
FROM ACTIVITIES AS ACT
JOIN (SELECT ID FROM USERS WHERE CITY_ID = 1) USERS
ON USERS.ID = ACT.OWNER_ID
WHERE ACT.OWNER_TYPE = 'USER'
GROUP BY ACT.TRACKABLE_TYPE, ACT.RECIPIENT_ID, ACT.RECIPIENT_TYPE
ORDER BY max_created DESC
LIMIT 20 OFFSET 0;
【讨论】:
我要去这两个,然后回复你:) 谢谢【参考方案4】:您能否告诉我们您的 users 表的大小,例如以下查询的结果:
select count(id) from users WHERE users.city_id = 1;
如果这是一个小数字,我建议使用
SELECT act.trackable_type, act.recipient_id, act.recipient_type, max(act.created_at) as max_created_at,
group_concat(act.owner_id order by act.created_at DESC) as owner_ids
FROM activities act
WHERE act.owner_id in (select id from users where city_id = 1)
AND act.owner_Type = 'User'
GROUP BY trackable_type, recipient_id, recipient_type
ORDER BY max_created_at
LIMIT 20
否则,使用join会更好
SELECT ACT.TRACKABLE_TYPE, ACT.RECIPIENT_ID, ACT.RECIPIENT_TYPE, MAX(ACT.CREATED_AT) AS max_created_at,
GROUP_CONCAT(ACT.OWNER_ID ORDER BY ACT.CREATED_AT DESC) AS OWNER_IDS
FROM ACTIVITIES ACT
JOIN USERS ON (USERS.CITY_ID = 1 AND USERS.ID = ACT.OWNER_ID)
WHERE ACT.OWNER_TYPE = 'USER'
GROUP BY ACT.TRACKABLE_TYPE, ACT.RECIPIENT_ID, ACT.RECIPIENT_TYPE
ORDER BY max_created DESC
LIMIT 20;
【讨论】:
【参考方案5】:首先,这是一个非常棘手的查询,根据解释其含义以及如何改进它,可以为开发人员职位构建一个有趣的面试 =)。
MySQL 使用nested loop joins,这意味着当有一个连接时,MySQL 从一个表开始,并且对于表中的每个匹配行,循环遍历连接中第二个表中的相关行。
当您没有索引时,对于每一行,MySQL 都会在磁盘上获取在条件中使用的字段,并对另一个表中的每一行执行相同的操作。上磁盘既昂贵又耗时,最好从内存中取信息,这样就可以从索引中取数据了。
连接的顺序由 MySQL 优化器选择。但是您可以通过创建特殊索引(有时还提供提示)来提示 MySQL。
当你这样做(select * from activities order by created_at desc)
时,你会将整个表加载到一个临时的未索引表中,这无论如何都不是一件好事。但最糟糕的是,MySQL 应该从表开始连接,否则它需要在嵌套循环中检查表的每一行的条件。
使用索引进行排序或分组(也需要排序)是什么意思?这意味着您按照索引的顺序读取数据。但由于 MySQL 使用嵌套循环连接,因此只有当您排序的字段所在的表来自连接中的第一个表时,您才能利用索引进行排序。
created_at
字段不包含在 group by
子句中,这意味着您不关心从组中选择哪个(并且它们在组中可能相同)
因此,您需要相当长的关于活动(owner_type, trackable_type, recipient_id, recipient_type, owner_id, created_at)
的复合索引,以及一个可能很奢侈但需要的索引
(id, city_id)
用户。
现在,将查询重写为:
SELECT *
FROM
(SELECT a.id, group_concat(a.owner_id order by a.created_at desc) as owner_ids
FROM activities a
JOIN users u ON a.owner_id = u.id AND u.city_id = 1
WHERE a.owner_type = 'User'
GROUP BY trackable_type, recipient_id, recipient_type
ORDER BY a.created_at desc
limit 20 offset 0) as owners
JOIN activities a USING (id);
您应该查看 EXPLAIN 并可能在子查询中使用 STRAIGHT_JOIN 而不是 JOIN 以确保正确的连接顺序。
此解决方案似乎需要资源,而且确实如此。但这应该是您后续实验的良好基准。您可能应该从引入一些其他字段进行分组开始(在索引中包含 varchar 255 效率不高,尤其是其中两个),因此您应该有一些足够的前缀,或者明确地引入它们作为排序器或强制索引带前缀。您可能会创建一个特殊的 grouper 字段,该字段是(trackable_type,recipient_id,recipient_type)中的一个函数。这个owner_type = 'User'
也不是很好,比较整数等比较好。
【讨论】:
以上是关于具有多个分组或排序的mysql查询优化的主要内容,如果未能解决你的问题,请参考以下文章