如何编写加总一列以创建离散存储桶的窗口查询?


11

我有一个表,其中包含一列十进制值,例如:

id value size
-- ----- ----
 1   100  .02
 2    99  .38
 3    98  .13
 4    97  .35
 5    96  .15
 6    95  .57
 7    94  .25
 8    93  .15

我需要完成的工作有点难以描述,因此请耐心等待。我正在尝试做的是创建size列的聚合值,每当前一行的总和为1时(根据降序排列),该列的值就会递增1 value。结果看起来像这样:

id value size bucket
-- ----- ---- ------
 1   100  .02      1
 2    99  .38      1
 3    98  .13      1
 4    97  .35      1
 5    96  .15      2
 6    95  .57      2
 7    94  .25      2
 8    93  .15      3

我天真的尝试是保持运行SUM,然后再保持CEILING该值,但是它不能解决某些记录size最终导致两个单独的存储桶总计的情况。下面的示例可以阐明这一点:

id value size crude_sum crude_bucket distinct_sum bucket
-- ----- ---- --------- ------------ ------------ ------
 1   100  .02       .02            1          .02      1
 2    99  .38       .40            1          .40      1
 3    98  .13       .53            1          .53      1
 4    97  .35       .88            1          .88      1
 5    96  .15      1.03            2          .15      2
 6    95  .57      1.60            2          .72      2
 7    94  .25      1.85            2          .97      2
 8    93  .15      2.00            2          .15      3

如您所见,如果我仅CEILINGcrude_sum记录#8 上使用,则会将其分配给存储桶2。这是由于size记录#5和#8被拆分到两个存储桶中引起的。取而代之的是,理想的解决方案是在每次总和达到1时重置总和,然后使该bucket列递增并从当前记录SUMsize值开始进行新操作。因为记录的顺序对于此操作很重要,所以我包括了该value列,该列旨在按降序排序。

我最初的尝试涉及对数据进行多次传递,一次执行SUM操作,再执行一次操作CEILING,等等。这是我创建crude_sum列的示例:

SELECT
  id,
  value,
  size,
  (SELECT TOP 1 SUM(size) FROM table t2 WHERE t2.value<=t1.value) as crude_sum
FROM
  table t1

在用于UPDATE将值插入表中以供以后使用的操作中使用的值。

编辑:我想在解释这一点时再做一次尝试,所以就到这里。想象每个记录是一个物理项目。该项目具有与之关联的值,并且物理尺寸小于1。我有一系列容量恰好为1的存储桶,我需要根据所需物品的值(从最高到最低)确定要使用多少个存储桶,以及每个项目进入哪个存储桶。

物理项目不能同时存在于两个位置,因此必须位于一个存储桶或另一个存储桶中。这就是为什么我无法执行运行中的total + CEILING解决方案的原因,因为那会使记录将其大小贡献给两个存储桶。


您应该添加您的SQL,以明确您最初尝试的内容。
mdahlman 2013年

您是否要根据要计算的存储桶汇总数据,还是存储桶编号是您要寻找的最终答案?
乔恩·塞格尔

2
阿克 我可能会选择一个客户端应用程序,因为它将支持更好的记录流式传输,而不是一次读取一行的游标循环。我认为,只要所有更新都是分批完成的,它的性能应该会很好。
乔恩·塞格尔

1
正如其他人已经提到的那样,对存储桶的需求distinct_count使事情变得复杂。Aaron Bertrand 对此类窗口工作在SQL Server上的选择进行了很好的总结。我使用了“古怪的更新”方法来计算distinct_sum,您可以在SQL Fiddle上看到此方法,但这是不可靠的。
Nick Chammas 2013年

1
@JonSeigel我们应该注意,使用SQL语言的逐行算法无法有效解决将X个项目放置在最小数量的存储桶中的问题。例如大小为0.7; 0.8; 0.3的商品将需要2个存储桶,但如果按ID排序,则将需要3个存储桶。
Stoleg 2013年

Answers:


9

我不确定您要寻找哪种类型的性能,但是如果不能选择CLR或外部应用程序,则只剩下一个游标。在我的旧笔记本电脑上,我使用以下解决方案在大约100秒内浏览了1,000,000行。它的好处是它可以线性缩放,因此我将花费大约20分钟的时间来浏览整个过程。使用一台像样的服务器,您会更快,但幅度不大,因此仍然需要几分钟才能完成。如果这是一个一次性的过程,那么您可能可以负担得起缓慢性。如果您需要定期将其作为报表或类似报表运行,则可能需要将值存储在同一表中,并在添加新行时(例如在触发器中)更新它们。

无论如何,这是代码:

IF OBJECT_ID('dbo.MyTable') IS NOT NULL DROP TABLE dbo.MyTable;

