高效的 ORDER BY 与大型表上的依赖子查询

Posted

技术标签:

【中文标题】高效的 ORDER BY 与大型表上的依赖子查询【英文标题】:Efficient ORDER BY with dependent subquery on large tables 【发布时间】:2020-07-07 15:55:21 【问题描述】:

使用 mysql 5.7(受 Google Cloud SQL 限制)。

我有一张大表(100,000 行),我们称之为cars

然后是一个相对较小的表(10,000 行),我们称之为dealerships

每秒几次,dealershipscars中的数据更新,需要实时通知我更新的顺序。

我需要跨多个列的复杂排序,并且在cars 上已经有一个复合索引,例如:

(`topSpeed` ASC, `heatedSeats` DESC, `mileage` DESC, `doors` ASC, `winterTyresIncluded` DESC)

我一个人就可以在cars上高效查询,太好了。

但是,我需要通过dealershipsgroup cars,为每个dealerships 选择最相关的cars。需要选择的cars 来订购最终结果。这是我的查询:

SELECT *
FROM `dealerships`
         INNER JOIN `cars` ON 
             `cars`.`dealershipId` = `dealerships`.`id` 
                 AND `cars`.`id` = (
                     SELECT id
                      FROM `cars`
                      WHERE `cars`.`status` IN ('pending')
                        AND `cars`.`dealershipId` = `dealerships`.`id`
                      ORDER BY `topSpeed` ASC,
                               `heatedSeats` DESC,
                               `mileage` DESC, `doors` ASC,
                               `winterTyresIncluded` DESC
                      LIMIT 1
                     )
WHERE `dealerships`.`isActive` = TRUE
ORDER BY `dealerships`.`updated` DESC, `cars`.`type` DESC, `dealerships`.`status` ASC, `cars`.`created` ASC
LIMIT 30;

这个查询对于我的实时数据库来说非常慢。 下面的EXPLAIN 来自我的本地环境:

*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: dealerships
   partitions: NULL
         type: range
possible_keys: PRIMARY,IDX_8b0666635781c2534cfdd3746c
          key: IDX_8b0666635781c2534cfdd3746c
      key_len: 36
          ref: NULL
         rows: 632
     filtered: 100.00
        Extra: Using where; Using filesort
*************************** 2. row ***************************
           id: 1
  select_type: PRIMARY
        table: cars
   partitions: NULL
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 36
          ref: func
         rows: 1
     filtered: 5.00
        Extra: Using where
*************************** 3. row ***************************
           id: 1
  select_type: PRIMARY
        table: cars
   partitions: NULL
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 36
          ref: func
         rows: 1
     filtered: 100.00
        Extra: Using where
*************************** 4. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: cars
   partitions: NULL
         type: ref
possible_keys: idx_cars_composite
          key: idx_cars_composite
      key_len: 74
          ref: database.dealerships.id,const
         rows: 1148
     filtered: 100.00
        Extra: Using where; Using index; Using filesort
4 rows in set, 2 warnings (0.18 sec)

如果外部查询的 ORDER BY 引用了连接表中的字段,我是否能够优化它? 为什么cars 上的依赖子查询使用上面的文件排序(EXPLAIN 中的4. 行)?独立运行时,它使用索引idx_cars_composite,没有文件排序。 我是否必须更改我的业务逻辑或数据库技术才能获得高效的结果?

【问题讨论】:

如果我们必须使用相关子查询,给定 WHERE 子句中的谓词,我想将dealershipId 作为复合索引中的前导列和 ORDER BY 中的第一列。如果状态列上的条件始终等于单个值“待定”,我希望它作为索引中的下一列和 ORDER BY。我的偏好是尝试重写查询以避免相关子查询,因为该子查询将针对外部查询返回的每一行(之前没有被过滤掉的每一行)执行 ...我之前评论的附录...看起来 idx_cars_composite 索引已经将经销商 ID 和状态作为前导列。我会在子查询中的 ORDER BY 的开头添加dealershipid 和状态列... 如果是我,我会重新开始。 meta.***.com/questions/333952/… 【参考方案1】:

问:我是否能够优化外部查询的 ORDER BY,因为它引用了连接表中的字段?

答: 不,除非重构表和/或查询设计。但我真的怀疑外部查询上的 ORDER BY 是否是最大的问题。我怀疑查询性能方面的“大石头”是相关子查询。

(从外部查询中删除 ORDER BY 的性能如何?)

