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

vulvrdjw  于 2021-06-20  发布在  Mysql
关注(0)|答案(6)|浏览(273)

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

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 使用索引。
我试着用 WHERE EXISTS (...) 以查找中是否有记录 left 加入了表格,但不幸的是没有任何运气。
关于这些表之间关系的一些注解:

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

所以,我在寻找一种优化查询的方法。

2w3kk1z5

2w3kk1z51#

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 排序字段 (date_order) 永远不会被使用。要么放弃它(节省一些gb),要么以某种有用的方式合并它。让我们看看您认为有用的查询(同样,这是一个与这个问题没有直接关系的建议。)
重新评论
我对m做了一些实质性的改变

imzjd6km

imzjd6km2#

billkarwin建议,如果使用前导列为 manufacture . 我赞成这个建议。特别是如果这很有选择性的话。
我还注意到我们正在做一个 GROUP BY t.id ,在哪里 id 是表的主键。
没有任何表中的列,除了 trackingSELECT 列表。
这表明我们真正感兴趣的是返回 t ,而不是由于多个外部联接而创建重复项。
好像是 COUNT() 如果聚合中有多个匹配行,则聚合有可能返回膨胀的计数 tracking_item 以及 bikes , cars , trucks . 如果有三个匹配的行来自汽车,四个匹配的行来自自行车。。。count()聚合将返回值12,而不是7(或者数据中有某种保证,这样就不会有多个匹配行。)
如果 manufacture 是非常有选择性的,它从 tracking ,如果查询可以使用索引。。。
因为我们不从任何表返回任何列,除了 tracking ,除了计数或相关项目。。。
我会尝试在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 表中,我们需要一个前导列为 type 以及 tracking_id . 包括 tracking_object_id 在这种情况下,索引意味着可以通过索引满足查询,而无需访问底层页面。
对于 cars , bikes 以及 trucks 查询表应使用前导列为的索引 car_id , bike_id ,和 truck_id 分别。无法绕过扫描 car_text , bike_text , truck_text 匹配字符串的列。。。我们所能做的就是缩小需要执行检查的行数。
这种方法(只是 tracking 表)应该不需要 GROUP BY ,识别和折叠重复行所需的工作。
但是这种用相关子查询替换连接的方法最适合于外部查询返回少量行的查询。对于外部查询处理的每一行,都会执行这些子查询。这些子查询必须有合适的索引可用。即使那些调整,仍然有潜力可怕的表现为大集。
这仍然留给我们一个“usingfilesort”操作 ORDER BY .
如果相关项的计数应该是乘法的乘积,而不是加法的乘积,那么我们可以调整查询来实现这一点(我们必须处理零的返回,having子句中的条件需要更改。)
如果不需要返回相关项的count(),那么我会尝试将相关子查询从select列表向下移动到 EXISTS 中的 predicate WHERE 条款。
附加说明:附议rick james关于索引的评论。。。似乎定义了冗余索引。即

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`)

-或者-至少是两列前导的非覆盖索引:

KEY `tracking_items_IX3` (`tracking_id`,`tracking_type`)
d6kp6zgx

d6kp6zgx3#

当有 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;
pinkon5k

pinkon5k4#

explain显示您正在执行索引扫描(“index”在 type 列)。索引扫描的成本与表扫描相当,尤其是当扫描的索引是主索引时。
这个 rows 列还显示这个索引扫描正在检查>355k行(因为这个数字只是一个粗略的估计,实际上它正在检查所有的400k行)。
有索引吗 t.manufacture ? 我看到两个索引在 possible keys 这可能包括该列(我不能完全根据索引的名称来确定),但由于某些原因,优化器没有使用它们。可能您搜索的值集与表中的每一行都匹配。
如果 manufacture 值是为了匹配表的一个子集,那么您可能需要给优化器一个提示,使其使用最佳索引。https://dev.mysql.com/doc/refman/5.6/en/index-hints.html
使用 LIKE '%word%' 模式匹配不能使用索引,必须计算每一行的模式匹配。请看我的演示文稿,全文搜索throwdown。
你房间里有多少东西 IN(...) 列表?mysql有时会遇到很长列表的问题。看到了吗https://dev.mysql.com/doc/refman/5.6/en/range-optimization.html#equality-范围优化
p、 当你问一个查询优化问题时,你应该总是包括 SHOW CREATE TABLE 查询中引用的每个表的输出,所以回答的人不必猜测您当前有哪些索引、数据类型和约束。

k3fezbri

k3fezbri5#

如果我的猜测是正确的 cars , bikes ,和 trucks 相互独立(即,特定的预聚合结果将只包含其中一个的数据)。最好合并三个更简单的子查询(每个子查询一个)。
虽然你不能做很多索引明智的喜欢涉及领先的通配符;将其拆分为联合查询可以避免计算 p.fb_message LIKE '%Europe%' OR p.fb_from_name LIKE '%Europe% 对于所有的 cars 以及 bikes 火柴,还有 c 所有项目的条件 b 以及 t 火柴,等等。

k75qkfdt

k75qkfdt6#

首先:您的查询对字符串内容进行了假设,但它不应该这样做。什么可能 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;

索引:
跟踪(制造商、上次活动日期、id)
跟踪\u项(跟踪\u id、跟踪\u类型、跟踪\u对象\u id)
车辆(车辆文本、车辆id)
自行车(自行车文本,自行车id)
卡车(卡车文本,卡车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;

相关问题