CREATE TABLE dbo.MyTable(
 Id INT IDENTITY(1,1) PRIMARY KEY CLUSTERED,
 v NUMERIC(5,3) DEFAULT ABS(CHECKSUM(NEWID())%100)/100.0
);


MERGE dbo.MyTable T
USING (SELECT TOP(1000000) 1 X FROM sys.system_internals_partition_columns A,sys.system_internals_partition_columns B,sys.system_internals_partition_columns C,sys.system_internals_partition_columns D)X
ON(1=0)
WHEN NOT MATCHED THEN
INSERT DEFAULT VALUES;

--SELECT * FROM dbo.MyTable

DECLARE @st DATETIME2 = SYSUTCDATETIME();
DECLARE cur CURSOR FAST_FORWARD FOR
  SELECT Id,v FROM dbo.MyTable
  ORDER BY Id;

DECLARE @id INT;
DECLARE @v NUMERIC(5,3);
DECLARE @running_total NUMERIC(6,3) = 0;
DECLARE @bucket INT = 1;

CREATE TABLE #t(
 id INT PRIMARY KEY CLUSTERED,
 v NUMERIC(5,3),
 bucket INT,
 running_total NUMERIC(6,3)
);

OPEN cur;
WHILE(1=1)
BEGIN
  FETCH NEXT FROM cur INTO @id,@v;
  IF(@@FETCH_STATUS <> 0) BREAK;
  IF(@running_total + @v > 1)
  BEGIN
    SET @running_total = 0;
    SET @bucket += 1;
  END;
  SET @running_total += @v;
  INSERT INTO #t(id,v,bucket,running_total)
  VALUES(@id,@v,@bucket, @running_total);
END;
CLOSE cur;
DEALLOCATE cur;
SELECT DATEDIFF(SECOND,@st,SYSUTCDATETIME());
SELECT * FROM #t;

GO 
DROP TABLE #t;

它删除并重新创建表MyTable,将其填充1000000行,然后开始工作。

在运行计算时,光标会将每一行复制到临时表中。最后,选择返回计算结果。如果您不复制数据,而是进行就地更新,则可能会更快一些。

如果您可以选择升级到SQL 2012,则可以查看新的窗口后台处理支持的移动窗口聚合,这将为您提供更好的性能。

附带说明一下,如果您安装的程序集的权限设置为“ safety_set = safe”,则与标准程序集相比,使用标准T-SQL可以对服务器执行更多的操作,因此,我将继续努力消除这种障碍-您有很好的用途在这种情况下,CLR确实可以为您提供帮助。


我接受此代码是因为实现起来很容易,并且以后可以根据需要轻松更改和调试它。@NickChammas的答案也是正确的,并且运行效率可能更高,所以我想这是其他遇到类似问题的人的偏爱。
Zikes

9

缺少SQL Server 2012中的新窗口功能,可以使用递归CTE来完成复杂的窗口。我不知道这对数百万行的性能如何。

以下解决方案涵盖了您描述的所有情况。您可以在此处的SQL Fiddle上看到它的运行情况。

-- schema setup
CREATE TABLE raw_data (
    id    INT PRIMARY KEY
  , value INT NOT NULL
  , size  DECIMAL(8,2) NOT NULL
);

INSERT INTO raw_data 
    (id, value, size)
VALUES 
   ( 1,   100,  .02) -- new bucket here
 , ( 2,    99,  .99) -- and here
 , ( 3,    98,  .99) -- and here
 , ( 4,    97,  .03)
 , ( 5,    97,  .04)
 , ( 6,    97,  .05)
 , ( 7,    97,  .40)
 , ( 8,    96,  .70) -- and here
;

现在深吸一口气。这里有两个关键的CTE,每个CTE前面都有简短的评论。其余的只是“清理” CTE,例如,在对它们进行排名之后,可以拉出正确的行。

-- calculate the distinct sizes recursively
WITH distinct_size AS (
  SELECT
      id
    , size
    , 0 as level
  FROM raw_data

  UNION ALL

  SELECT 
      base.id
    , CAST(base.size + tower.size AS DECIMAL(8,2)) AS distinct_size
    , tower.level + 1 as level
  FROM 
                raw_data AS base
    INNER JOIN  distinct_size AS tower
      ON base.id = tower.id + 1
  WHERE base.size + tower.size <= 1
)
, ranked_sum AS (
  SELECT 
      id
    , size AS distinct_size
    , level
    , RANK() OVER (PARTITION BY id ORDER BY level DESC) as rank
  FROM distinct_size  
)
, top_level_sum AS (
  SELECT
      id
    , distinct_size
    , level
    , rank
  FROM ranked_sum
  WHERE rank = 1
)
-- every level reset to 0 means we started a new bucket
, bucket AS (
  SELECT
      base.id
    , COUNT(base.id) AS bucket
  FROM 
               top_level_sum base
    INNER JOIN top_level_sum tower
      ON base.id >= tower.id
  WHERE tower.level = 0
  GROUP BY base.id
)
-- join the bucket info back to the original data set
SELECT
    rd.id
  , rd.value
  , rd.size
  , tls.distinct_size
  , b.bucket
