Esse problema é sobre seguir os links entre os itens. Isso o coloca no domínio de gráficos e processamento de gráficos . Especificamente, todo o conjunto de dados forma um gráfico e estamos procurando componentes desse gráfico. Isso pode ser ilustrado por um gráfico dos dados de amostra da pergunta.
A pergunta diz que podemos seguir GroupKey ou RecordKey para encontrar outras linhas que compartilham esse valor. Assim, podemos tratar ambos como vértices em um gráfico. A pergunta continua para explicar como as teclas de grupo 1 a 3 têm a mesma tecla de supergrupo. Isso pode ser visto como o cluster à esquerda unido por linhas finas. A imagem também mostra os outros dois componentes (SupergroupKey) formados pelos dados originais.
O SQL Server possui alguma capacidade de processamento gráfico incorporada ao T-SQL. No momento, porém, é bastante escasso e não ajuda com esse problema. O SQL Server também pode chamar R e Python, e o rico e robusto conjunto de pacotes disponíveis para eles. Um deles é o gráfico . Foi escrito para "manipulação rápida de gráficos grandes, com milhões de vértices e arestas ( link )".
Usando R e igraph, fui capaz de processar um milhão de linhas em 2 minutos e 22 segundos no teste local 1 . É assim que ele se compara à melhor solução atual:
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.
Ao processar 1 milhão de linhas, 1m40s foram usados para carregar e processar o gráfico e atualizar a tabela. 42s foram necessários para preencher uma tabela de resultados do SSMS com a saída.
A observação do Gerenciador de Tarefas enquanto as linhas de 1 milhão foram processadas sugere que são necessários cerca de 3 GB de memória de trabalho. Isso estava disponível neste sistema sem paginação.
Posso confirmar a avaliação de Ypercube da abordagem CTE recursiva. Com algumas centenas de chaves de registro, consumiu 100% da CPU e toda a RAM disponível. Eventualmente, o tempdb cresceu para mais de 80 GB e o SPID travou.
Eu usei a tabela de Paul com a coluna SupergroupKey, para que haja uma comparação justa entre as soluções.
Por alguma razão, R se opôs ao sotaque de Poincaré. Mudá-lo para um "e" simples permitiu a execução. Não investiguei, pois não é relevante para o problema em questão. Tenho certeza que há uma solução.
Aqui está o código
-- 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);
É isso que o código R faz
@input_data_1
é como o SQL Server transfere dados de uma tabela para o código R e os converte em um dataframe R chamado InputDataSet.
library(igraph)
importa a biblioteca para o ambiente de execução R.
df.g <- graph.data.frame(d = InputDataSet, directed = FALSE)
carregar os dados em um objeto igraph. Este é um gráfico não direcionado, pois podemos seguir os links do grupo para gravar ou gravar para o grupo. InputDataSet é o nome padrão do SQL Server para o conjunto de dados enviado para R.
cpts <- components(df.g, mode = c("weak"))
processe o gráfico para encontrar subgráficos discretos (componentes) e outras medidas.
OutputDataSet <- data.frame(cpts$membership)
O SQL Server espera um quadro de dados de volta de R. Seu nome padrão é OutputDataSet. Os componentes são armazenados em um vetor chamado "associação". Esta declaração converte o vetor em um quadro de dados.
OutputDataSet$VertexName <- V(df.g)$name
V () é um vetor de vértices no gráfico - uma lista de Teclas de Grupo e Teclas de Registro. Isso os copia no quadro de dados de saída, criando uma nova coluna chamada VertexName. Essa é a chave usada para corresponder à tabela de origem para atualizar o SupergroupKey.
Eu não sou um especialista em R. Provavelmente isso pode ser otimizado.
Dados de teste
Os dados do OP foram utilizados para validação. Para testes de escala, usei o seguinte script.
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);
Acabei de perceber que entendi errado as proporções da definição do OP. Eu não acredito que isso afetará os horários. Registros e grupos são simétricos para esse processo. Para o algoritmo, todos são apenas nós em um gráfico.
No teste, os dados invariavelmente formaram um único componente. Eu acredito que isso se deve à distribuição uniforme dos dados. Se, em vez da proporção estática de 1: 8, codificada na rotina de geração, eu permitisse que a proporção variasse , provavelmente haveria outros componentes.
1 Especificação da máquina: Microsoft SQL Server 2017 (RTM-CU12), Developer Edition (64 bits), Windows 10 Home. 16GB de RAM, SSD, i7 hyperthreaded de 4 núcleos, 2,8 GHz nominal. Os testes foram os únicos itens em execução no momento, exceto a atividade normal do sistema (cerca de 4% da CPU).