Эта проблема касается следующих ссылок между элементами. Это помещает это в область графов и обработки графов. В частности, весь набор данных образует граф, и мы ищем компоненты этого графа. Это может быть проиллюстрировано графиком выборки данных из вопроса.
Вопрос говорит, что мы можем следовать GroupKey или RecordKey, чтобы найти другие строки, которые разделяют это значение. Таким образом, мы можем рассматривать обе вершины графа. Этот вопрос объясняет, почему у GroupKeys 1–3 одинаковый SupergroupKey. Это можно увидеть как кластер слева, соединенный тонкими линиями. На рисунке также показаны два других компонента (SupergroupKey), сформированные из исходных данных.
SQL Server имеет некоторые возможности обработки графиков, встроенные в T-SQL. В настоящее время это довольно скудно, но не помогает с этой проблемой. SQL Server также имеет возможность обращаться к R и Python, а также к богатому и надежному набору пакетов, доступных для них. Одним из таких является igraph . Он написан для «быстрой обработки больших графов с миллионами вершин и ребер ( ссылка )».
Используя R и igraph, я смог обработать миллион строк за 2 минуты 22 секунды в локальном тестировании 1 . Вот как он сравнивается с текущим лучшим решением:
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.
При обработке 1М строк 1м40 использовались для загрузки и обработки графика, а также для обновления таблицы. 42 были обязаны заполнить таблицу результатов SSMS с выводом.
Наблюдение за диспетчером задач при обработке 1М строк позволяет предположить, что требовалось около 3 ГБ рабочей памяти. Это было доступно в этой системе без подкачки страниц.
Я могу подтвердить оценку Ypercube рекурсивного подхода CTE. С несколькими сотнями ключей записи он потребляет 100% процессорного времени и всей доступной оперативной памяти. В конце концов, tempdb выросла до 80 ГБ, и SPID потерпел крах.
Я использовал таблицу Пола со столбцом SupergroupKey, чтобы было справедливое сравнение между решениями.
По какой-то причине Р. возражал против акцента на Пуанкаре. Изменение его на обычное «е» позволило ему работать. Я не расследовал, так как это не относится к рассматриваемой проблеме. Я уверен, что есть решение.
Вот код
-- 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 и переводит их в кадр данных R с именем InputDataSet.
library(igraph)
импортирует библиотеку в среду выполнения R.
df.g <- graph.data.frame(d = InputDataSet, directed = FALSE)
загрузить данные в объект igraph. Это неориентированный график, поскольку мы можем переходить по ссылкам от группы к записи или записи к группе. InputDataSet - это имя SQL Server по умолчанию для набора данных, отправляемого в R.
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 Скорее всего, это можно оптимизировать.
Тестовые данные
Данные ОП были использованы для проверки. Для масштабных тестов я использовал следующий скрипт.
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);
Я только сейчас понял, что неправильно понял соотношения из определения ОП. Я не верю, что это повлияет на время. Записи и группы симметричны этому процессу. По алгоритму они все просто узлы на графике.
При тестировании данных неизменно формируется единый компонент. Я считаю, что это связано с равномерным распределением данных. Если бы вместо статического соотношения 1: 8, жестко запрограммированного в процедуру генерации, я бы позволил этому соотношению изменяться , скорее всего, были бы дополнительные компоненты.
1 Спецификация машины: Microsoft SQL Server 2017 (RTM-CU12), Developer Edition (64-разрядная версия), Windows 10 Home. 16 ГБ ОЗУ, твердотельный накопитель, 4-ядерный Hyper-Threading i7, номинал 2,8 ГГц. Тесты были единственными элементами, запущенными в то время, кроме нормальной системной активности (около 4% ЦП).