Sınırlı işbirliği mesafesine sahip satırlara benzersiz değerler atama çözümü


9

Oluşturulabilir ve aşağıdaki kod ile doldurulmuş bir tablo var:

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

Başka bir satıra dayalı sonlu bir işbirliği mesafesine sahip tüm satırlar için, RecordKeybenzersiz bir değer atamak istiyorum; benzersiz değerin nasıl veya ne tür bir veri olduğu umurumda değil.

İstediğimi karşılayan doğru bir sonuç kümesi aşağıdaki sorgu ile oluşturulabilir:

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;

Sorduğum şeylere daha iyi yardımcı GroupKeyolmak için, 1-3'ün neden aynı olduğunu açıklayacağım SupergroupKey:

  • GroupKey1, 2'de RecordKeybulunan Euler'i içerir GroupKey; bu nedenle GroupKeys 1 ve 2 aynı olmalıdır SupergroupKey.
  • Gauss hem GroupKeys 2'de hem de 3'te bulunduğundan, bunların da aynı olması gerekir SupergroupKey. Bu da GroupKeys 1-3'ün aynısına sahip olmasına neden olur SupergroupKey.
  • Yana GroupKeyler 1-3 herhangi paylaşmayan RecordKeykalan s GroupKeys, onlar atanan tek olanlar SupergroupKey1 değerini.

Çözümün genel olması gerektiğini de eklemeliyim. Yukarıdaki tablo ve sonuç kümesi sadece bir örnektir.

ek

Çözümün yinelemesiz olması ihtiyacını ortadan kaldırdım. Böyle bir çözümü tercih ederken, bunun mantıksız bir kısıtlama olduğuna inanıyorum. Ne yazık ki, CLR tabanlı bir çözüm kullanamıyorum; ancak böyle bir çözüm eklemek istiyorsanız, çekinmeyin. Yine de cevap olarak kabul etmeyeceğim.

Gerçek masamdaki satır sayısı 5 milyon kadar büyük, ancak satır sayısının sadece on bin civarında olacağı günler var. Ortalama 8 RecordKeys GroupKeyve 4 GroupKeys vardır RecordKey. Bir çözümün üstel zaman karmaşıklığına sahip olacağını hayal ediyorum, ancak yine de bir çözümle ilgileniyorum.

Yanıtlar:


7

Bu, performans karşılaştırması için yinelemeli bir T-SQL çözümüdür.

Süper grup anahtarını saklamak için tabloya fazladan bir sütun eklenebileceğini ve dizin oluşturmanın değiştirilebileceğini varsayar:

Kurmak

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

Mevcut birincil anahtarın anahtar sırasını tersine çevirebiliyorsanız, ekstra benzersiz dizin gerekli olmayacaktır.

taslak

Bu çözümün yaklaşımı:

  1. Süper grup kimliğini 1 olarak ayarlayın
  2. En düşük numaralı işlenmemiş grup anahtarını bulun
  3. Hiçbiri bulunmadıysa çıkın
  4. Geçerli grup anahtarıyla tüm satırlar için süper grubu ayarlama
  5. Geçerli gruptaki satırlarla ilgili tüm satırlar için süper grubu ayarlama
  6. Hiçbir satır güncellenene kadar 5. adımı tekrarlayın
  7. Geçerli süper grup kimliğini artırma
  8. 2. adıma gidin

uygulama

Satır içi yorumlar:

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

Yürütme planı

Anahtar güncelleme için:

Güncelleme planı

Sonuç

Tablonun son durumu:

╔═══════════════╦══════════╦══════════════╗
║ 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          ║
╚═══════════════╩══════════╩══════════════╝

Gösteri: db <> keman

Performans testleri

Michael Green'in cevabında verilen genişletilmiş test veri setini kullanarak, dizüstü bilgisayarımdaki * zamanlamalar şunlardır:

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

* Microsoft SQL Server 2017 (RTM-CU13), Developer Edition (64 bit), Windows 10 Pro, 16GB RAM, SSD, 4 çekirdekli hiper iş parçacıklı i7, 2.4GHz nominal.


Bu harika bir cevap. Sorumda öngörüldüğü gibi, "büyük günler" için çok yavaş; ama küçük günlerim için harika. 2,5 milyon table satırlık masamda çalışmak yaklaşık 5 saat sürdü.
basketballfan22

10

Bu sorun, öğeler arasındaki bağlantıları izlemekle ilgilidir. Bu onu grafikler ve grafik işleme alanına sokar . Özellikle, tüm veri kümesi bir grafik oluşturur ve biz bu grafiğin bileşenlerini arıyoruz . Bu, sorudan alınan örnek verilerin bir grafiği ile gösterilebilir.

resim açıklamasını buraya girin

Soru, bu değeri paylaşan diğer satırları bulmak için GroupKey veya RecordKey'i izleyebileceğimizi söylüyor. Böylece her ikisini de bir grafikte tepe noktası olarak ele alabiliriz. Soru, Grup Tuşları 1–3'ün aynı Üst GrupKey'e nasıl sahip olduğunu açıklamaya devam eder. Bu, soldaki kümenin ince çizgilerle birleştiği görülebilir. Resim ayrıca orijinal veriler tarafından oluşturulan diğer iki bileşeni (SupergroupKey) göstermektedir.

