【问题标题】:How do I optimize a "latest sales" sql query?如何优化“最新销售”sql 查询?
【发布时间】: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 等)哪个版本?

标签: sql-server-2008 query-optimization


【解决方案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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-24
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多