在优化 ORDER BY 的性能方面,我们需要查看大小、字节数、排序键和行的整体大小。如果我们增加 sort_buffer_size 系统变量,我们可能能够避免排序溢出到磁盘。我们真的需要从经销商和汽车返回所有列吗?我们可以用我们实际需要返回的特定表达式列表替换惰性*,并缩短行吗?


问: 为什么汽车上的依赖子查询使用上面的文件排序(EXPLAIN 中的 4. 行)?独立运行时,它使用索引 idx_cars_composite 没有文件排序。

答: 看起来相关子查询正在使用索引 idx_cars_composite ,根据 EXPLAIN 的输出,它看起来索引中的前导列是经销商 ID 和状态。如果我试图避免使用文件排序,并让索引满足 ORDER BY,我会将索引的前导列包含在 ORDER BY 中,并使用出现在 ORDER BY 中的所有列定义复合索引子句,顺序相同。

   ORDER BY dealershipid
          , status
          , topspeed
          , heatedseats   DESC
          , mileage       DESC
          , doors
          , wintertyresincluded  DESC

和索引

 ON cars (dealershipid, status, topspeed, heatedseats, mileage, doors, wintertyresincluded, id)

(我不确定优化器如何在 ORDER BY 子句中处理 ASC 和 DESC 的混合,以及是否可以避免使用文件排序操作。我知道在 MySQL 5.7 中,ASC 和 DESC CREATE INDEX 语句中出现的关键字无效,索引始终按升序排列;在较新的版本中可能会有所不同。)

我的偏好是尝试重写查询以避免相关子查询的(可能代价高昂的)重复执行。


问:我是否必须改变我的业务逻辑或数据库技术才能获得高效的结果?

答:这个问题没有答案。

我们确定查询满足规范吗?

假设 id 是汽车的主键,看起来我们每个经销商只退回一辆汽车。

看起来外部查询正在获取所有活跃经销商的所有辆汽车,这可能是很多行。对于这些行中的每一行,正在执行子查询,这将是很多执行。 (不清楚优化器计划是否将子查询视为确定性,并避免重复调用相同的dealershipid。)

我建议测试重写查询。可能这样的事情可能会更快:

SELECT d.*
     , c.*
  FROM ( SELECT e.dealershipid
              , SUBSTRING_INDEX(
                  GROUP_CONCAT( e.id 
                    ORDER BY e.status
                           , e.topspeed
                           , e.heatedseats  DESC
                           , e.mileage  DESC
                           , e.doors
                           , e.wintertyresincluded  DESC
                    ,','
                    ,1
                  )
                ) AS carid
           FROM cars e
          WHERE e.status = 'pending'
          GROUP
             BY e.status
              , e.dealershipid
       ) r
  JOIN cars c
    ON c.id = r.carid
  JOIN dealerships d
    ON d.id = r.dealershipid
   AND d.isactive = TRUE
 ORDER
    BY d.isactive DESC
     , d.updated  DESC
     , c.type     DESC
     , d.status   ASC
     , c.created  ASC
 LIMIT 30

(注意:MySQL 5.7 不支持窗口/分析函数,因此我们使用 hack 进行模拟,以使用 GROUP_CONCAT( ... ORDER BY ...) 按顺序获取 id 值并使用 SUBSTRING_INDEX 提取第一个 id value, ) 这个查询对于大型集合来说会很昂贵。

另请注意,在 MySQL 5.7 中,索引定义中的 ASC 和 DESC 关键字被忽略;索引按升序存储。

我们假设 status = 'pending' 代表相对较小的汽车子集;再次,我想要一个以状态和经销商 ID 作为前导列的索引(以匹配等式谓词和 GROUP BY)

【讨论】:

最终使用了您的建议和附加业务逻辑的组合,该逻辑在 car 插入/更新时计算“每个经销商最相关的汽车”,因为缺少窗口函数和组 concat 的开销等。

以上是关于高效的 ORDER BY 与大型表上的依赖子查询的主要内容,如果未能解决你的问题,请参考以下文章

连接表上的 ORDER BY

如何使用具有多个 GROUP BY、子查询和 WHERE IN 在大表上的查询来优化查询?

提高大型表上的 SQL Server 查询性能

基于日期时间值的相同表上的高效sql子查询

在 group by 中使用 datetime 日期并在单个 SELECT 中使用 order by 与使用子查询

更新大型表上的行的最高效方法