我有以下数据库(简体):
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
这是我的“问题”查询,运行速度非常慢。
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
表中找到可能与tracking_items
(1:n)中的记录相关联的所有记录,其中tracking_items
中的每个记录可能与中的记录相关联左联接表。过滤条件是查询中的关键部分。
上面的查询有什么问题?
当有order by
和group by
子句时,查询运行非常慢,例如以上配置需要10-15秒才能完成。但是,如果我省略这些子句中的任何一个,则查询运行得非常快(〜0.2秒)。
我已经尝试过什么?
FULLTEXT
索引,但是并没有太大帮助,因为LIKE
使用索引缩小了JOINs
statemenet评估的结果。 / li>
WHERE EXISTS (...)
来查找left
联接表中是否有记录,但是很遗憾,没有运气。关于这些表之间关系的一些注释:
tracking -> tracking_items (1:n)
tracking_items -> cars (1:1)
tracking_items -> bikes (1:1)
tracking_items -> trucks (1:1)
因此,我正在寻找一种优化该查询的方法。
答案 0 :(得分:5)
Bill Karwin建议,如果查询使用前导列为manufacture
的索引,则查询的性能可能会更好。我赞同这个建议。特别是如果那是非常有选择性的。
我还注意到我们正在做GROUP BY t.id
,其中id
是表的主键。
tracking
列表中未引用SELECT
以外的任何表中的列。
这表明我们真的只对从t
返回行感兴趣,而不对由于多个外部联接而创建重复项感兴趣。
如果COUNT()
和tracking_item
,bikes
,{{1}中有多个匹配行,则好像cars
聚合有可能返回虚增计数}。如果汽车中有3个匹配行,而自行车中有4个匹配行,则COUNT()集合将返回值12,而不是7。(或者数据中有一定的保证,使得永远不会有多个匹配的行。)
如果trucks
的选择性很强,并且如果查询可以使用索引...,则返回manufacture
中相当小的行集...
并且由于我们没有返回tracking
以外的任何表中的任何列,除了计数或相关项之外……
我很想测试SELECT列表中的相关子查询,以获取计数,并使用HAVING子句过滤掉零计数行。
类似的东西:
tracking
我们希望查询可以有效地使用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
和type
。并且在该索引中包含tracking_id
意味着可以从索引中满足查询条件,而无需访问基础页面。
对于tracking_object_id
,cars
和bikes
表,查询应使用索引为trucks
,car_id
和{{ 1}}。无法绕过bike_id
,truck_id
,car_text
列以查找匹配的字符串...我们能做的最好的就是缩小需要执行检查的行数
这种方法(只是外部查询中的bike_text
表)应消除对truck_text
的需要,而tracking
是识别和折叠重复行所需的工作。
但这种方法用相关子查询替换联接,最适合外部查询返回的行数为 SMALL 的查询。这些子查询针对由外部查询处理的每行执行。必须使这些子查询具有合适的索引。即使对这些参数进行了调整,大型设备的性能仍然可能令人恐惧。
这仍然使GROUP BY
拥有“使用文件排序”操作。
如果相关项目的计数应该是一个乘积而不是加法的乘积,我们可以调整查询以实现这一点。 (我们必须用零的返回值来处理,并且必须更改HAVING子句中的条件。)
如果不需要返回相关项目的COUNT(),那么我很想将相关的子查询从SELECT列表中移到ORDER BY
子句中的EXISTS
谓词中
其他说明:赞同Rick James关于索引的意见...似乎定义了多余的索引。即
WHERE
单例列上的索引不是必需的,因为还有另一个索引将该列作为前导列。
任何可以有效利用KEY `manufacture` (`manufacture`)
KEY `manufacture_date_last_activity` (`manufacture`, `date_last_activity`)
索引的查询都将能够有效利用manufacture
索引。也就是说,manufacture_date_last_activity
索引可能会被删除。
manufacture
表和这两个索引也是如此:
tracking_items
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`)
答案 1 :(得分:4)
EXPLAIN显示您正在跟踪表上进行索引扫描(type
列中的“ index”)。索引扫描与表扫描几乎一样昂贵,尤其是当扫描的索引是PRIMARY索引时。
rows
列还显示此索引扫描正在检查> 35.5万行(由于该数字只是一个粗略的估计,因此实际上是在检查所有40万行)。
您在t.manufacture
上有索引吗?我看到在possible keys
中命名的两个索引可能包括该列(我不能完全根据索引的名称来确定),但是由于某些原因,优化器没有使用它们。也许无论如何,表中的每一行都会匹配您要搜索的值集。
如果manufacture
值列表旨在匹配表的子集,则可能需要向优化器提示,以使其使用最佳索引。 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
PS:在询问查询优化问题时,应始终为查询中引用的每个表包括SHOW CREATE TABLE
输出,因此回答问题的人们不必猜测什么索引,数据类型,约束条件您当前拥有。
答案 2 :(得分:4)
首先:您的查询对字符串内容进行了假设,而不应该这样做。 car_text like '%europe%'
可能表示什么?像'Sold in Europe only'
之类的东西?还是Sold outside Europe only
?两个可能含义相反的字符串。因此,如果您在字符串中找到europe
后就假设了某种含义,那么您应该能够在数据库中引入这一知识-例如带有欧洲标志或区域代码。
无论如何,您正在显示其欧洲运输量的某些跟踪。因此,选择跟踪,选择运输计数。您可以在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;
索引:
有时候,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;
答案 3 :(得分:2)
如果我的猜测是正确的,并且cars
,bikes
和trucks
彼此独立(即特定的预汇总结果将仅包含其中一个的数据)。您最好对三个更简单的子查询(每个子查询一个)进行UNION。
虽然您不能对涉及前导通配符的LIKE做很多索引处理;将其拆分为UNIONed查询可以避免评估所有p.fb_message LIKE '%Europe%' OR p.fb_from_name LIKE '%Europe%
和cars
匹配项的bikes
,以及所有c
和{{1 }}匹配,依此类推。
答案 4 :(得分:2)
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;
查看它是否可以正确地为您提供15辆汽车。
如果看起来还可以,那么将三个结合在一起:
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)
。 (这无助于所查询,但将有助于磁盘空间和INSERT
性能。)
当我看到PRIMARY KEY(id), UNIQUE(x)
时,我有充分的理由不放弃id
而改成PRIMARY KEY(x)
。除非模式的“简化”有重大意义,否则这种更改将有所帮助。是的,car_id
很庞大,等等,但是它是一个很大的表,并且多余的查找(从索引BTree到数据BTree)令人讨厌,等等。
我认为极不可能使用KEY
sort_field (date_order)
。要么删除它(节省几GB),要么以某种有用的方式组合它。让我们看看您认为它可能有用的查询。 (同样,与该课题没有直接关系的建议。)
是评论
我对自己的配方做了一些实质性的改变。
我的公式有4个GROUP BYs
,“派生”表中有3个(即FROM ( ... UNION ... )
),外面有一个。由于外部限制为3 * 15行,因此我不必担心那里的性能。
还要注意,派生表仅传递t.id
,然后重新探测tracking
以获取其他列。这样可以使派生表运行得更快,但只需花很少的钱在外面的额外JOIN
上。
请详细说明COUNT(t.id)
的意图;它不适用于我的配方,我也不知道它在计算什么。
我必须摆脱ORs
;它们是次要性能杀手。 (第一个杀手是LIKE '%...'
。)
答案 5 :(得分:2)
当有
order by
和group by
子句时,查询运行非常慢,例如以上配置需要10-15秒才能完成。但是,如果我省略这些子句中的任何一个,则查询运行得非常快(〜0.2秒)。
这很有趣……通常我所知道的最佳优化技术是善用临时表,这听起来像在这里确实能很好地工作。因此,您首先要创建临时表:
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;
答案 6 :(得分:2)
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
子查询在加入时以及要过滤掉大量记录时执行得更快。
跟踪表的子查询将过滤掉许多其他不需要的 制造 ,并生成较小的表 t 要加入。
类似地为 tracking_items 表应用了条件,因为我们仅对 tracking_types 1,2和3 感兴趣;创建较小的表 ti 。如果跟踪对象很多,您甚至可以在此子查询中添加跟踪对象过滤器。
表的相似方法汽车,自行车,卡车,并根据其各自的 文本包含欧洲 的条件,有助于我们创建较小的表<分别是strong> c,b,tr 。
通过t.id删除组也是唯一的,因为不需要,我们正在对该表或结果表执行内部联接和左联接。
最后,我只从每个表中选择 必需的列 ,这还将减少内存空间和运行时的负担。
希望这会有所帮助。请让我知道您的反馈并运行统计信息。
答案 7 :(得分:0)
我不确定它是否可以工作,如何在ON子句中的每个表(汽车,自行车和卡车)上应用过滤器,在联接之前,它应该过滤出行?