【问题标题】:Multiple Row_Number() Calls in a Single SQL Query单个 SQL 查询中的多个 Row_Number() 调用
【发布时间】:2023-03-06 19:31:02
【问题描述】:

我正在尝试设置一些数据来计算 SQL Server 2008 中的多个中位数,但我遇到了性能问题。现在,我正在使用这个pattern([另一个例子bottom)。是的,我没有使用 CTE,但是使用 CTE 并不能解决我遇到的问题,而且性能很差,因为 row_number 子查询是串行运行的,而不是并行运行的。

这是一个完整的例子。在 SQL 下我更详细地解释了这个问题。

-- build the example table    

CREATE TABLE #TestMedian (
    StateID INT,
    TimeDimID INT,
    ConstructionStatusID INT,

    PopulationSize BIGINT,
    SquareMiles BIGINT
);

INSERT INTO #TestMedian (StateID, TimeDimID, ConstructionStatusID, PopulationSize, SquareMiles)
VALUES (1, 1, 1, 100000, 200000);

INSERT INTO #TestMedian (StateID, TimeDimID, ConstructionStatusID, PopulationSize, SquareMiles)
VALUES (1, 1, 1, 200000, 300000);

INSERT INTO #TestMedian (StateID, TimeDimID, ConstructionStatusID, PopulationSize, SquareMiles)
VALUES (1, 1, 1, 300000, 400000);

INSERT INTO #TestMedian (StateID, TimeDimID, ConstructionStatusID, PopulationSize, SquareMiles)
VALUES (1, 1, 1, 100000, 200000);

INSERT INTO #TestMedian (StateID, TimeDimID, ConstructionStatusID, PopulationSize, SquareMiles)
VALUES (1, 1, 1, 250000, 300000);

INSERT INTO #TestMedian (StateID, TimeDimID, ConstructionStatusID, PopulationSize, SquareMiles)
VALUES (1, 1, 1, 350000, 400000);

--TruNCATE TABLE TestMedian

    SELECT
        StateID
        ,TimeDimID
        ,ConstructionStatusID
        ,NumberOfRows = COUNT(*) OVER (PARTITION BY StateID, TimeDimID, ConstructionStatusID)
        ,PopulationSizeRowNum = ROW_NUMBER() OVER (PARTITION BY StateID, TimeDimID, ConstructionStatusID ORDER BY PopulationSize)
        ,SquareMilesRowNum = ROW_NUMBER() OVER (PARTITION BY StateID, TimeDimID, ConstructionStatusID ORDER BY SquareMiles)
        ,PopulationSize
        ,SquareMiles
    INTO #MedianData
    FROM #TestMedian

    SELECT MinRowNum = MIN(PopulationSizeRowNum), MaxRowNum = MAX(PopulationSizeRowNum), StateID, TimeDimID, ConstructionStatusID, MedianPopulationSize= AVG(PopulationSize) 
    FROM #MedianData T
    WHERE PopulationSizeRowNum IN((NumberOfRows + 1) / 2, (NumberOfRows + 2) / 2)
    GROUP BY StateID, TimeDimID, ConstructionStatusID

    SELECT MinRowNum = MIN(SquareMilesRowNum), MaxRowNum = MAX(SquareMilesRowNum), StateID, TimeDimID, ConstructionStatusID, MedianSquareMiles= AVG(SquareMiles) 
    FROM #MedianData T
    WHERE SquareMilesRowNum IN((NumberOfRows + 1) / 2, (NumberOfRows + 2) / 2)
    GROUP BY StateID, TimeDimID, ConstructionStatusID


    DROP TABLE #MedianData
    DROP TABLE #TestMedian

此查询的问题在于 SQL Server 以串行方式而不是并行方式执行两个“ROW__NUMBER() OVER...”子查询。所以如果我有 10 个这样的 ROW__NUMBER 计算,它会一个接一个地计算它们,我得到线性增长,这很糟糕。我有一个运行此查询的 8 路 32GB 系统,我希望有一些并行性。我正在尝试在 5,000,000 行表上运行此类查询。

我可以通过查看查询计划并查看同一执行路径中的排序来告诉它这样做(在 SO 上显示查询计划的 XML 并不能很好地工作)。

所以我的问题是:如何更改此查询以便 ROW_NUMBER 查询并行执行?有没有一种完全不同的技术可以用来为多个中值计算准备数据?

