この問題は、アイテム間のリンクをたどることに関するものです。これにより、グラフとグラフ処理の領域に配置されます。具体的には、データセット全体がグラフを形成し、そのグラフのコンポーネントを探しています。これは、質問のサンプルデータのプロットで説明できます。
質問では、GroupKeyまたはRecordKeyをたどって、その値を共有する他の行を見つけることができると述べています。したがって、両方をグラフの頂点として扱うことができます。質問は、GroupKeys 1〜3が同じSupergroupKeyをどのように持つかを説明します。これは、細い線で結ばれた左側のクラスターとして見ることができます。この図は、元のデータによって形成された他の2つのコンポーネント(SupergroupKey)も示しています。
SQL Serverには、T-SQLに組み込まれたグラフ処理機能がいくつかあります。現時点では、これは非常に貧弱であり、この問題には役立ちません。SQL Serverには、RおよびPythonを呼び出す機能と、それらに使用できる豊富で堅牢なパッケージスイートもあります。その1つがigraphです。それは「何百万もの頂点とエッジを持つ大きなグラフの高速処理(link)」のために書かれています。
Rとigraphを使用して、ローカルテスト1で2分22秒で100万行を処理することができました。これは、現在の最良のソリューションと比較する方法です。
Record Keys Paul White R
------------ ---------- --------
Per question 15ms ~220ms
100 80ms ~270ms
1,000 250ms 430ms
10,000 1.4s 1.7s
100,000 14s 14s
1M 2m29 2m22s
1M n/a 1m40 process only, no display
The first column is the number of distinct RecordKey values. The number of rows
in the table will be 8 x this number.
100万行を処理する場合、グラフの読み込みと処理、およびテーブルの更新に1m40が使用されました。SSMS結果テーブルに出力を取り込むには、42秒が必要でした。
100万行が処理されている間のタスクマネージャーの観察は、約3GBの作業メモリが必要であることを示唆しています。これは、ページングなしでこのシステムで使用できました。
再帰的なCTEアプローチに対するYpercubeの評価を確認できます。数百のレコードキーを使用して、CPUと使用可能なすべてのRAMを100%消費しました。最終的にtempdbが80GB以上になり、SPIDがクラッシュしました。
PaulgroupのテーブルとSupergroupKey列を使用したので、ソリューション間で公平な比較ができます。
何らかの理由でRはポアンカレのアクセントに異議を唱えました。プレーンな「e」に変更すると、実行できます。それは目前の問題に密接に関係していないので、私は調査しませんでした。私は解決策があると確信しています。
これがコードです
-- This captures the output from R so the base table can be updated.
drop table if exists #Results;
create table #Results
(
Component int not NULL,
Vertex varchar(12) not NULL primary key
);
truncate table #Results; -- facilitates re-execution
declare @Start time = sysdatetimeoffset(); -- for a 'total elapsed' calculation.
insert #Results(Component, Vertex)
exec sp_execute_external_script
@language = N'R',
@input_data_1 = N'select GroupKey, RecordKey from dbo.Example',
@script = N'
library(igraph)
df.g <- graph.data.frame(d = InputDataSet, directed = FALSE)
cpts <- components(df.g, mode = c("weak"))
OutputDataSet <- data.frame(cpts$membership)
OutputDataSet$VertexName <- V(df.g)$name
';
-- Write SuperGroupKey to the base table, as other solutions do
update e
set
SupergroupKey = r.Component
from dbo.Example as e
inner join #Results as r
on r.Vertex = e.RecordKey;
-- Return all rows, as other solutions do
select
e.SupergroupKey,
e.GroupKey,
e.RecordKey
from dbo.Example as e;
-- Calculate the elapsed
declare @End time = sysdatetimeoffset();
select Elapse_ms = DATEDIFF(MILLISECOND, @Start, @End);
これはRコードが行うことです
@input_data_1
SQL ServerがテーブルからRコードにデータを転送し、それをInputDataSetと呼ばれるRデータフレームに変換する方法です。
library(igraph)
ライブラリをR実行環境にインポートします。
df.g <- graph.data.frame(d = InputDataSet, directed = FALSE)
データをigraphオブジェクトにロードします。グループからレコードへ、またはレコードからグループへのリンクをたどることができるため、これは無向グラフです。InputDataSetは、Rに送信されるデータセットのSQL Serverのデフォルト名です。
cpts <- components(df.g, mode = c("weak"))
グラフを処理して、離散サブグラフ(コンポーネント)およびその他のメジャーを見つけます。
OutputDataSet <- data.frame(cpts$membership)
SQL Serverは、Rから返されるデータフレームを予期します。そのデフォルト名はOutputDataSetです。コンポーネントは「メンバーシップ」と呼ばれるベクターに保存されます。このステートメントは、ベクターをデータフレームに変換します。
OutputDataSet$VertexName <- V(df.g)$name
V()は、グラフ内の頂点のベクトル-GroupKeysおよびRecordKeysのリストです。これにより、それらが出力データフレームにコピーされ、VertexNameという新しい列が作成されます。これは、SupergroupKeyを更新するためにソーステーブルと照合するために使用されるキーです。
私はRの専門家ではありません。おそらくこれは最適化できるでしょう。
テストデータ
OPのデータは検証に使用されました。スケールテストでは、次のスクリプトを使用しました。
drop table if exists Records;
drop table if exists Groups;
create table Groups(GroupKey int NOT NULL primary key);
create table Records(RecordKey varchar(12) NOT NULL primary key);
go
set nocount on;
-- Set @RecordCount to the number of distinct RecordKey values desired.
-- The number of rows in dbo.Example will be 8 * @RecordCount.
declare @RecordCount int = 1000000;
-- @Multiplier was determined by experiment.
-- It gives the OP's "8 RecordKeys per GroupKey and 4 GroupKeys per RecordKey"
-- and allows for clashes of the chosen random values.
declare @Multiplier numeric(4, 2) = 2.7;
-- The number of groups required to reproduce the OP's distribution.
declare @GroupCount int = FLOOR(@RecordCount * @Multiplier);
-- This is a poor man's numbers table.
insert Groups(GroupKey)
select top(@GroupCount)
ROW_NUMBER() over (order by (select NULL))
from sys.objects as a
cross join sys.objects as b
--cross join sys.objects as c -- include if needed
declare @c int = 0
while @c < @RecordCount
begin
-- Can't use a set-based method since RAND() gives the same value for all rows.
-- There are better ways to do this, but it works well enough.
-- RecordKeys will be 10 letters, a-z.
insert Records(RecordKey)
select
CHAR(97 + (26*RAND())) +
CHAR(97 + (26*RAND())) +
CHAR(97 + (26*RAND())) +
CHAR(97 + (26*RAND())) +
CHAR(97 + (26*RAND())) +
CHAR(97 + (26*RAND())) +
CHAR(97 + (26*RAND())) +
CHAR(97 + (26*RAND())) +
CHAR(97 + (26*RAND())) +
CHAR(97 + (26*RAND()));
set @c += 1;
end
-- Process each RecordKey in alphabetical order.
-- For each choose 8 GroupKeys to pair with it.
declare @RecordKey varchar(12) = '';
declare @Groups table (GroupKey int not null);
truncate table dbo.Example;
select top(1) @RecordKey = RecordKey
from Records
where RecordKey > @RecordKey
order by RecordKey;
while @@ROWCOUNT > 0
begin
print @Recordkey;
delete @Groups;
insert @Groups(GroupKey)
select distinct C
from
(
-- Hard-code * from OP's statistics
select FLOOR(RAND() * @GroupCount)
union all
select FLOOR(RAND() * @GroupCount)
union all
select FLOOR(RAND() * @GroupCount)
union all
select FLOOR(RAND() * @GroupCount)
union all
select FLOOR(RAND() * @GroupCount)
union all
select FLOOR(RAND() * @GroupCount)
union all
select FLOOR(RAND() * @GroupCount)
union all
select FLOOR(RAND() * @GroupCount)
) as T(C);
insert dbo.Example(GroupKey, RecordKey)
select
GroupKey, @RecordKey
from @Groups;
select top(1) @RecordKey = RecordKey
from Records
where RecordKey > @RecordKey
order by RecordKey;
end
-- Rebuild the indexes to have a consistent environment
alter index iExample on dbo.Example rebuild partition = all
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF,
ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON);
-- Check what we ended up with:
select COUNT(*) from dbo.Example; -- Should be @RecordCount * 8
-- Often a little less due to random clashes
select
ByGroup = AVG(C)
from
(
select CONVERT(float, COUNT(1) over(partition by GroupKey))
from dbo.Example
) as T(C);
select
ByRecord = AVG(C)
from
(
select CONVERT(float, COUNT(1) over(partition by RecordKey))
from dbo.Example
) as T(C);
OPの定義から比率を間違った方法で取得したことに気づきました。これがタイミングに影響するとは思わない。レコードとグループは、このプロセスと対称的です。アルゴリズムにとって、それらはすべてグラフ内の単なるノードです。
データのテストでは、常に単一のコンポーネントが形成されました。これはデータが均一に分布しているためだと思います。静的な1:8の比率の代わりに生成ルーチンにハードコード化した場合、比率を変化させることができれば、さらにコンポーネントが存在する可能性が高くなります。
1マシン仕様:Microsoft SQL Server 2017(RTM-CU12)、Developer Edition(64ビット)、Windows 10 Home。16 GB RAM、SSD、4コアハイパースレッドi7、公称2.8 GHz テストは、通常のシステムアクティビティ(CPU約4%)を除いて、現時点で実行されている唯一の項目でした。