如何优化“最新销售”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 查询?的主要内容,如果未能解决你的问题,请参考以下文章

优化SQL查询:如何写出高性能SQL语句

优化SQL查询:如何写出高性能SQL语句

如何优化这个嵌套的 SQL 查询

如何优化我的 SQL 查询?

sql查询速度慢如何优化

数据库牛人是如何进行SQL优化的?