web-dev-qa-db-ja.com

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

次のような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
_

ご覧のように、単純に_crude_sum_でCEILINGを使用すると、レコード#8がバケット2に割り当てられます。これは、レコード#5と#のsizeが原因です。 8つが2つのバケットに分割されます。代わりに、理想的な解決策は、合計が1に達するたびに合計をリセットすることです。これにより、bucket列がインクリメントされ、新しいSUM操作がsize値から開始されます。現在のレコード。この操作ではレコードの順序が重要であるため、降順でソートすることを目的としたvalue列を含めました。

私の最初の試みは、SUM操作を実行するために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つのバケットにサイズを提供できるようになります。

11
Zikes

どのような種類のパフォーマンスを探しているのかはわかりませんが、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が本当に役立つケース。

9
Sebastian Meine

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))。

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

9
Nick Chammas

これはばかげたソリューションのように感じられ、おそらく適切にスケーリングされないため、使用する場合は慎重にテストしてください。主な問題はバケット内の「スペース」に起因するため、最初にデータに結合するフィラーレコードを作成する必要がありました。

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/

5
SQLFox

以下は別の再帰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の降順に基づいて計算されたランキング列を導入し、valueの代わりに再帰CTEで使用して、再帰部分を結合する必要があります。アンカー。

SQL Fiddleこのクエリのデモは here で見つけることができます。

3
Andriy M