Soluzione per assegnare valori univoci a righe con distanza di collaborazione finita


9

Ho una tabella che può essere creata e popolata con il seguente codice:

CREATE TABLE dbo.Example(GroupKey int NOT NULL, RecordKey varchar(12) NOT NULL);
ALTER TABLE dbo.Example
    ADD CONSTRAINT iExample PRIMARY KEY CLUSTERED(GroupKey ASC, RecordKey ASC);
INSERT INTO dbo.Example(GroupKey, RecordKey)
VALUES (1, 'Archimedes'), (1, 'Newton'), (1, 'Euler'), (2, 'Euler'), (2, 'Gauss'),
       (3, 'Gauss'), (3, 'Poincaré'), (4, 'Ramanujan'), (5, 'Neumann'),
       (5, 'Grothendieck'), (6, 'Grothendieck'), (6, 'Tao');

Per tutte le righe che hanno una distanza di collaborazione limitata basata su RecordKeyun'altra riga, vorrei assegnare un valore univoco: non mi interessa come o quale tipo di dati sia il valore univoco.

Un set di risultati corretto che soddisfa ciò che sto chiedendo può essere generato con la seguente query:

SELECT 1 AS SupergroupKey, GroupKey, RecordKey
FROM dbo.Example
WHERE GroupKey IN(1, 2, 3)
UNION ALL
SELECT 2 AS SupergroupKey, GroupKey, RecordKey
FROM dbo.Example
WHERE GroupKey = 4
UNION ALL
SELECT 3 AS SupergroupKey, GroupKey, RecordKey
FROM dbo.Example
WHERE GroupKey IN(5, 6)
ORDER BY SupergroupKey ASC, GroupKey ASC, RecordKey ASC;

Per aiutare meglio ciò che sto chiedendo, spiegherò perché GroupKey1–3 ha lo stesso SupergroupKey:

  • GroupKey1 contiene l' RecordKeyEulero che a sua volta è contenuto in GroupKey2; quindi GroupKeys 1 e 2 devono avere lo stesso SupergroupKey.
  • Poiché Gauss è contenuto sia in GroupKeys 2 che in 3, anche loro devono avere lo stesso SupergroupKey. Questo porta a GroupKeys 1–3 di avere lo stesso SupergroupKey.
  • Poiché GroupKeys 1–3 non condivide alcuna RecordKeys con le restanti GroupKeys, sono le uniche a cui viene assegnato un SupergroupKeyvalore di 1.

Dovrei aggiungere che la soluzione deve essere generica. La tabella sopra e il set di risultati erano solo un esempio.

appendice

Ho rimosso il requisito per la soluzione non iterativa. Mentre preferirei una soluzione del genere, credo che sia un vincolo irragionevole. Sfortunatamente, non sono in grado di utilizzare alcuna soluzione basata su CLR; ma se vuoi includere una soluzione del genere, sentiti libero di farlo. Probabilmente non lo accetterò come risposta.

Il numero di righe nella mia tabella reale è grande quanto 5 milioni, ma ci sono giorni in cui il numero di righe "solo" sarà circa diecimila. In media ci sono 8 RecordKeys per GroupKeye 4 GroupKeys per RecordKey. Immagino che una soluzione avrà una complessità temporale esponenziale, ma sono comunque interessato a una soluzione.

Risposte:


7

Questa è una soluzione iterativa T-SQL per il confronto delle prestazioni.

Presuppone che una colonna aggiuntiva possa essere aggiunta alla tabella per memorizzare la chiave del supergruppo e che l'indicizzazione possa essere modificata:

Impostare

DROP TABLE IF EXISTS 
    dbo.Example;

CREATE TABLE dbo.Example
(
    SupergroupKey integer NOT NULL
        DEFAULT 0, 
    GroupKey integer NOT NULL, 
    RecordKey varchar(12) NOT NULL,

    CONSTRAINT iExample 
    PRIMARY KEY CLUSTERED 
        (GroupKey ASC, RecordKey ASC),

    CONSTRAINT [IX dbo.Example RecordKey, GroupKey]
    UNIQUE NONCLUSTERED (RecordKey, GroupKey),

    INDEX [IX dbo.Example SupergroupKey, GroupKey]
        (SupergroupKey ASC, GroupKey ASC)
);

INSERT dbo.Example
    (GroupKey, RecordKey)