【问题讨论】:

  • +1,足够的代码可以在我的系统上试用!!
  • +1,因为我不知道您可以在排名函数之外使用 OVER 子句——在 SQL 2005 中也是如此。哇!
  • Philip:对于普通的聚合函数,只有 PARTITION BY 子句,而不是 ORDER BY 部分 :-(
  • @RBarry:无论输入的顺序如何,AVG、SUM、COUNT、MAX、MIN 等都应该给出相同的结果。
  • Remus:ORDER BY 部分意味着顺序聚合。换句话说,SUM(..) OVER(ORDER BY id) 将产生运行总计(根据 SQL 标准)。不幸的是,SQL Server 没有实现它。

标签: sql sql-server tsql


【解决方案1】:

每个 ROW_NUMBER 都要求首先对行进行排序。由于您的两个 RN 具有不同的 ORDER BY 条件,因此查询必须生成结果,然后为第一个 RN 排序(可能已经排序),生成 RN,然后为第二个 RN 排序并生成第二个 RN 结果。根本没有任何神奇的小精灵可以在不计算行在所需顺序中的位置的情况下实现行数值。

【讨论】:

  • 我知道没有可用的魔法精灵粉,世界范围内都存在短缺。 :) 我知道如果没有先订购它,它无法弄清楚 RN 是什么。我该如何设置它,以便它以不同的方式并行排序以计算 RN?有没有一种技术可以将其分解为多个查询,然后加入结果集?我不喜欢使用 RN 风格,所以任何建设性的想法都会受到赞赏。我不可能成为世界上第一个想要获取一组数据并一次有效地计算多个中位数的人!为此,必须以不同的方式对数据进行排序。
  • 对于超过 8 个不同订单的 row_numbers 以及按要求进行分区真的很难。即使有可能被并行化的子查询,它们也不太可能。并行选项可用作对单个操作(如表扫描)的分区执行进行分区的选项,而不是用于拆分多个不同的子查询。我会重新审视要求并重新考虑对所有 row_numbers 的需求......
  • 不幸的是,计算中位数需要对数据进行排序。 Row_Number 只是告诉您这些数据是如何为给定字段排序的。感谢到目前为止的帮助...
【解决方案2】:

我不确定它是否可以并行化,因为它需要进行非分区(wrt 人口与平方英里)扫描。它们会与磁盘上的每一个发生冲突,因此它必须至少将所有内容放入内存一次,首先,如果它足够大,它可能有资格进行并行化。

无论如何,对我来说,以下的执行速度要快得多(40%):

;WITH cte AS (
    SELECT
        StateID
        ,TimeDimID
        ,ConstructionStatusID
        ,NumberOfRows = COUNT(*) OVER (PARTITION BY StateID, TimeDimID, ConstructionStatusID)
        ,PopulationSizeRowNum = ROW_NUMBER() OVER (PARTITION BY StateID, TimeDimID, ConstructionStatusID ORDER BY PopulationSize)
        ,SquareMilesRowNum = ROW_NUMBER() OVER (PARTITION BY StateID, TimeDimID, ConstructionStatusID ORDER BY SquareMiles)
        ,PopulationSize
        ,SquareMiles
    FROM TestMedian
)
, ctePop AS (
    SELECT MinPopNum = MIN(PopulationSizeRowNum)
    , MaxPopNum = MAX(PopulationSizeRowNum)
    , StateID, TimeDimID, ConstructionStatusID
    , MedianPopulationSize= AVG(PopulationSize) 
    FROM cte T
    WHERE PopulationSizeRowNum IN((NumberOfRows + 1) / 2, (NumberOfRows + 2) / 2)
    GROUP BY StateID, TimeDimID, ConstructionStatusID
)
, cteSqM AS (
    SELECT MinSqMNum = MIN(SquareMilesRowNum)
    , MaxSqMNum = MAX(SquareMilesRowNum)
    , StateID, TimeDimID, ConstructionStatusID
    , MedianSquareMiles= AVG(SquareMiles) 
    FROM cte T
    WHERE SquareMilesRowNum IN((NumberOfRows + 1) / 2, (NumberOfRows + 2) / 2)
    GROUP BY StateID, TimeDimID, ConstructionStatusID
)
SELECT s.StateID, s.TimeDimID, s.ConstructionStatusID
, MinPopNum, MaxPopNum, MedianPopulationSize
, MinSqMNum, MaxSqMNum, MedianSquareMiles
FROM ctePop p
JOIN cteSqM s ON s.StateID = p.StateID
    AND s.TimeDimID = p.TimeDimID
    AND s.ConstructionStatusID = p.ConstructionStatusID

此外,一旦它们变得足够大,它们本身应该被并行化。不过,您需要至少 100,000 行测试才能发生这种情况。


好的,是的,在我用这个语句加载足够多之后,我得到了并行性:

INSERT INTO TestMedian 
SELECT abs(id)%3,abs(id)%2,abs(id)%5, abs(id), colid * 10000
  From master.sys.syscolumns, (select top 10 * from master.dbo.spt_values)a

【讨论】:

  • 谢谢。我现在正在我的实际数据集上测试这种方法,以查看行数是否被并行化。在一个小子集上,它看起来很有希望。
【解决方案3】:

一些横向思考:如果您经常和/或快速需要这些数据,并且基础数据集不经常更改(对于相当高的“频繁”值),您是否可以预先计算任何这些值并将它们存储在某种形式的预聚合表?

(是的,这是去规范化,但如果您需要性能高于一切,则值得考虑。)

【讨论】:

  • 我的意思是说“非规范化”。诚实。
  • 我相信你:)。不幸的是,我在这里没有看到预聚合步骤。在此示例中,人口规模分布在一组维度上。对于每组维度,我需要找到人口规模的中值。我能想到的唯一预聚合是用标识符替换单个维度,以便在更少的列上完成分区、分组和连接(可能真的很值得)。
猜你喜欢
  • 2015-10-26
  • 1970-01-01
  • 1970-01-01
  • 2015-02-21
  • 1970-01-01
  • 1970-01-01
  • 2010-11-19
  • 2014-06-08
  • 1970-01-01
相关资源
最近更新 更多