Este problema se trata de seguir enlaces entre elementos. Esto lo coloca en el ámbito de los gráficos y el procesamiento de gráficos. Específicamente, todo el conjunto de datos forma un gráfico y estamos buscando componentes de ese gráfico. Esto se puede ilustrar con una gráfica de los datos de muestra de la pregunta.
La pregunta dice que podemos seguir GroupKey o RecordKey para encontrar otras filas que compartan ese valor. Entonces podemos tratar a ambos como vértices en un gráfico. La pregunta continúa para explicar cómo GroupKeys 1–3 tienen la misma SupergroupKey. Esto se puede ver como el grupo a la izquierda unido por líneas finas. La imagen también muestra los otros dos componentes (SupergroupKey) formados por los datos originales.
SQL Server tiene alguna capacidad de procesamiento de gráficos integrada en T-SQL. Sin embargo, en este momento es bastante escaso y no ayuda con este problema. SQL Server también tiene la capacidad de llamar a R y Python, y al conjunto de paquetes rico y robusto disponible para ellos. Uno de ellos es igraph . Está escrito para "el manejo rápido de gráficos grandes, con millones de vértices y bordes ( enlace )".
Usando R e igraph pude procesar un millón de filas en 2 minutos y 22 segundos en la prueba local 1 . Así es como se compara con la mejor solución actual:
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.
Al procesar filas de 1M, se usaron 1m40 para cargar y procesar el gráfico, y para actualizar la tabla. Se requirieron 42 para completar una tabla de resultados SSMS con la salida.
La observación del Administrador de tareas mientras se procesaban 1 millón de filas sugiere que se necesitaban unos 3 GB de memoria de trabajo. Esto estaba disponible en este sistema sin paginación.
Puedo confirmar la evaluación de Ypercube del enfoque recursivo de CTE. Con unos pocos cientos de claves de grabación consumió el 100% de la CPU y toda la RAM disponible. Finalmente, tempdb creció a más de 80 GB y el SPID se bloqueó.
Usé la tabla de Paul con la columna SupergroupKey para que haya una comparación justa entre las soluciones.
Por alguna razón, R se opuso al acento en Poincaré. Cambiarlo a una simple "e" le permitió ejecutarse. No investigué ya que no está relacionado con el problema en cuestión. Estoy seguro de que hay una solución.
Aqui esta el codigo
-- 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);
Esto es lo que hace el código R
@input_data_1
es cómo SQL Server transfiere datos de una tabla a código R y los traduce a un marco de datos R llamado InputDataSet.
library(igraph)
importa la biblioteca al entorno de ejecución de R.
df.g <- graph.data.frame(d = InputDataSet, directed = FALSE)
cargar los datos en un objeto igraph. Este es un gráfico no dirigido ya que podemos seguir enlaces de grupo a registro o grabar a grupo. InputDataSet es el nombre predeterminado de SQL Server para el conjunto de datos enviado a R.
cpts <- components(df.g, mode = c("weak"))
procesar el gráfico para encontrar sub-gráficos discretos (componentes) y otras medidas.
OutputDataSet <- data.frame(cpts$membership)
SQL Server espera un marco de datos de R. Su nombre predeterminado es OutputDataSet. Los componentes se almacenan en un vector llamado "membresía". Esta declaración traduce el vector a un marco de datos.
OutputDataSet$VertexName <- V(df.g)$name
V () es un vector de vértices en el gráfico, una lista de GroupKeys y RecordKeys. Esto los copia en el marco de datos de salida, creando una nueva columna llamada VertexName. Esta es la clave utilizada para coincidir con la tabla de origen para actualizar SupergroupKey.
No soy un experto en R. Probablemente esto podría optimizarse.
Datos de prueba
Los datos del OP se utilizaron para la validación. Para las pruebas de escala utilicé el siguiente 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);
Me acabo de dar cuenta de que obtuve las proporciones al revés de la definición del OP. No creo que esto afecte los tiempos. Los registros y grupos son simétricos a este proceso. Para el algoritmo, todos son nodos en un gráfico.
Al probar, los datos invariablemente formaron un solo componente. Creo que esto se debe a la distribución uniforme de los datos. Si en lugar de la relación estática 1: 8 codificada en la rutina de generación, hubiera permitido que la relación variara , probablemente habría habido más componentes.
1 Especificaciones de la máquina: Microsoft SQL Server 2017 (RTM-CU12), Developer Edition (64 bits), Windows 10 Home. 16 GB de RAM, SSD, i7 de 4 núcleos hyperthreaded, 2.8GHz nominal. Las pruebas fueron los únicos elementos que se ejecutaron en ese momento, aparte de la actividad normal del sistema (aproximadamente 4% de CPU).