VALUES 
    (1, 'Archimedes'), 
    (1, 'Newton'),
    (1, 'Euler'),
    (2, 'Euler'),
    (2, 'Gauss'),
    (3, 'Gauss'),
    (3, 'Poincaré'),
    (4, 'Ramanujan'),
    (5, 'Neumann'),
    (5, 'Grothendieck'),
    (6, 'Grothendieck'),
    (6, 'Tao');

Se si è in grado di invertire l'ordine delle chiavi della presente chiave primaria, non sarà richiesto l'indice univoco aggiuntivo.

Contorno

L'approccio di questa soluzione è:

  1. Impostare l'id del supergruppo su 1
  2. Trova la chiave di gruppo non elaborata con il numero più basso
  3. Se non viene trovato nessuno, esci
  4. Imposta il supergruppo per tutte le righe con la chiave di gruppo corrente
  5. Imposta il supergruppo per tutte le righe relative alle righe del gruppo corrente
  6. Ripetere il passaggio 5 fino a quando non vengono aggiornate righe
  7. Incrementa l'ID del supergruppo corrente
  8. Vai al passaggio 2

Implementazione

Commenti in linea:

-- No execution plans or rows affected messages
SET NOCOUNT ON;
SET STATISTICS XML OFF;

-- Reset all supergroups
UPDATE E
SET SupergroupKey = 0
FROM dbo.Example AS E
    WITH (TABLOCKX)
WHERE 
    SupergroupKey != 0;

DECLARE 
    @CurrentSupergroup integer = 0,
    @CurrentGroup integer = 0;

WHILE 1 = 1
BEGIN
    -- Next super group
    SET @CurrentSupergroup += 1;

    -- Find the lowest unprocessed group key
    SELECT 
        @CurrentGroup = MIN(E.GroupKey)
    FROM dbo.Example AS E
    WHERE 
        E.SupergroupKey = 0;

    -- Exit when no more unprocessed groups
    IF @CurrentGroup IS NULL BREAK;

    -- Set super group for all records in the current group
    UPDATE E
    SET E.SupergroupKey = @CurrentSupergroup
    FROM dbo.Example AS E 
    WHERE 
        E.GroupKey = @CurrentGroup;

    -- Iteratively find all groups for the super group
    WHILE 1 = 1
    BEGIN
        WITH 
            RecordKeys AS
            (
                SELECT DISTINCT
                    E.RecordKey
                FROM dbo.Example AS E
                WHERE
                    E.SupergroupKey = @CurrentSupergroup
            ),
            GroupKeys AS
            (
                SELECT DISTINCT
                    E.GroupKey
                FROM RecordKeys AS RK
                JOIN dbo.Example AS E
                    WITH (FORCESEEK)
                    ON E.RecordKey = RK.RecordKey
            )
        UPDATE E WITH (TABLOCKX)
        SET SupergroupKey = @CurrentSupergroup
        FROM GroupKeys AS GK
        JOIN dbo.Example AS E
            ON E.GroupKey = GK.GroupKey
        WHERE
            E.SupergroupKey = 0
        OPTION (RECOMPILE, QUERYTRACEON 9481); -- The original CE does better

        -- Break when no more related groups found
        IF @@ROWCOUNT = 0 BREAK;
    END;
END;

SELECT
    E.SupergroupKey,
    E.GroupKey,
    E.RecordKey
FROM dbo.Example AS E;

Progetto esecutivo

Per l'aggiornamento chiave:

Piano di aggiornamento

Risultato

Lo stato finale della tabella è:

╔═══════════════╦══════════╦══════════════╗
║ SupergroupKey ║ GroupKey ║  RecordKey   ║
╠═══════════════╬══════════╬══════════════╣
║             1 ║        1 ║ Archimedes   ║
║             1 ║        1 ║ Euler        ║
║             1 ║        1 ║ Newton       ║
║             1 ║        2 ║ Euler        ║
║             1 ║        2 ║ Gauss        ║
║             1 ║        3 ║ Gauss        ║
║             1 ║        3 ║ Poincaré     ║
║             2 ║        4 ║ Ramanujan    ║
║             3 ║        5 ║ Grothendieck ║
║             3 ║        5 ║ Neumann      ║
║             3 ║        6 ║ Grothendieck ║
║             3 ║        6 ║ Tao          ║
╚═══════════════╩══════════╩══════════════╝

