如何优化“最新销售”sql 查询?
Posted
技术标签:
【中文标题】如何优化“最新销售”sql 查询?【英文标题】:How do I optimize a "latest sales" sql query? 【发布时间】:2011-06-01 20:00:28 【问题描述】:在过去的几年里,这个查询已经成为我的克星,因为我从来没有找到优化它的方法。现在我的克星变成了你的克星! :)
考虑下表:
create table Sales (
SaleId int identity(1,1) primary key,
SalesmanId int not null,
Amount smallmoney not null
)
为了论证起见,假设这张表有10^100行(生意兴隆),因此不可能进行表扫描。
现在我们要确定每个销售员最近一次销售的 SaleId。很简单,对吧?这是查询:
select
SalesmanId,
max(SaleId) SaleId
from Sales
group by Sales.SalesmanId
当我们运行这个查询时,查询优化器会进行全表扫描,这是意料之中的,因为它无法知道每个销售员的销售额在表中的哪个位置。因此,让我们通过添加以下索引来帮助它:
create unique nonclustered index IX_Sales on Sales
(
SalesmanId asc,
SaleId asc
)
现在找到最近的值应该是微不足道的(无论如何对于人类来说),因为我们使用索引的第一列的值来识别所有可能的推销员,并使用第二列的最后一个条目来定位每个推销员的最新销售。不幸的是,在这种情况下,查询优化器仍然对整个索引(所有 10^100 行)进行索引查找,所以它需要的时间一样长。
有趣的是,如果我们编写查询来查找给定推销员的最新销售,
select max(SaleId)
from Sales
where SalesmanId = 1
查询优化器在 IX_Sales 上使用索引查找并通过一行 I/O 获取它。即使没有 IX_Sales,它也会进行聚集索引扫描,以某种方式在一行 I/O 中获取它(也许使用表统计信息?)。但是如果我们将其修改为
select max(SaleId)
from Sales
where SalesmanId = 1
group by SalesmanId
或
select max(SaleId)
from Sales
group by SalesmanId
having SalesmanId = 1
我们又回到了对大量行的高行数索引搜索(尽管比完全省略过滤器的情况要少,同样可能是由于统计数据)。
那么...关于如何打败我的克星有什么想法吗?
更新
有些人建议加入可能的 SalesmanId 值表,像这样
select Latest.*
from
(
select
SalesmanId,
max(SaleId) SaleId
from Sales
group by SalesmanId
) Latest
inner join Salesmen on
Salesmen.SalesmanId = Latest.SalesmanId
我测试了这个想法,但查询优化器仍然选择进行全表扫描。
【问题讨论】:
你的数据库引擎是什么? (SQL Server、mysql、PostgreSQL 等)哪个版本? 【参考方案1】:这是一个与您的光标解决方案采用类似方法的解决方案。
SELECT
salesmanId,
(SELECT MAX(saleid) FROM sales WHERE salesmanid = salesmen.salesmanId) AS MaxSaleId
FROM salesmen
执行计划显示它正在对销售表使用搜索。
【讨论】:
【参考方案2】:跳出框框思考。每当发生销售时,更新 salesman 表中的列以引用最近的 saleid。我们都陷入了正常化陷阱。有时最好是多余的。请参阅 CQRS 以将其发挥到极致。
希望这会有所帮助。
【讨论】:
更新一列以跟踪最近的 SaleId 只有在有人要求稍微更改查询时才有帮助(即“每个销售员的最新销售额是多少,金额大于 1,000 美元?”或“什么是过去 12 个月每个推销员的最新销售额?”)。我正在寻找一种更通用的方法,可用于与此类似的一整类查询。【参考方案3】:因为你这样说:
select max(SaleId)
from Sales
where SalesmanId = 1
很快,但分组不是...尝试将特定查询放入视图中,然后 SELECT
all the salesman 和 JOIN
视图。
这应该强制每个JOIN
的视图上的查询计划。通常我认为这种方法不会是最有效的,但考虑到您的查询是如何处理的,它可能会起作用。
【讨论】:
我刚刚尝试过,但得到了相同的结果。我对查询优化器的体验是,它会在优化之前将所有引用的视图组合成一个大查询,所以我认为你不能用这种方式欺骗它。【参考方案4】:如果您按 SalesmanID 分区(使用适当的每表索引和表上的 CHECK 约束),优化器会做得更好吗??
【讨论】:
@Mike:如果您的优化器足够聪明,可以像人类一样处理分区表,那么它会很好地处理所有按销售员的查询。所以我认为该评论不适用。但是,我用 PG 9.0 测试了我的方法,并使用表继承进行分区,但它不起作用。如果您询问一张桌子,请索引。询问一位推销员,在正确的分区上进行表扫描,在哪里可以使用索引扫描+限制。我觉得这是优化器的错误功能。【参考方案5】:" 在 Sales 上创建唯一的非聚集索引 IX_Sales ( 推销员Id asc, 销售编号升序 )
现在它应该是微不足道的(对于人类来说, 无论如何)找到最新的值 因为我们使用第一个值 标识所有索引的列 可能的推销员和最后的条目 的第二列来定位每个 推销员的最新销售。很遗憾, 查询优化器仍然执行 索引查找整个索引(所有 10^100 行)在这种情况下,所以它需要 一样长。”
当然,但我敢打赌,计算机的速度仍然比人类快。
无论如何,请考虑这个其他索引声明:
create unique nonclustered index IX_Sales on Sales
(
SalesmanId asc,
SaleId DESC
)
现在 MAX(SaleId) 是每个销售员索引中的第一行。那应该快很多。您可能认为将整个索引用于解决一个查询是相当奢侈的,但有时需要采取绝望的措施来击败自己的克星!
我说只解决一个查询,因为此索引对您在评论中提到的其他查询没有帮助:
"每位推销员的最新销售额是多少 超过 1,000 美元的金额?”或 “每个推销员的最新销售额是多少 过去 12 个月的每个月?”
唉,在如此庞大的表格上,您无法为所有与日期相关的查询提供单一解决方案。解决这些问题是组织构建数据仓库的原因,这些数据仓库具有称为维度和事实表的巴洛克式结构,以及可以并行运行查询的大型 grunt 服务器。
【讨论】:
我刚试了一下,查询优化器仍然在寻找整个索引。不过,DESC 可能会使人类的速度更快。我感觉电脑还是会赢。【参考方案6】:好的,我将尝试回答我自己的问题,冒着冒犯整个 sql 社区的风险。
declare @Result table (
SalesmanId int not null primary key,
SaleId int not null
)
declare @SalesmanId int
declare Salesman cursor local fast_forward for
select SalesmanId
from Salesmen
open Salesman
fetch next from Salesman into @SalesmanId
while @@FETCH_STATUS = 0
begin
insert @Result (
SalesmanId,
SaleId
)
select
@SalesmanId SalesmanId,
max(SaleId) SaleId
from Sales
where SalesmanId = @SalesmanId
fetch next from Salesman into @SalesmanId
end
close Salesman
deallocate Salesman
select *
from @Result
在 cursors-are-bad 火焰开始之前,让我们考虑一下性能。问题的原始问题需要进行表扫描,其复杂性为 O(N),其中 N 是销售数量。由于查询优化器可以在恒定时间内找到给定推销员的答案,因此该建议解决方案的复杂性是 O(M),其中 M 是推销员的数量。假设 M
【讨论】:
以上是关于如何优化“最新销售”sql 查询?的主要内容,如果未能解决你的问题,请参考以下文章