次のような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つのバケットにサイズを提供できるようになります。
どのような種類のパフォーマンスを探しているのかはわかりませんが、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が本当に役立つケース。
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)
)。
率直に言って、これはかなり冗長です。
これはばかげたソリューションのように感じられ、おそらく適切にスケーリングされないため、使用する場合は慎重にテストしてください。主な問題はバケット内の「スペース」に起因するため、最初にデータに結合するフィラーレコードを作成する必要がありました。
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
以下は別の再帰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 で見つけることができます。