FROM 
             raw_data rd
  INNER JOIN top_level_sum tls
    ON rd.id = tls.id
  INNER JOIN bucket   b
    ON rd.id = b.id
ORDER BY
  rd.id
;

该解决方案假定这id是一个无间隙序列。如果不是这样,您将需要通过在开头添加一个额外的CTE来生成自己的无缝序列,该CTE会ROW_NUMBER()根据所需顺序(例如ROW_NUMBER() OVER (ORDER BY value DESC))对行进行编号。

Fankly,这很冗长。


1
该解决方案似乎无法解决行可能将其大小分配给多个存储桶的情况。滚动总和很容易,但是我需要在每次达到1时重置该总和。请参阅问题中的最后一个示例表,并crude_sumdistinct_sum及其关联的bucket列进行比较以了解我的意思。
Zikes 2013年

2
@Zikes-我已经用更新的解决方案解决了这种情况。
Nick Chammas 2013年

看起来现在应该可以正常工作了。我将把它集成到数据库中进行测试。
Zikes 2013年

@Zikes-很好奇,此处发布的各种解决方案如何针对您的大数据集执行?我猜Andriy是最快的。
Nick Chammas 2013年

5

这似乎是一个愚蠢的解决方案,并且可能无法很好地扩展,因此请仔细测试是否使用它。由于主要问题来自存储桶中剩余的“空间”,因此我首先必须创建一个填充记录以合并到数据中。

with bar as (
select
  id
  ,value
  ,size
  from foo
union all
select
  f.id
  ,value = null
  ,size = 1 - sum(f2.size) % 1
  from foo f
  inner join foo f2
    on f2.id < f.id
  group by f.id
    ,f.value
    ,f.size
  having cast(sum(f2.size) as int) <> cast(sum(f2.size) + f.size as int)
)
select
  f.id
  ,f.value
  ,f.size
  ,bucket = cast(sum(b.size) as int) + 1
  from foo f
  inner join bar b
    on b.id <= f.id
  group by f.id
    ,f.value
    ,f.size

http://sqlfiddle.com/#!3/72ad4/14/0


1
+1我认为如果存在适当的索引,这就有潜力。
乔恩·塞格尔

3

以下是另一个递归CTE解决方案,尽管我会说它比@Nick的建议更直接。实际上,它更接近@Sebastian的光标,只有我使用运行差异而不是运行总计。(起初,我什至以为@Nick的答案将与我在这里建议的内容一致,并且是在得知他实际上是一个非常不同的查询之后,我决定提供我的答案。)

WITH rec AS (
  SELECT TOP 1
    id,
    value,
    size,
    bucket        = 1,
    room_left     = CAST(1.0 - size AS decimal(5,2))
  FROM atable
  ORDER BY value DESC
  UNION ALL
  SELECT
    t.id,
    t.value,
    t.size,
    bucket        = r.bucket + x.is_new_bucket,
    room_left     = CAST(CASE x.is_new_bucket WHEN 1 THEN 1.0 ELSE r.room_left END - t.size AS decimal(5,2))
  FROM atable t
  INNER JOIN rec r ON r.value = t.value + 1
  CROSS APPLY (
    SELECT CAST(CASE WHEN t.size > r.room_left THEN 1 ELSE 0 END AS bit)
  ) x (is_new_bucket)
)
SELECT
  id,
  value,
  size,
  bucket
FROM rec
ORDER BY value DESC
;

注意:此查询假定该value列由不带空格的唯一值组成。如果不是这种情况,则需要基于的降序引入一个计算的排名列,value并将其用于递归CTE中,而不是value将递归部分与锚点连接起来。

可以在此处找到此查询的SQL Fiddle演示。


这比我写的要短得多。辛苦了 您是否有理由倒数剩余的空间而不是增加空间?
Nick Chammas 2013年

是的,不过,不确定我最终在此处发布的版本是否有意义。无论如何,原因是它似乎更容易/更自然具有单个值(以比较单个值sizeroom_left),而不是与(表达式进行比较的单个值1running_size+ size)。我最初没有使用is_new_bucket标志,而是使用了几个标志CASE WHEN t.size > r.room_left ...(“几个”,因为我也正在计算(并返回)总大小,但为了简单起见对此进行了考虑),所以我认为它会更优雅那样。
Andriy M
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.