Demo: db <> violino

Test delle prestazioni

Utilizzando il set di dati di test ampliato fornito nella risposta di Michael Green , i tempi sul mio laptop * sono:

╔═════════════╦════════╗
║ Record Keys ║  Time  ║
╠═════════════╬════════╣
║ 10k         ║ 2s     ║
║ 100k        ║ 12s    ║
║ 1M          ║ 2m 30s ║
╚═════════════╩════════╝

* Microsoft SQL Server 2017 (RTM-CU13), Developer Edition (64 bit), Windows 10 Pro, 16 GB di RAM, SSD, i7 hyperthreaded i7 a 4 core, 2,4 GHz nominali.


Questa è una risposta fantastica. Come prefigurato nella mia domanda, è troppo lento per i "grandi giorni"; ma è fantastico per i miei giorni più piccoli. Il mio tavolo di ≈2,5 milioni di righe ha impiegato circa 5 ore.
basketballfan22,

10

Questo problema riguarda i seguenti collegamenti tra elementi. Questo lo mette nel regno dei grafici e dell'elaborazione dei grafici . In particolare, l'intero set di dati forma un grafico e stiamo cercando componenti di quel grafico. Questo può essere illustrato da un grafico dei dati di esempio dalla domanda.

inserisci qui la descrizione dell'immagine

La domanda dice che possiamo seguire GroupKey o RecordKey per trovare altre righe che condividono quel valore. Quindi possiamo trattare entrambi come vertici in un grafico. La domanda continua spiegando come GroupKeys 1–3 abbia lo stesso SupergroupKey. Questo può essere visto come il cluster a sinistra unito da linee sottili. L'immagine mostra anche gli altri due componenti (SupergroupKey) formati dai dati originali.

SQL Server ha alcune capacità di elaborazione dei grafici integrate in T-SQL. Al momento, tuttavia, è piuttosto scarso e non è utile con questo problema. SQL Server ha anche la possibilità di chiamare R e Python e la ricca e robusta suite di pacchetti disponibili. Uno di questi è igraph . È scritto per "gestione rapida di grafici di grandi dimensioni, con milioni di vertici e bordi ( link )".

Usando R ed igraph sono stato in grado di elaborare un milione di righe in 2 minuti e 22 secondi nei test locali 1 . Ecco come si confronta con l'attuale migliore soluzione:

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.

Durante l'elaborazione di righe 1M, sono stati utilizzati 1m40 per caricare ed elaborare il grafico e aggiornare la tabella. Sono stati necessari 42 secondi per popolare una tabella dei risultati SSMS con l'output.

L'osservazione di Task Manager mentre venivano elaborate 1M righe suggerisce che erano necessari circa 3 GB di memoria di lavoro. Questo era disponibile su questo sistema senza paging.

Posso confermare la valutazione di Ypercube dell'approccio CTE ricorsivo. Con poche centinaia di chiavi di registrazione ha consumato il 100% della CPU e tutta la RAM disponibile. Alla fine tempdb è cresciuto fino a oltre 80 GB e lo SPID si è bloccato.

Ho usato il tavolo di Paul con la colonna SupergroupKey, quindi c'è un giusto confronto tra le soluzioni.

Per qualche ragione R ha contestato l'accento su Poincaré. Modificandolo in una semplice "e" gli ha permesso di funzionare. Non ho indagato poiché non è germano del problema attuale. Sono sicuro che c'è una soluzione.

Ecco il codice

-- 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);

Questo è ciò che fa il codice R.

  • @input_data_1 è il modo in cui SQL Server trasferisce i dati da una tabella al codice R e li traduce in un frame di dati R chiamato InputDataSet.

  • library(igraph) importa la libreria nell'ambiente di esecuzione R.

  • df.g <- graph.data.frame(d = InputDataSet, directed = FALSE)carica i dati in un oggetto igraph. Questo è un grafico non indirizzato poiché possiamo seguire i collegamenti da un gruppo per registrare o registrare per un gruppo. InputDataSet è il nome predefinito di SQL Server per il set di dati inviato a R.

  • cpts <- components(df.g, mode = c("weak")) elabora il grafico per trovare sotto-grafici (componenti) discreti e altre misure.

  • OutputDataSet <- data.frame(cpts$membership)SQL Server prevede un frame di dati da R. Il nome predefinito è OutputDataSet. I componenti sono memorizzati in un vettore chiamato "appartenenza". Questa affermazione traduce il vettore in un frame di dati.

  • OutputDataSet$VertexName <- V(df.g)$nameV () è un vettore di vertici nel grafico - un elenco di GroupKeys e RecordKeys. Questo li copia nel frame di dati di output, creando una nuova colonna chiamata VertexName. Questa è la chiave utilizzata per abbinare la tabella di origine per l'aggiornamento di SupergroupKey.

