如何优化具有多个外连接到大表、group by 和 order by 子句的查询的执行计划?
How to optimize execution plan for query with multiple outer joins to huge tables, group by and order by clauses?
我有以下数据库(简化版):
CREATE TABLE `tracking` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`manufacture` varchar(100) NOT NULL,
`date_last_activity` datetime NOT NULL,
`date_created` datetime NOT NULL,
`date_updated` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `manufacture` (`manufacture`),
KEY `manufacture_date_last_activity` (`manufacture`, `date_last_activity`),
KEY `date_last_activity` (`date_last_activity`),
) ENGINE=InnoDB AUTO_INCREMENT=401353 DEFAULT CHARSET=utf8
CREATE TABLE `tracking_items` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`tracking_id` int(11) NOT NULL,
`tracking_object_id` varchar(100) NOT NULL,
`tracking_type` int(11) NOT NULL COMMENT 'Its used to specify the type of each item, e.g. car, bike, etc',
`date_created` datetime NOT NULL,
`date_updated` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `tracking_id` (`tracking_id`),
KEY `tracking_object_id` (`tracking_object_id`),
KEY `tracking_id_tracking_object_id` (`tracking_id`,`tracking_object_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1299995 DEFAULT CHARSET=utf8
CREATE TABLE `cars` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`car_id` varchar(255) NOT NULL COMMENT 'It must be VARCHAR, because the data is coming from external source.',
`manufacture` varchar(255) NOT NULL,
`car_text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`date_order` datetime NOT NULL,
`date_created` datetime NOT NULL,
`date_updated` datetime NOT NULL,
`deleted` tinyint(4) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `car_id` (`car_id`),
KEY `sort_field` (`date_order`)
) ENGINE=InnoDB AUTO_INCREMENT=150000025 DEFAULT CHARSET=utf8
这是我的 "problematic" 查询,运行速度极慢。
SELECT sql_no_cache `t`.*,
count(`t`.`id`) AS `cnt_filtered_items`
FROM `tracking` AS `t`
INNER JOIN `tracking_items` AS `ti` ON (`ti`.`tracking_id` = `t`.`id`)
LEFT JOIN `cars` AS `c` ON (`c`.`car_id` = `ti`.`tracking_object_id`
AND `ti`.`tracking_type` = 1)
LEFT JOIN `bikes` AS `b` ON (`b`.`bike_id` = `ti`.`tracking_object_id`
AND `ti`.`tracking_type` = 2)
LEFT JOIN `trucks` AS `tr` ON (`tr`.`truck_id` = `ti`.`tracking_object_id`
AND `ti`.`tracking_type` = 3)
WHERE (`t`.`manufacture` IN('1256703406078',
'9600048390403',
'1533405067830'))
AND (`c`.`car_text` LIKE '%europe%'
OR `b`.`bike_text` LIKE '%europe%'
OR `tr`.`truck_text` LIKE '%europe%')
GROUP BY `t`.`id`
ORDER BY `t`.`date_last_activity` ASC,
`t`.`id` ASC
LIMIT 15
这是上述查询 EXPLAIN
的结果:
+----+-------------+-------+--------+-----------------------------------------------------------------------+-------------+---------+-----------------------------+---------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | extra |
+----+-------------+-------+--------+-----------------------------------------------------------------------+-------------+---------+-----------------------------+---------+----------------------------------------------+
| 1 | SIMPLE | t | index | PRIMARY,manufacture,manufacture_date_last_activity,date_last_activity | PRIMARY | 4 | NULL | 400,000 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | ti | ref | tracking_id,tracking_object_id,tracking_id_tracking_object_id | tracking_id | 4 | table.t.id | 1 | NULL |
| 1 | SIMPLE | c | eq_ref | car_id | car_id | 767 | table.ti.tracking_object_id | 1 | Using where |
| 1 | SIMPLE | b | eq_ref | bike_id | bike_id | 767 | table.ti.tracking_object_id | 1 | Using where |
| 1 | SIMPLE | t | eq_ref | truck_id | truck_id | 767 | table.ti.tracking_object_id | 1 | Using where |
+----+-------------+-------+--------+-----------------------------------------------------------------------+-------------+---------+-----------------------------+---------+----------------------------------------------+
这个查询试图解决的问题是什么?
基本上,我需要找到 tracking
table 中可能与 tracking_items
(1:n) 中的记录关联的所有记录,其中 tracking_items
中的每条记录可能与左连接 table 中的记录相关联。过滤条件是查询中的关键部分。
我上面的查询有什么问题?
当有 order by
和 group by
子句时,查询运行得非常慢,例如完成上述配置需要 10-15 秒。但是,如果我省略这些子句中的任何一个,查询会 运行 非常快(~0.2 秒)。
我已经尝试了什么?
- 我尝试使用
FULLTEXT
索引,但它没有太大帮助,因为 LIKE
语句评估的结果被 JOINs
使用索引缩小.
- 我试过使用
WHERE EXISTS (...)
来查找left
中是否有记录加入tables,但不幸的是没有任何运气。
关于这些 table 之间关系的一些注释:
tracking -> tracking_items (1:n)
tracking_items -> cars (1:1)
tracking_items -> bikes (1:1)
tracking_items -> trucks (1:1)
所以,我正在寻找优化该查询的方法。
EXPLAIN 显示您正在对跟踪 table 进行索引扫描(type
列中的 "index")。索引扫描与 table 扫描的成本几乎一样,尤其是当扫描的索引是 PRIMARY 索引时。
rows
列还显示此索引扫描正在检查 > 355K 行(由于此数字只是粗略估计,它实际上正在检查所有 400K 行)。
你有 t.manufacture
的索引吗?我看到在 possible keys
中命名的两个索引可能包含该列(我不能仅根据索引名称确定),但由于某种原因优化器没有使用它们。也许您搜索的值集与 table 中的每一行都匹配。
如果 manufacture
值的列表旨在匹配 table 的子集,那么您可能需要向优化器提供提示以使其使用最佳索引。 https://dev.mysql.com/doc/refman/5.6/en/index-hints.html
使用LIKE '%word%'
模式匹配永远不能使用索引,并且必须在每一行上评估模式匹配。请参阅我的演示文稿,Full Text Search Throwdown。
您的 IN(...)
列表中有多少项? MySQL 有时会遇到很长的列表问题。参见 https://dev.mysql.com/doc/refman/5.6/en/range-optimization.html#equality-range-optimization
P.S.: 当你问一个查询优化问题时,你应该总是包括查询中引用的每个 table 的 SHOW CREATE TABLE
输出,所以回答的人不会必须猜测您当前拥有的索引、数据类型和约束条件。
如果我的猜测是正确的并且 cars
、bikes
和 trucks
彼此独立(即特定的预聚合结果将仅包含来自其中一个的数据).您最好联合三个更简单的子查询(每个子查询一个)。
虽然您不能对涉及前导通配符的 LIKE 做太多索引;将其拆分为 UNIONed 查询可以避免为所有 cars
和 bikes
匹配项评估 p.fb_message LIKE '%Europe%' OR p.fb_from_name LIKE '%Europe%
,以及为所有 b
和 [=18] 评估 c
条件=] 匹配,依此类推。
我不确定它是否有效,如何在 ON 子句中对每个 table(汽车、自行车和卡车)应用过滤器,在加入之前,它应该过滤掉行?
ALTER TABLE cars ADD FULLTEXT(car_text)
然后尝试
select sql_no_cache
`t`.*, -- If you are not using all, spell out the list
count(`t`.`id`) as `cnt_filtered_items` -- This does not make sense
-- and is possibly delivering an inflated value
from `tracking` as `t`
inner join `tracking_items` as `ti` ON (`ti`.`tracking_id` = `t`.`id`)
join -- not LEFT JOIN
`cars` as `c` ON `c`.`car_id` = `ti`.`tracking_object_id`
AND `ti`.`tracking_type` = 1
where `t`.`manufacture` in('1256703406078', '9600048390403', '1533405067830')
AND MATCH(c.car_text) AGAINST('+europe' IN BOOLEAN MODE)
group by `t`.`id` -- I don't know if this is necessary
order by `t`.`date_last_activity` asc, `t`.`id` asc
limit 15;
看看它是否会正确给你一个suitable 15 cars.
如果看起来没问题,那么将三者结合起来:
SELECT sql_no_cache
t2.*,
-- COUNT(*) -- this is probably broken
FROM (
( SELECT t.id FROM ... cars ... ) -- the query above
UNION ALL -- unless you need UNION DISTINCT
( SELECT t.id FROM ... bikes ... )
UNION ALL
( SELECT t.id FROM ... trucks ... )
) AS u
JOIN tracking AS t2 ON t2.id = u.id
ORDER BY t2.date_last_activity, t2.id
LIMIT 15;
注意里面的SELECTs
只送t.id
,不送t.*
。
需要另一个索引:
ti: (tracking_type, tracking_object_id) -- in either order
索引
当你有 INDEX(a,b)
时,你不需要 INDEX(a)
。 (这对有问题的查询没有帮助,但会帮助磁盘 space 和 INSERT
性能。)
当我看到 PRIMARY KEY(id), UNIQUE(x)
时,我会寻找任何好的理由不摆脱 id
并更改为 PRIMARY KEY(x)
。除非架构的 'simplification' 中有重要内容,否则这样的更改会有所帮助。是的,car_id
很笨重,等等,但它很大 table 并且额外的查找(从索引 BTree 到数据 BTree)正在受到伤害,等等。
我认为 KEY
sort_field(date_order)
不太可能被使用。要么放弃它(节省几 GB),要么以某种有用的方式组合它。让我们看看您认为它可能有用的查询。 (同样,与这个问题没有直接关系的建议。)
重新评论
我对我的公式做了一些实质性的修改。
我的公式有4个GROUP BYs
,3个在'derived' table(即FROM ( ... UNION ... )
),1个在外面。由于外部限制为 3*15 行,因此我不担心那里的性能。
进一步注意派生的 table 仅提供 t.id
,然后重新探测 tracking
以获取其他列。这让派生的 table 运行 快得多,但以额外的 JOIN
为代价。
请详细说明COUNT(t.id)
的意图;它在我的公式中不起作用,我不知道它在计算什么。
我不得不摆脱 ORs
;它们是次要的性能杀手。 (第一杀手是LIKE '%...'
。)
When there's order by
and group by
clauses the query runs extremely slow, e.g. 10-15 seconds to complete for the above configuration. However, if I omit any of these clauses, the query is running pretty quick (~0.2 seconds).
这很有趣...一般来说,我所知道的最好的优化技术是充分利用临时 tables,听起来它在这里非常有效。所以你首先要创建临时 table:
create temporary table tracking_ungrouped (
key (id)
)
select sql_no_cache `t`.*
from `tracking` as `t`
inner join `tracking_items` as `ti` on (`ti`.`tracking_id` = `t`.`id`)
left join `cars` as `c` on (`c`.`car_id` = `ti`.`tracking_object_id` AND `ti`.`tracking_type` = 1)
left join `bikes` as `b` on (`b`.`bike_id` = `ti`.`tracking_object_id` AND `ti`.`tracking_type` = 2)
left join `trucks` as `tr` on (`tr`.`truck_id` = `ti`.`tracking_object_id` AND `ti`.`tracking_type` = 3)
where
(`t`.`manufacture` in('1256703406078', '9600048390403', '1533405067830')) and
(`c`.`car_text` like '%europe%' or `b`.`bike_text` like '%europe%' or `tr`.`truck_text` like '%europe%');
然后查询你需要的结果:
select t.*, count(`t`.`id`) as `cnt_filtered_items`
from tracking_ungrouped t
group by `t`.`id`
order by `t`.`date_last_activity` asc, `t`.`id` asc
limit 15;
首先:您的查询对字符串内容做出了假设,这是不应该的。 car_text like '%europe%'
可能表示什么?也许是 'Sold in Europe only'
之类的东西?还是Sold outside Europe only
?具有矛盾含义的两个可能的字符串。因此,如果您在字符串中找到 europe
后假设了某种含义,那么您应该能够将此知识引入数据库中 - 例如,使用欧洲国旗或区域代码。
无论如何,您正在显示某些跟踪及其欧洲运输计数。所以 select 跟踪,select 运输很重要。您可以在 SELECT
子句或 FROM
子句中包含运输计数的聚合子查询。
SELECT
子句中的子查询:
select
t.*,
(
select count(*)
from tracking_items ti
where ti.tracking_id = t.id
and (tracking_type, tracking_object_id) in
(
select 1, car_id from cars where car_text like '%europe%'
union all
select 2, bike_id from bikes where bike_text like '%europe%'
union all
select 3, truck_id from trucks where truck_text like '%europe%'
)
from tracking t
where manufacture in ('1256703406078', '9600048390403', '1533405067830')
order by date_last_activity, id;
FROM
子句中的子查询:
select
t.*, agg.total
from tracking t
left join
(
select tracking_id, count(*) as total
from tracking_items ti
and (tracking_type, tracking_object_id) in
(
select 1, car_id from cars where car_text like '%europe%'
union all
select 2, bike_id from bikes where bike_text like '%europe%'
union all
select 3, truck_id from trucks where truck_text like '%europe%'
)
group by tracking_id
) agg on agg.tracking_id = t.id
where manufacture in ('1256703406078', '9600048390403', '1533405067830')
order by date_last_activity, id;
索引:
- 跟踪(制造商,date_last_activity,id)
- tracking_items(tracking_id, tracking_type, tracking_object_id)
- 汽车(car_text, car_id)
- 自行车(bike_text, bike_id)
- 卡车(truck_text, truck_id)
有时 MySQL 在简单连接上比在其他任何东西上都更强,因此可能值得尝试盲目连接运输记录,然后再看看它是汽车、自行车还是卡车:
select
t.*, agg.total
from tracking t
left join
(
select
tracking_id,
sum((ti.tracking_type = 1 and c.car_text like '%europe%')
or
(ti.tracking_type = 2 and b.bike_text like '%europe%')
or
(ti.tracking_type = 3 and t.truck_text like '%europe%')
) as total
from tracking_items ti
left join cars c on c.car_id = ti.tracking_object_id
left join bikes b on c.bike_id = ti.tracking_object_id
left join trucks t on t.truck_id = ti.tracking_object_id
group by tracking_id
) agg on agg.tracking_id = t.id
where manufacture in ('1256703406078', '9600048390403', '1533405067830')
order by date_last_activity, id;
Bill Karwin 建议如果查询使用前导列为 manufacture
的索引,则查询可能会执行得更好。我同意这个建议。特别是如果那是非常有选择性的。
我还注意到我们正在做 GROUP BY t.id
,其中 id
是 table 的主键。
SELECT
列表中没有引用除 tracking
之外的任何 table 的列。
这表明我们真的只对 returning 来自 t
的行感兴趣,而不对由于多个外部连接而创建重复行感兴趣。
如果 tracking_item
和 bikes
、[=24= 中有多个匹配行,COUNT()
聚合似乎有可能 return 膨胀计数],trucks
。如果有来自汽车的三行匹配行和来自自行车的四行匹配行,...... COUNT() 聚合将 return 值为 12,而不是 7。(或者数据中可能有一些保证这样就不会出现多个匹配行。)
如果 manufacture
非常有选择性,并且 return 是 tracking
中相当小的一组行,如果查询可以使用索引...
并且因为我们没有 returning 来自除 tracking
之外的任何 table 的任何列,除了计数或相关项目之外......
我很想测试 SELECT 列表中的相关子查询,以获取计数,并使用 HAVING 子句过滤掉计数为零的行。
像这样:
SELECT SQL_NO_CACHE `t`.*
, ( ( SELECT COUNT(1)
FROM `tracking_items` `tic`
JOIN `cars` `c`
ON `c`.`car_id` = `tic`.`tracking_object_id`
AND `c`.`car_text` LIKE '%europe%'
WHERE `tic`.`tracking_id` = `t`.`id`
AND `tic`.`tracking_type` = 1
)
+ ( SELECT COUNT(1)
FROM `tracking_items` `tib`
JOIN `bikes` `b`
ON `b`.`bike_id` = `tib`.`tracking_object_id`
AND `b`.`bike_text` LIKE '%europe%'
WHERE `tib`.`tracking_id` = `t`.`id`
AND `tib`.`tracking_type` = 2
)
+ ( SELECT COUNT(1)
FROM `tracking_items` `tit`
JOIN `trucks` `tr`
ON `tr`.`truck_id` = `tit`.`tracking_object_id`
AND `tr`.`truck_text` LIKE '%europe%'
WHERE `tit`.`tracking_id` = `t`.`id`
AND `tit`.`tracking_type` = 3
)
) AS cnt_filtered_items
FROM `tracking` `t`
WHERE `t`.`manufacture` IN ('1256703406078', '9600048390403', '1533405067830')
HAVING cnt_filtered_items > 0
ORDER
BY `t`.`date_last_activity` ASC
, `t`.`id` ASC
我们希望查询可以有效地利用 tracking
上的索引,前导列为 manufacture
。
而在 tracking_items
table 上,我们需要一个前导列为 type
和 tracking_id
的索引。在该索引中包含 tracking_object_id
意味着可以从索引中满足查询,而无需访问基础页面。
对于 cars
、bikes
和 trucks
table,查询应使用前导列为 car_id
、bike_id
和 truck_id
分别。无法绕过匹配字符串的 car_text
、bike_text
、truck_text
列的扫描……我们能做的最好的事情就是缩小需要执行该检查的行数.
这种方法(只是外部查询中的 tracking
table)应该消除对 GROUP BY
的需要,识别和折叠重复行所需的工作。
BUT 这种方法用相关的子查询代替连接,最适合有 SMALL 行数的查询 return由外部查询编辑。这些子查询针对外部查询处理的 每个 行执行。这些子查询必须有可用的 suitable 索引。即使进行了调整,大型集的性能仍然可能很差。
这仍然给我们留下了 ORDER BY
.
的 "Using filesort" 操作
如果相关项目的计数应该是乘积而不是加法的乘积,我们可以调整查询来实现这一点。 (我们必须处理零的 return,并且需要更改 HAVING 子句中的条件。)
如果不需要 return 相关项的 COUNT(),那么我很想将相关子查询从 SELECT 列表向下移动到 EXISTS
WHERE
子句中的谓词。
附加说明:支持 Rick James 关于索引的评论...似乎定义了冗余索引。即
KEY `manufacture` (`manufacture`)
KEY `manufacture_date_last_activity` (`manufacture`, `date_last_activity`)
单例列上的索引不是必需的,因为有另一个索引将该列作为前导列。
任何可以有效利用 manufacture
索引的查询都将能够有效利用 manufacture_date_last_activity
索引。也就是说,可以去掉manufacture
索引。
tracking_items
table也是如此,这两个索引:
KEY `tracking_id` (`tracking_id`)
KEY `tracking_id_tracking_object_id` (`tracking_id`,`tracking_object_id`)
可以删除 tracking_id
索引,因为它是多余的。
对于上面的查询,我建议添加一个覆盖索引:
KEY `tracking_items_IX3` (`tracking_id`,`tracking_type`,`tracking_object_id`)
-或者- 至少,一个非覆盖索引,其中这两列领先:
KEY `tracking_items_IX3` (`tracking_id`,`tracking_type`)
SELECT t.*
FROM (SELECT * FROM tracking WHERE manufacture
IN('1256703406078','9600048390403','1533405067830')) t
INNER JOIN (SELECT tracking_id, tracking_object_id, tracking_type FROM tracking_items
WHERE tracking_type IN (1,2,3)) ti
ON (ti.tracking_id = t.id)
LEFT JOIN (SELECT car_id, FROM cars WHERE car_text LIKE '%europe%') c
ON (c.car_id = ti.tracking_object_id AND ti.tracking_type = 1)
LEFT JOIN (SELECT bike_id FROM bikes WHERE bike_text LIKE '%europe%') b
ON (b.bike_id = ti.tracking_object_id AND ti.tracking_type = 2)
LEFT JOIN (SELECT truck_id FROM trucks WHERE truck_text LIKE '%europe%') tr
ON (tr.truck_id = ti.tracking_object_id AND ti.tracking_type = 3)
ORDER BY t.date_last_activity ASC, t.id ASC
子查询在连接时执行得更快,如果它们要过滤掉大量记录。
tracking table 的子查询将过滤掉很多其他不需要的 manufacture 并导致更小的 table t 被加入。
类似地应用了 tracking_items table 的条件,因为我们只对 [=56 感兴趣=] 1,2 和 3;创建一个更小的 table ti。如果有很多tracking_objects,你甚至可以在这个子查询中添加跟踪对象过滤器。
tables 汽车、自行车、卡车 的类似方法及其各自的条件 包含欧洲的文本 帮助我们分别创建更小的 tables c,b,tr。
还删除了 t.id 组,因为 t.id 是唯一的,我们正在对其执行内部连接和左连接或结果 table,因为没有必要。
最后,我只从每个 tables 中选择 所需的列,这也将减少内存 space 和 运行 时间的负载。
希望这对您有所帮助。请让我知道您的反馈和 运行 统计数据。
我有以下数据库(简化版):
CREATE TABLE `tracking` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`manufacture` varchar(100) NOT NULL,
`date_last_activity` datetime NOT NULL,
`date_created` datetime NOT NULL,
`date_updated` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `manufacture` (`manufacture`),
KEY `manufacture_date_last_activity` (`manufacture`, `date_last_activity`),
KEY `date_last_activity` (`date_last_activity`),
) ENGINE=InnoDB AUTO_INCREMENT=401353 DEFAULT CHARSET=utf8
CREATE TABLE `tracking_items` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`tracking_id` int(11) NOT NULL,
`tracking_object_id` varchar(100) NOT NULL,
`tracking_type` int(11) NOT NULL COMMENT 'Its used to specify the type of each item, e.g. car, bike, etc',
`date_created` datetime NOT NULL,
`date_updated` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `tracking_id` (`tracking_id`),
KEY `tracking_object_id` (`tracking_object_id`),
KEY `tracking_id_tracking_object_id` (`tracking_id`,`tracking_object_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1299995 DEFAULT CHARSET=utf8
CREATE TABLE `cars` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`car_id` varchar(255) NOT NULL COMMENT 'It must be VARCHAR, because the data is coming from external source.',
`manufacture` varchar(255) NOT NULL,
`car_text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`date_order` datetime NOT NULL,
`date_created` datetime NOT NULL,
`date_updated` datetime NOT NULL,
`deleted` tinyint(4) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `car_id` (`car_id`),
KEY `sort_field` (`date_order`)
) ENGINE=InnoDB AUTO_INCREMENT=150000025 DEFAULT CHARSET=utf8
这是我的 "problematic" 查询,运行速度极慢。
SELECT sql_no_cache `t`.*,
count(`t`.`id`) AS `cnt_filtered_items`
FROM `tracking` AS `t`
INNER JOIN `tracking_items` AS `ti` ON (`ti`.`tracking_id` = `t`.`id`)
LEFT JOIN `cars` AS `c` ON (`c`.`car_id` = `ti`.`tracking_object_id`
AND `ti`.`tracking_type` = 1)
LEFT JOIN `bikes` AS `b` ON (`b`.`bike_id` = `ti`.`tracking_object_id`
AND `ti`.`tracking_type` = 2)
LEFT JOIN `trucks` AS `tr` ON (`tr`.`truck_id` = `ti`.`tracking_object_id`
AND `ti`.`tracking_type` = 3)
WHERE (`t`.`manufacture` IN('1256703406078',
'9600048390403',
'1533405067830'))
AND (`c`.`car_text` LIKE '%europe%'
OR `b`.`bike_text` LIKE '%europe%'
OR `tr`.`truck_text` LIKE '%europe%')
GROUP BY `t`.`id`
ORDER BY `t`.`date_last_activity` ASC,
`t`.`id` ASC
LIMIT 15
这是上述查询 EXPLAIN
的结果:
+----+-------------+-------+--------+-----------------------------------------------------------------------+-------------+---------+-----------------------------+---------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | extra |
+----+-------------+-------+--------+-----------------------------------------------------------------------+-------------+---------+-----------------------------+---------+----------------------------------------------+
| 1 | SIMPLE | t | index | PRIMARY,manufacture,manufacture_date_last_activity,date_last_activity | PRIMARY | 4 | NULL | 400,000 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | ti | ref | tracking_id,tracking_object_id,tracking_id_tracking_object_id | tracking_id | 4 | table.t.id | 1 | NULL |
| 1 | SIMPLE | c | eq_ref | car_id | car_id | 767 | table.ti.tracking_object_id | 1 | Using where |
| 1 | SIMPLE | b | eq_ref | bike_id | bike_id | 767 | table.ti.tracking_object_id | 1 | Using where |
| 1 | SIMPLE | t | eq_ref | truck_id | truck_id | 767 | table.ti.tracking_object_id | 1 | Using where |
+----+-------------+-------+--------+-----------------------------------------------------------------------+-------------+---------+-----------------------------+---------+----------------------------------------------+
这个查询试图解决的问题是什么?
基本上,我需要找到 tracking
table 中可能与 tracking_items
(1:n) 中的记录关联的所有记录,其中 tracking_items
中的每条记录可能与左连接 table 中的记录相关联。过滤条件是查询中的关键部分。
我上面的查询有什么问题?
当有 order by
和 group by
子句时,查询运行得非常慢,例如完成上述配置需要 10-15 秒。但是,如果我省略这些子句中的任何一个,查询会 运行 非常快(~0.2 秒)。
我已经尝试了什么?
- 我尝试使用
FULLTEXT
索引,但它没有太大帮助,因为LIKE
语句评估的结果被JOINs
使用索引缩小. - 我试过使用
WHERE EXISTS (...)
来查找left
中是否有记录加入tables,但不幸的是没有任何运气。
关于这些 table 之间关系的一些注释:
tracking -> tracking_items (1:n)
tracking_items -> cars (1:1)
tracking_items -> bikes (1:1)
tracking_items -> trucks (1:1)
所以,我正在寻找优化该查询的方法。
EXPLAIN 显示您正在对跟踪 table 进行索引扫描(type
列中的 "index")。索引扫描与 table 扫描的成本几乎一样,尤其是当扫描的索引是 PRIMARY 索引时。
rows
列还显示此索引扫描正在检查 > 355K 行(由于此数字只是粗略估计,它实际上正在检查所有 400K 行)。
你有 t.manufacture
的索引吗?我看到在 possible keys
中命名的两个索引可能包含该列(我不能仅根据索引名称确定),但由于某种原因优化器没有使用它们。也许您搜索的值集与 table 中的每一行都匹配。
如果 manufacture
值的列表旨在匹配 table 的子集,那么您可能需要向优化器提供提示以使其使用最佳索引。 https://dev.mysql.com/doc/refman/5.6/en/index-hints.html
使用LIKE '%word%'
模式匹配永远不能使用索引,并且必须在每一行上评估模式匹配。请参阅我的演示文稿,Full Text Search Throwdown。
您的 IN(...)
列表中有多少项? MySQL 有时会遇到很长的列表问题。参见 https://dev.mysql.com/doc/refman/5.6/en/range-optimization.html#equality-range-optimization
P.S.: 当你问一个查询优化问题时,你应该总是包括查询中引用的每个 table 的 SHOW CREATE TABLE
输出,所以回答的人不会必须猜测您当前拥有的索引、数据类型和约束条件。
如果我的猜测是正确的并且 cars
、bikes
和 trucks
彼此独立(即特定的预聚合结果将仅包含来自其中一个的数据).您最好联合三个更简单的子查询(每个子查询一个)。
虽然您不能对涉及前导通配符的 LIKE 做太多索引;将其拆分为 UNIONed 查询可以避免为所有 cars
和 bikes
匹配项评估 p.fb_message LIKE '%Europe%' OR p.fb_from_name LIKE '%Europe%
,以及为所有 b
和 [=18] 评估 c
条件=] 匹配,依此类推。
我不确定它是否有效,如何在 ON 子句中对每个 table(汽车、自行车和卡车)应用过滤器,在加入之前,它应该过滤掉行?
ALTER TABLE cars ADD FULLTEXT(car_text)
然后尝试
select sql_no_cache
`t`.*, -- If you are not using all, spell out the list
count(`t`.`id`) as `cnt_filtered_items` -- This does not make sense
-- and is possibly delivering an inflated value
from `tracking` as `t`
inner join `tracking_items` as `ti` ON (`ti`.`tracking_id` = `t`.`id`)
join -- not LEFT JOIN
`cars` as `c` ON `c`.`car_id` = `ti`.`tracking_object_id`
AND `ti`.`tracking_type` = 1
where `t`.`manufacture` in('1256703406078', '9600048390403', '1533405067830')
AND MATCH(c.car_text) AGAINST('+europe' IN BOOLEAN MODE)
group by `t`.`id` -- I don't know if this is necessary
order by `t`.`date_last_activity` asc, `t`.`id` asc
limit 15;
看看它是否会正确给你一个suitable 15 cars.
如果看起来没问题,那么将三者结合起来:
SELECT sql_no_cache
t2.*,
-- COUNT(*) -- this is probably broken
FROM (
( SELECT t.id FROM ... cars ... ) -- the query above
UNION ALL -- unless you need UNION DISTINCT
( SELECT t.id FROM ... bikes ... )
UNION ALL
( SELECT t.id FROM ... trucks ... )
) AS u
JOIN tracking AS t2 ON t2.id = u.id
ORDER BY t2.date_last_activity, t2.id
LIMIT 15;
注意里面的SELECTs
只送t.id
,不送t.*
。
需要另一个索引:
ti: (tracking_type, tracking_object_id) -- in either order
索引
当你有 INDEX(a,b)
时,你不需要 INDEX(a)
。 (这对有问题的查询没有帮助,但会帮助磁盘 space 和 INSERT
性能。)
当我看到 PRIMARY KEY(id), UNIQUE(x)
时,我会寻找任何好的理由不摆脱 id
并更改为 PRIMARY KEY(x)
。除非架构的 'simplification' 中有重要内容,否则这样的更改会有所帮助。是的,car_id
很笨重,等等,但它很大 table 并且额外的查找(从索引 BTree 到数据 BTree)正在受到伤害,等等。
我认为 KEY
sort_field(date_order)
不太可能被使用。要么放弃它(节省几 GB),要么以某种有用的方式组合它。让我们看看您认为它可能有用的查询。 (同样,与这个问题没有直接关系的建议。)
重新评论
我对我的公式做了一些实质性的修改。
我的公式有4个GROUP BYs
,3个在'derived' table(即FROM ( ... UNION ... )
),1个在外面。由于外部限制为 3*15 行,因此我不担心那里的性能。
进一步注意派生的 table 仅提供 t.id
,然后重新探测 tracking
以获取其他列。这让派生的 table 运行 快得多,但以额外的 JOIN
为代价。
请详细说明COUNT(t.id)
的意图;它在我的公式中不起作用,我不知道它在计算什么。
我不得不摆脱 ORs
;它们是次要的性能杀手。 (第一杀手是LIKE '%...'
。)
When there's
order by
andgroup by
clauses the query runs extremely slow, e.g. 10-15 seconds to complete for the above configuration. However, if I omit any of these clauses, the query is running pretty quick (~0.2 seconds).
这很有趣...一般来说,我所知道的最好的优化技术是充分利用临时 tables,听起来它在这里非常有效。所以你首先要创建临时 table:
create temporary table tracking_ungrouped (
key (id)
)
select sql_no_cache `t`.*
from `tracking` as `t`
inner join `tracking_items` as `ti` on (`ti`.`tracking_id` = `t`.`id`)
left join `cars` as `c` on (`c`.`car_id` = `ti`.`tracking_object_id` AND `ti`.`tracking_type` = 1)
left join `bikes` as `b` on (`b`.`bike_id` = `ti`.`tracking_object_id` AND `ti`.`tracking_type` = 2)
left join `trucks` as `tr` on (`tr`.`truck_id` = `ti`.`tracking_object_id` AND `ti`.`tracking_type` = 3)
where
(`t`.`manufacture` in('1256703406078', '9600048390403', '1533405067830')) and
(`c`.`car_text` like '%europe%' or `b`.`bike_text` like '%europe%' or `tr`.`truck_text` like '%europe%');
然后查询你需要的结果:
select t.*, count(`t`.`id`) as `cnt_filtered_items`
from tracking_ungrouped t
group by `t`.`id`
order by `t`.`date_last_activity` asc, `t`.`id` asc
limit 15;
首先:您的查询对字符串内容做出了假设,这是不应该的。 car_text like '%europe%'
可能表示什么?也许是 'Sold in Europe only'
之类的东西?还是Sold outside Europe only
?具有矛盾含义的两个可能的字符串。因此,如果您在字符串中找到 europe
后假设了某种含义,那么您应该能够将此知识引入数据库中 - 例如,使用欧洲国旗或区域代码。
无论如何,您正在显示某些跟踪及其欧洲运输计数。所以 select 跟踪,select 运输很重要。您可以在 SELECT
子句或 FROM
子句中包含运输计数的聚合子查询。
SELECT
子句中的子查询:
select
t.*,
(
select count(*)
from tracking_items ti
where ti.tracking_id = t.id
and (tracking_type, tracking_object_id) in
(
select 1, car_id from cars where car_text like '%europe%'
union all
select 2, bike_id from bikes where bike_text like '%europe%'
union all
select 3, truck_id from trucks where truck_text like '%europe%'
)
from tracking t
where manufacture in ('1256703406078', '9600048390403', '1533405067830')
order by date_last_activity, id;
FROM
子句中的子查询:
select
t.*, agg.total
from tracking t
left join
(
select tracking_id, count(*) as total
from tracking_items ti
and (tracking_type, tracking_object_id) in
(
select 1, car_id from cars where car_text like '%europe%'
union all
select 2, bike_id from bikes where bike_text like '%europe%'
union all
select 3, truck_id from trucks where truck_text like '%europe%'
)
group by tracking_id
) agg on agg.tracking_id = t.id
where manufacture in ('1256703406078', '9600048390403', '1533405067830')
order by date_last_activity, id;
索引:
- 跟踪(制造商,date_last_activity,id)
- tracking_items(tracking_id, tracking_type, tracking_object_id)
- 汽车(car_text, car_id)
- 自行车(bike_text, bike_id)
- 卡车(truck_text, truck_id)
有时 MySQL 在简单连接上比在其他任何东西上都更强,因此可能值得尝试盲目连接运输记录,然后再看看它是汽车、自行车还是卡车:
select
t.*, agg.total
from tracking t
left join
(
select
tracking_id,
sum((ti.tracking_type = 1 and c.car_text like '%europe%')
or
(ti.tracking_type = 2 and b.bike_text like '%europe%')
or
(ti.tracking_type = 3 and t.truck_text like '%europe%')
) as total
from tracking_items ti
left join cars c on c.car_id = ti.tracking_object_id
left join bikes b on c.bike_id = ti.tracking_object_id
left join trucks t on t.truck_id = ti.tracking_object_id
group by tracking_id
) agg on agg.tracking_id = t.id
where manufacture in ('1256703406078', '9600048390403', '1533405067830')
order by date_last_activity, id;
Bill Karwin 建议如果查询使用前导列为 manufacture
的索引,则查询可能会执行得更好。我同意这个建议。特别是如果那是非常有选择性的。
我还注意到我们正在做 GROUP BY t.id
,其中 id
是 table 的主键。
SELECT
列表中没有引用除 tracking
之外的任何 table 的列。
这表明我们真的只对 returning 来自 t
的行感兴趣,而不对由于多个外部连接而创建重复行感兴趣。
如果 tracking_item
和 bikes
、[=24= 中有多个匹配行,COUNT()
聚合似乎有可能 return 膨胀计数],trucks
。如果有来自汽车的三行匹配行和来自自行车的四行匹配行,...... COUNT() 聚合将 return 值为 12,而不是 7。(或者数据中可能有一些保证这样就不会出现多个匹配行。)
如果 manufacture
非常有选择性,并且 return 是 tracking
中相当小的一组行,如果查询可以使用索引...
并且因为我们没有 returning 来自除 tracking
之外的任何 table 的任何列,除了计数或相关项目之外......
我很想测试 SELECT 列表中的相关子查询,以获取计数,并使用 HAVING 子句过滤掉计数为零的行。
像这样:
SELECT SQL_NO_CACHE `t`.*
, ( ( SELECT COUNT(1)
FROM `tracking_items` `tic`
JOIN `cars` `c`
ON `c`.`car_id` = `tic`.`tracking_object_id`
AND `c`.`car_text` LIKE '%europe%'
WHERE `tic`.`tracking_id` = `t`.`id`
AND `tic`.`tracking_type` = 1
)
+ ( SELECT COUNT(1)
FROM `tracking_items` `tib`
JOIN `bikes` `b`
ON `b`.`bike_id` = `tib`.`tracking_object_id`
AND `b`.`bike_text` LIKE '%europe%'
WHERE `tib`.`tracking_id` = `t`.`id`
AND `tib`.`tracking_type` = 2
)
+ ( SELECT COUNT(1)
FROM `tracking_items` `tit`
JOIN `trucks` `tr`
ON `tr`.`truck_id` = `tit`.`tracking_object_id`
AND `tr`.`truck_text` LIKE '%europe%'
WHERE `tit`.`tracking_id` = `t`.`id`
AND `tit`.`tracking_type` = 3
)
) AS cnt_filtered_items
FROM `tracking` `t`
WHERE `t`.`manufacture` IN ('1256703406078', '9600048390403', '1533405067830')
HAVING cnt_filtered_items > 0
ORDER
BY `t`.`date_last_activity` ASC
, `t`.`id` ASC
我们希望查询可以有效地利用 tracking
上的索引,前导列为 manufacture
。
而在 tracking_items
table 上,我们需要一个前导列为 type
和 tracking_id
的索引。在该索引中包含 tracking_object_id
意味着可以从索引中满足查询,而无需访问基础页面。
对于 cars
、bikes
和 trucks
table,查询应使用前导列为 car_id
、bike_id
和 truck_id
分别。无法绕过匹配字符串的 car_text
、bike_text
、truck_text
列的扫描……我们能做的最好的事情就是缩小需要执行该检查的行数.
这种方法(只是外部查询中的 tracking
table)应该消除对 GROUP BY
的需要,识别和折叠重复行所需的工作。
BUT 这种方法用相关的子查询代替连接,最适合有 SMALL 行数的查询 return由外部查询编辑。这些子查询针对外部查询处理的 每个 行执行。这些子查询必须有可用的 suitable 索引。即使进行了调整,大型集的性能仍然可能很差。
这仍然给我们留下了 ORDER BY
.
如果相关项目的计数应该是乘积而不是加法的乘积,我们可以调整查询来实现这一点。 (我们必须处理零的 return,并且需要更改 HAVING 子句中的条件。)
如果不需要 return 相关项的 COUNT(),那么我很想将相关子查询从 SELECT 列表向下移动到 EXISTS
WHERE
子句中的谓词。
附加说明:支持 Rick James 关于索引的评论...似乎定义了冗余索引。即
KEY `manufacture` (`manufacture`)
KEY `manufacture_date_last_activity` (`manufacture`, `date_last_activity`)
单例列上的索引不是必需的,因为有另一个索引将该列作为前导列。
任何可以有效利用 manufacture
索引的查询都将能够有效利用 manufacture_date_last_activity
索引。也就是说,可以去掉manufacture
索引。
tracking_items
table也是如此,这两个索引:
KEY `tracking_id` (`tracking_id`)
KEY `tracking_id_tracking_object_id` (`tracking_id`,`tracking_object_id`)
可以删除 tracking_id
索引,因为它是多余的。
对于上面的查询,我建议添加一个覆盖索引:
KEY `tracking_items_IX3` (`tracking_id`,`tracking_type`,`tracking_object_id`)
-或者- 至少,一个非覆盖索引,其中这两列领先:
KEY `tracking_items_IX3` (`tracking_id`,`tracking_type`)
SELECT t.*
FROM (SELECT * FROM tracking WHERE manufacture
IN('1256703406078','9600048390403','1533405067830')) t
INNER JOIN (SELECT tracking_id, tracking_object_id, tracking_type FROM tracking_items
WHERE tracking_type IN (1,2,3)) ti
ON (ti.tracking_id = t.id)
LEFT JOIN (SELECT car_id, FROM cars WHERE car_text LIKE '%europe%') c
ON (c.car_id = ti.tracking_object_id AND ti.tracking_type = 1)
LEFT JOIN (SELECT bike_id FROM bikes WHERE bike_text LIKE '%europe%') b
ON (b.bike_id = ti.tracking_object_id AND ti.tracking_type = 2)
LEFT JOIN (SELECT truck_id FROM trucks WHERE truck_text LIKE '%europe%') tr
ON (tr.truck_id = ti.tracking_object_id AND ti.tracking_type = 3)
ORDER BY t.date_last_activity ASC, t.id ASC
子查询在连接时执行得更快,如果它们要过滤掉大量记录。
tracking table 的子查询将过滤掉很多其他不需要的 manufacture 并导致更小的 table t 被加入。
类似地应用了 tracking_items table 的条件,因为我们只对 [=56 感兴趣=] 1,2 和 3;创建一个更小的 table ti。如果有很多tracking_objects,你甚至可以在这个子查询中添加跟踪对象过滤器。
tables 汽车、自行车、卡车 的类似方法及其各自的条件 包含欧洲的文本 帮助我们分别创建更小的 tables c,b,tr。
还删除了 t.id 组,因为 t.id 是唯一的,我们正在对其执行内部连接和左连接或结果 table,因为没有必要。
最后,我只从每个 tables 中选择 所需的列,这也将减少内存 space 和 运行 时间的负载。
希望这对您有所帮助。请让我知道您的反馈和 运行 统计数据。