如何优化具有多个外部联接的大型表查询的执行计划,group by和order by子句?

时间:2018-08-14 16:33:33

标签: mysql sql select innodb sql-optimization

我有以下数据库(简体):

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 bygroup by子句时,查询运行非常慢,例如以上配置需要10-15秒才能完成。但是,如果我省略这些子句中的任何一个,则查询运行得非常快(〜0.2秒)。

我已经尝试过什么?

  1. 我尝试使用FULLTEXT索引,但是并没有太大帮助,因为LIKE使用索引缩小了JOINs statemenet评估的结果。 / li>
  2. 我试图使用WHERE EXISTS (...)来查找left联接表中是否有记录,但是很遗憾,没有运气。

关于这些表之间关系的一些注释:

tracking -> tracking_items (1:n)
tracking_items -> cars (1:1)
tracking_items -> bikes (1:1)
tracking_items -> trucks (1:1)

因此,我正在寻找一种优化该查询的方法。

8 个答案:

答案 0 :(得分:5)

Bill Karwin建议,如果查询使用前导列为manufacture的索引,则查询的性能可能会更好。我赞同这个建议。特别是如果那是非常有选择性的。

我还注意到我们正在做GROUP BY t.id,其中id是表的主键。

tracking列表中未引用SELECT以外的任何表中的列。

这表明我们真的只对从t返回行感兴趣,而不对由于多个外部联接而创建重复项感兴趣。

如果COUNT()tracking_itembikes,{{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_itemstype。并且在该索引中包含tracking_id意味着可以从索引中满足查询条件,而无需访问基础页面。

对于tracking_object_idcarsbikes表,查询应使用索引为truckscar_id和{{ 1}}。无法绕过bike_idtruck_idcar_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;

索引:

  • 跟踪(制造,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;

答案 3 :(得分:2)

如果我的猜测是正确的,并且carsbikestrucks彼此独立(即特定的预汇总结果将仅包含其中一个的数据)。您最好对三个更简单的子查询(每个子查询一个)进行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 bygroup 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子句中的每个表(汽车,自行车和卡车)上应用过滤器,在联接之前,它应该过滤出行?