高效的 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
。
每秒几次,dealerships
和cars
中的数据更新,需要实时通知我更新的顺序。
我需要跨多个列的复杂排序,并且在cars
上已经有一个复合索引,例如:
(`topSpeed` ASC, `heatedSeats` DESC, `mileage` DESC, `doors` ASC, `winterTyresIncluded` DESC)
我一个人就可以在cars
上高效查询,太好了。
但是,我需要通过dealerships
来group
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 与大型表上的依赖子查询的主要内容,如果未能解决你的问题,请参考以下文章
如何使用具有多个 GROUP BY、子查询和 WHERE IN 在大表上的查询来优化查询?