SQL Server, T-SQL'de yerleşik bazı grafik işleme yeteneğine sahiptir. Ancak şu anda oldukça yetersiz ve bu sorunla yardımcı değil. SQL Server ayrıca R ve Python ve zengin ve sağlam paketler için çağrı yapabilir. Bunlardan biri igraph . Bu için yazılmıştır "hızlı köşeler ve kenarlar (milyonlarca, büyük grafikler taşıma bağlantı )."

R ve igraph I kullanarak lokal testlerde 2 dakika 22 saniyede bir milyon satırı işleyebildim 1 . Mevcut en iyi çözümle şu şekilde karşılaştırır:

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.

1M satırları işlerken, grafiği yüklemek ve işlemek ve tabloyu güncellemek için 1m40'lar kullanıldı. Bir SSMS sonuç tablosunu çıktı ile doldurmak için 42'ler gerekiyordu.

Görev Yöneticisi'nin 1M satırları işlenirken gözlemlenmesi yaklaşık 3GB çalışma belleği gerektiğini gösterir. Bu, disk belleği olmadan bu sistemde mevcuttu.

Ypercube'un özyinelemeli CTE yaklaşımı hakkındaki değerlendirmesini doğrulayabilirim. Birkaç yüz kayıt tuşu ile CPU'nun% 100'ünü ve mevcut tüm RAM'i tüketiyordu. Sonunda tempdb 80GB'ın üzerine çıktı ve SPID çöktü.

Paul'ün masasını SupergroupKey sütunuyla kullandım, böylece çözümler arasında adil bir karşılaştırma var.

Nedense R Poincaré aksanına itiraz etti. Düz bir "e" olarak değiştirilmesine izin verdi. Eldeki probleme almanca olmadığı için araştırmadım. Eminim bir çözüm var.

İşte kod

-- 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 kodunun yaptığı budur

  • @input_data_1 SQL Server'ın verileri tablodan R koduna nasıl aktardığı ve InputDataSet adı verilen bir R veri çerçevesine nasıl çevirdiği.

  • library(igraph) kütüphaneyi R yürütme ortamına aktarır.

  • df.g <- graph.data.frame(d = InputDataSet, directed = FALSE)verileri bir igraph nesnesine yükleyin. Bu, yönlendirilmemiş bir grafiktir, çünkü gruptan kayda veya gruba kayıt bağlantılarını takip edebiliriz. InputDataSet, SQL Server'ın R'ye gönderilen veri kümesi için varsayılan adıdır.

  • cpts <- components(df.g, mode = c("weak")) ayrık alt grafikler (bileşenler) ve diğer ölçümleri bulmak için grafiği işleyin.

  • OutputDataSet <- data.frame(cpts$membership)SQL Server, R'den bir veri çerçevesi bekler. Varsayılan adı OutputDataSet'dir. Bileşenler "üyelik" adı verilen bir vektörde saklanır. Bu ifade vektörü bir veri çerçevesine çevirir.

  • OutputDataSet$VertexName <- V(df.g)$nameV (), grafikteki köşe noktalarının bir vektörüdür - GroupKeys ve RecordKeys listesi. Bu, onları çıkış veri çerçevesine kopyalayarak VertexName adlı yeni bir sütun oluşturur. Bu, SupergroupKey'i güncellemek için kaynak tabloyla eşleştirmek için kullanılan anahtardır.

Ben bir R uzmanı değilim. Muhtemelen bu optimize edilebilir.

Test verisi

Doğrulama için OP verileri kullanıldı. Ölçek testleri için aşağıdaki betiği kullandım.

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

Şimdilik oranları OP'nin tanımından yanlış bir şekilde aldığımı fark ettim. Bunun zamanlamaları etkileyeceğine inanmıyorum. Kayıtlar ve Gruplar bu işlem için simetriktir. Algoritmaya hepsi bir grafikteki düğümlerdir.

Verilerin testinde her zaman tek bir bileşen oluştu. Bunun verilerin eşit dağılımından kaynaklandığına inanıyorum. Eğer üretim rutinine sabit olarak kodlanmış statik 1: 8 oranı yerine oranın değişmesine izin vermiş olsaydı, daha fazla bileşen olabilirdi.



1 Makine özellikleri: Microsoft SQL Server 2017 (RTM-CU12), Geliştirici Sürümü (64 bit), Windows 10 Home. 16 GB RAM, SSD, 4 çekirdekli hiper iş parçacıklı i7, 2,8 GHz nominal. Testler, normal sistem etkinliği (yaklaşık% 4 CPU) dışında, o sırada çalışan tek öğelerdi.


6

Özyinelemeli bir CTE yöntemi - büyük tablolarda korkunç derecede verimsiz olması muhtemeldir:

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

Dbfiddle.uk içinde test edildi

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.