列を合計して個別のバケットを作成するウィンドウクエリを作成するにはどうすればよいですか?


11

次のような10進値の列を含むテーブルがあります。

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列の集計値を作成することvalueです。これは、に従って降順で、前の行の合計が1になるたびに1ずつ増加します。結果は次のようになります。

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が2つの別々のバケットの合計の原因となるケースには対応していません。以下の例はこれを明確にするかもしれません:

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が2つのバケットに分割されていることが原因です。代わりに、理想的な解決策は、合計が1に達するたびに合計をリセットすることです。これにより、bucket列が増分さSUMsize、現在のレコードの値から新しい操作が開始されます。この操作ではレコードの順序が重要valueであるため、降順でソートすることを目的とした列を含めました。

私の最初の試みは、データに対して複数回のパスを実行することを含みましたSUM。1回は操作を実行し、もう1回はそれを実行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の一連のバケットがあり、これらのバケットのうちいくつが必要か、およびアイテムの値に応じて各アイテムが入るバケットを、高いものから低いものへとソートする必要があります。

物理アイテムは一度に2か所に存在することはできないため、どちらか一方のバケットに存在する必要があります。これが、実行中の合計+ CEILINGソリューションを実行できない理由です。これにより、レコードが2つのバケットにサイズを提供できるようになります。


SQLを追加して、最初の試みに何が含まれているかを明確にする必要があります。
mdahlman 2013年

計算しているバケットに従ってデータを集計するのですか、それともバケット番号が探している最後の答えですか?
Jon Seigel 2013年

2
ああ。一度に1行ずつフェッチするカーソルループとは対照的に、レコードのストリーミングのサポートが向上するため、クライアント側のアプリを使用することになります。すべての更新がバッチで行われている限り、それはかなりうまく機能するはずです。
Jon Seigel 2013年

1
他の人がすでに述べたように、バケット化の要件はdistinct_count事態を複雑にします。Aaron Bertrandはこの種のウィンドウ処理に関するSQL Serverのオプションの優れた要約を持っています。「風変わりな更新」メソッドを使用して計算しましたがdistinct_sum、これはSQL Fiddleで確認できますが、これは信頼できません。
Nick Chammas 2013年

1
@JonSeigel Xアイテムを最小数のバケットに配置するという問題は、SQL言語の行ごとのアルゴリズムを使用して効率的に解決できないことに注意してください。たとえば、サイズ0.7、0.8、0.3のアイテムには2つのバケットが必要ですが、IDで並べ替えると、3つのバケットが必要になります。
Stoleg 2013年

回答:


9

どのような種類のパフォーマンスを探しているのかはわかりませんが、CLRまたは外部アプリを選択できない場合は、カーソルだけが残ります。私の古いラップトップでは、次のソリューションを使用して約100秒で1,000,000行を通過しました。それの良いところは、それが線形にスケーリングすることです。そのため、全体を実行するのに約20分かかります。まともなサーバーを使用すると、速度は速くなりますが、1桁ではないため、これを完了するには数分かかります。これが1回限りのプロセスである場合は、おそらく遅延を許容できます。これをレポートなどとして定期的に実行する必要がある場合は、同じテーブルに値を格納して、トリガーなどで新しい行が追加されたときに値を更新しないようにすることができます。

とにかく、ここにコードがあります:

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行で埋めてから機能します。

カーソルは、計算の実行中に各行を一時テーブルにコピーします。最後に、selectは計算結果を返します。データをコピーせず、代わりにインプレース更新を行うと、少し速くなる可能性があります。

SQL 2012にアップグレードするオプションがある場合は、新しいウィンドウスプールでサポートされている移動ウィンドウアグリゲートを確認できます。これにより、パフォーマンスが向上します。

余談ですが、permission_set = safeを使用してアセンブリをインストールしている場合、標準のT-SQLを使用すると、アセンブリを使用するよりもサーバーに悪いことを行う可能性があります。ここで、CLRが本当に役立つケース。


実装が簡単で、必要に応じて後で簡単に変更してデバッグできるため、これを受け入れました。@NickChammasの答えも正しく、おそらくより効率的に実行されるので、同様の問題に直面する他の人にとっては好みの問題だと思います。
Zikes 2013年

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
;

深呼吸してください。ここには2つの主要な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がギャップレスシーケンスであることを前提としています。そうでない場合は、ROW_NUMBER()希望の順序(例:)に従って行に番号を付ける追加のCTEを先頭に追加して、独自のギャップレスシーケンスを生成する必要がありますROW_NUMBER() OVER (ORDER BY value DESC)

率直に言って、これはかなり冗長です。


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適切なインデックスがあれば、これは可能性があると思います。
Jon Seigel 2013年

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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.