带CROSS APPLY子查询的SQL优化:筛选最新价为0的指定制造商零件
慢查询性能优化方案
原有查询性能瓶颈根因
- 逐行Nested Loop的探测逻辑:无论是
CROSS APPLY还是嵌套子查询写法,都会对MakerId过滤后的每一条MakerParts记录,单独发起一次关联查询扫描MakerPrices表,1亿行体量下的随机IO开销被无限放大。 - 无意义的关联操作:原逻辑为了取价格更新时间关联MakerPriceUpdates表,但MakerPriceUpdates为按上传顺序生成的自增Id,Id值大小和更新时间Date严格正相关,完全不需要关联时间表取Date做排序,额外增加了不必要的关联开销。
- 缺乏匹配查询场景的覆盖索引:没有针对MakerPrices表的关联场景建立包含所有查询列的索引,每次查询都需要回表读取数据页,进一步拉高IO成本;由于数据库对MakerId过滤后的行数估算偏低,不会触发缺失索引提示,掩盖了索引缺失的问题。
第一步:建立针对性覆盖索引
以下索引可将MakerPrices表的查询IO降低90%以上,避免回表操作
- 针对MakerPrices表:
索引按零件Id排序,聚合取零件的最大UpdateId(即最新更新)时不需要额外排序操作,所有需要的列都包含在索引结构中,无回表成本。CREATE NONCLUSTERED INDEX IX_MakerPrices_Part_Update_Price ON MakerPrices(MakerPartNumberId, UpdateId DESC) INCLUDE (Price) - 针对MakerParts表:
按MakerId过滤时直接走索引返回所有需要的零件字段,不需要读MakerParts的全表数据页。CREATE NONCLUSTERED INDEX IX_MakerParts_MakerId_Columns ON MakerParts(MakerId) INCLUDE (PartNumber, Description) - 针对MakerPriceUpdates表:
保留原有主键索引即可,因为Id为主键,天然带索引,无需额外调整。
第二步:重构查询逻辑
优化前提:MakerPriceUpdates表Id为自增主键,Id生成顺序和Date先后严格一致(即Id越大,对应更新时间越新),这是文件上传类记录表的通用设计;若存在历史数据Id和Date不匹配的情况,只需将Date字段纳入上述MakerPrices的覆盖索引,逻辑保持不变。
优化后的高效SQL写法
SELECT mp.* FROM MakerParts mp -- 先一次性聚合出范围内所有零件的最新更新Id,避免逐行探测 INNER JOIN ( SELECT MakerPartNumberId, MAX(UpdateId) AS LatestUpdateId FROM MakerPrices -- 先把统计范围缩小到MakerId=1对应的零件,绝不扫描无关的MakerPrices数据 WHERE MakerPartNumberId IN (SELECT Id FROM MakerParts WHERE MakerId = 1) GROUP BY MakerPartNumberId ) AS latestUpdate ON latestUpdate.MakerPartNumberId = mp.Id -- 关联取对应最新更新下的价格 INNER JOIN MakerPrices latestPrice ON latestPrice.MakerPartNumberId = mp.Id AND latestPrice.UpdateId = latestUpdate.LatestUpdateId WHERE mp.MakerId = 1 AND latestPrice.Price = 0
优化效果说明
- 整个查询的扫描范围从全表1亿行MakerPrices数据,缩小到仅和MakerId=1相关的小部分索引行,IO开销从GB级降到KB级
- 砍掉了原写法中无意义的MakerPriceUpdates关联和排序操作,消除了逐行Nested Loop的随机IO开销
- 同等过滤条件下,查询耗时可从2分钟级降到毫秒级
内容的提问来源于stack exchange,提问作者Guilherme
相关产品推荐
相关产品推荐