Non sono un esperto di R. Probabilmente questo potrebbe essere ottimizzato.

Dati di test

I dati del PO sono stati utilizzati per la convalida. Per i test di scala ho usato il seguente 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);

Mi sono appena reso conto che ho ottenuto i rapporti nel modo sbagliato dalla definizione del PO. Non credo che ciò influirà sui tempi. Record e gruppi sono simmetrici a questo processo. Per l'algoritmo sono tutti solo nodi in un grafico.

Nel testare i dati formava invariabilmente un singolo componente. Credo che ciò sia dovuto alla distribuzione uniforme dei dati. Se invece del rapporto statico 1: 8 codificato nella routine di generazione avessi permesso al rapporto di variare, ci sarebbero stati probabilmente ulteriori componenti.



1 Specifiche della macchina: Microsoft SQL Server 2017 (RTM-CU12), Developer Edition (64-bit), Windows 10 Home. 16 GB di RAM, SSD, i7 hyperthreaded a 4 core, 2,8 GHz nominali. I test erano gli unici elementi in esecuzione al momento, diversi dalla normale attività del sistema (circa il 4% della CPU).


6

Un metodo CTE ricorsivo - che è probabilmente terribilmente inefficiente nelle grandi tabelle:

WITH rCTE AS 
(
    -- Anchor
    SELECT 
        GroupKey, RecordKey, 
        CAST('|' + CAST(GroupKey AS VARCHAR(10)) + '|' AS VARCHAR(100)) AS GroupKeys,
        CAST('|' + CAST(RecordKey AS VARCHAR(10)) + '|' AS VARCHAR(100)) AS RecordKeys,
        1 AS lvl
    FROM Example

    UNION ALL

    -- Recursive
    SELECT
        e.GroupKey, e.RecordKey, 
        CASE WHEN r.GroupKeys NOT LIKE '%|' + CAST(e.GroupKey AS VARCHAR(10)) + '|%'
            THEN CAST(r.GroupKeys + CAST(e.GroupKey AS VARCHAR(10)) + '|' AS VARCHAR(100))
            ELSE r.GroupKeys
        END,
        CASE WHEN r.RecordKeys NOT LIKE '%|' + CAST(e.RecordKey AS VARCHAR(10)) + '|%'
            THEN CAST(r.RecordKeys + CAST(e.RecordKey AS VARCHAR(10)) + '|' AS VARCHAR(100))
            ELSE r.RecordKeys
        END,
        r.lvl + 1
    FROM rCTE AS r
         JOIN Example AS e
         ON  e.RecordKey = r.RecordKey
         AND r.GroupKeys NOT LIKE '%|' + CAST(e.GroupKey AS VARCHAR(10)) + '|%'
         -- 
         OR e.GroupKey = r.GroupKey
         AND r.RecordKeys NOT LIKE '%|' + CAST(e.RecordKey AS VARCHAR(10)) + '|%'
)
SELECT 
    ROW_NUMBER() OVER (ORDER BY GroupKeys) AS SuperGroupKey,
    GroupKeys, RecordKeys
FROM rCTE AS c
WHERE NOT EXISTS
      ( SELECT 1
        FROM rCTE AS m
        WHERE m.lvl > c.lvl
          AND m.GroupKeys LIKE '%|' + CAST(c.GroupKey AS VARCHAR(10)) + '|%'
        OR    m.lvl = c.lvl
          AND ( m.GroupKey > c.GroupKey
             OR m.GroupKey = c.GroupKey
             AND m.RecordKeys > c.RecordKeys
              )
          AND m.GroupKeys LIKE '%|' + CAST(c.GroupKey AS VARCHAR(10)) + '|%'
          AND c.GroupKeys LIKE '%|' + CAST(m.GroupKey AS VARCHAR(10)) + '|%'
      ) 
OPTION (MAXRECURSION 0) ;

Testato in dbfiddle.uk

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.