Ce problème concerne les liens entre les éléments. Cela le place dans le domaine des graphiques et du traitement des graphiques. Plus précisément, l'ensemble de données forme un graphique et nous recherchons des composants de ce graphique. Cela peut être illustré par un tracé des données de l'échantillon de la question.
La question dit que nous pouvons suivre GroupKey ou RecordKey pour trouver d'autres lignes qui partagent cette valeur. Nous pouvons donc traiter les deux comme des sommets dans un graphique. La question continue pour expliquer comment les GroupKeys 1 à 3 ont la même SupergroupKey. Cela peut être vu comme le cluster à gauche rejoint par des lignes fines. L'image montre également les deux autres composants (SupergroupKey) formés par les données d'origine.
SQL Server a une certaine capacité de traitement graphique intégrée à T-SQL. À l'heure actuelle, il est cependant assez maigre et n'aide pas avec ce problème. SQL Server a également la possibilité d'appeler R et Python, ainsi que la suite riche et robuste de packages à leur disposition. Un tel est l' igraph . Il est écrit pour "une gestion rapide des grands graphes, avec des millions de sommets et d'arêtes ( lien )".
En utilisant R et igraph, j'ai pu traiter un million de lignes en 2 minutes 22 secondes dans les tests locaux 1 . Voici comment il se compare à la meilleure solution actuelle:
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.
Lors du traitement de 1M de lignes, 1m40 a été utilisé pour charger et traiter le graphique et mettre à jour le tableau. Il a fallu 42 secondes pour remplir une table de résultats SSMS avec la sortie.
L'observation du gestionnaire de tâches pendant le traitement de 1 million de lignes suggère qu'il fallait environ 3 Go de mémoire de travail. C'était disponible sur ce système sans pagination.
Je peux confirmer l'évaluation par Ypercube de l'approche CTE récursive. Avec quelques centaines de clés d'enregistrement, il a consommé 100% du CPU et toute la RAM disponible. Finalement, tempdb est passé à plus de 80 Go et le SPID s'est écrasé.
J'ai utilisé la table de Paul avec la colonne SupergroupKey afin qu'il y ait une comparaison équitable entre les solutions.
Pour une raison quelconque, R s'est opposé à l'accent sur Poincaré. Le changer en un simple "e" lui a permis de s'exécuter. Je n'ai pas enquêté car ce n'est pas lié au problème actuel. Je suis sûr qu'il y a une solution.
Voici le code
-- 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);
C'est ce que fait le code R
@input_data_1
est la façon dont SQL Server transfère les données d'une table au code R et les convertit en une trame de données R appelée InputDataSet.
library(igraph)
importe la bibliothèque dans l'environnement d'exécution R.
df.g <- graph.data.frame(d = InputDataSet, directed = FALSE)
charger les données dans un objet igraph. Il s'agit d'un graphique non orienté car nous pouvons suivre les liens d'un groupe à l'autre ou d'un enregistrement à un autre. InputDataSet est le nom par défaut de SQL Server pour l'ensemble de données envoyé à R.
cpts <- components(df.g, mode = c("weak"))
traiter le graphique pour trouver des sous-graphiques discrets (composants) et d'autres mesures.
OutputDataSet <- data.frame(cpts$membership)
SQL Server attend une trame de données à partir de R. Son nom par défaut est OutputDataSet. Les composants sont stockés dans un vecteur appelé "appartenance". Cette instruction traduit le vecteur en un bloc de données.
OutputDataSet$VertexName <- V(df.g)$name
V () est un vecteur de sommets dans le graphique - une liste de GroupKeys et RecordKeys. Cela les copie dans le bloc de données de sortie, créant une nouvelle colonne appelée VertexName. Il s'agit de la clé utilisée pour correspondre à la table source pour la mise à jour de SupergroupKey.
Je ne suis pas un expert en R. Cela pourrait probablement être optimisé.
Données de test
Les données du PO ont été utilisées pour la validation. Pour les tests d'échelle, j'ai utilisé le script suivant.
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);
Je viens juste de réaliser que j'ai obtenu les rapports dans le mauvais sens à partir de la définition de l'OP. Je ne pense pas que cela affectera les horaires. Les enregistrements et les groupes sont symétriques à ce processus. Pour l'algorithme, ce ne sont que des nœuds dans un graphique.
Lors des tests, les données formaient invariablement un seul composant. Je pense que cela est dû à la distribution uniforme des données. Si, au lieu du rapport statique 1: 8 codé en dur dans la routine de génération, j'avais permis au rapport de varier, il y aurait probablement eu d'autres composants.
1 Spécifications de la machine: Microsoft SQL Server 2017 (RTM-CU12), Developer Edition (64 bits), Windows 10 Home. 16 Go de RAM, SSD, i7 hyperthreadé à 4 cœurs, 2,8 GHz nominal. Les tests étaient les seuls éléments exécutés à l'époque, à l'exception de l'activité normale du système (environ 4% du processeur).