Рішення для присвоєння унікальних значень рядкам із обмеженою дистанцією співпраці


9

У мене є таблиця, яку можна створити та заповнити таким кодом:

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

Для всіх рядків, які мають кінцеву відстань співпраці на основі RecordKeyіншого рядка, я хотів би призначити унікальне значення - мені не важливо, як і для якого типу даних є унікальне значення.

Правильний набір результатів, який відповідає тому, про що я прошу, може бути створений за допомогою наступного запиту:

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;

Щоб краще допомогти тому, про що я прошу, я поясню, чому GroupKeys 1–3 мають те саме SupergroupKey:

  • GroupKey1 містить RecordKeyЕйлера, який, у свою чергу, міститься у GroupKey2; таким чином GroupKeys 1 і 2 повинні мати однакові SupergroupKey.
  • Оскільки Гаусс міститься в GroupKeys 2 і 3, вони теж повинні мати однакове SupergroupKey. Це призводить до того, що GroupKeys 1–3 має однакове SupergroupKey.
  • Оскільки GroupKeys 1–3 не ділиться жодним RecordKeysм з рештою GroupKeys, вони єдині присвоюють SupergroupKeyзначення 1.

Додам, що рішення має бути загальним. Наведена вище таблиця та набір результатів були лише прикладом.

Додаток

Я зняв вимогу, щоб рішення не було ітераційним. Хоча я вважаю за краще таке рішення, я вважаю, що це необгрунтоване обмеження. На жаль, я не в змозі використовувати жодне рішення на основі CLR; але якщо ви хочете включити таке рішення, не соромтеся. Я, швидше за все, не сприйму це як відповідь.

Кількість рядків у моїй справжній таблиці становить цілих 5 мільйонів, але бувають дні, коли кількість рядків буде "лише" близько десяти тисяч. В середньому це 8 RecordKeyс GroupKeyта 4 GroupKeyс на RecordKey. Я думаю, що рішення матиме експоненціальну часову складність, але все ж мене цікавить рішення.

Відповіді:


7

Це ітеративне рішення T-SQL для порівняння продуктивності.

Передбачається, що в таблицю може бути доданий додатковий стовпець для зберігання ключа супергрупи, а індексація може бути змінена:

Налаштування

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

Якщо ви зможете змінити порядок клавіш цього первинного ключа, додатковий унікальний індекс не буде потрібен.

Структура

Підхід цього рішення:

  1. Встановіть ідентифікатор супергрупи на 1
  2. Знайдіть груповий ключ з найменшим числом
  3. Якщо жодного не знайдено, вийдіть
  4. Встановіть супергрупу для всіх рядків за допомогою поточної клавіші групи
  5. Встановіть супер групу для всіх рядків , що відносяться до рядків в поточній групі
  6. Повторіть крок 5, поки жодні рядки не будуть оновлені
  7. Збільшення поточного ідентифікатора супергрупи
  8. Перейдіть до кроку 2

Впровадження

Коментарі вбудовані:

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

План виконання

Для ключового оновлення:

Оновити план

Результат

Кінцевий стан таблиці:

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

Демонстрація: db <> fiddle

Тести на продуктивність

Використання розширеного набору тестових даних , представлений в Майкла Гріна відповіді , таймінги на моєму ноутбуці * є:

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

* Microsoft SQL Server 2017 (RTM-CU13), версія для розробників (64-розрядна), Windows 10 Pro, 16 Гб оперативної пам’яті, SSD, 4-ядерний гіпертоковий i7, номінал 2,4 ГГц.


Це приголомшлива відповідь. Як передбачив у моєму питанні, це занадто повільно для "великих днів"; але це чудово для моїх менших днів. Минуло близько 5 годин, щоб пробігти на моєму столі ≈2,5 млн. Рядків.
basketballfan22

10

Ця проблема стосується наступних зв’язків між елементами. Це ставить його у сферу графіків та обробки графіків. Зокрема, весь набір даних формує графік, і ми шукаємо компоненти цього графіка. Це можна проілюструвати графіком зразкових даних із запитання.

введіть тут опис зображення

Питання говорить, що ми можемо слідувати GroupKey або RecordKey, щоб знайти інші рядки, які поділяють це значення. Тож ми можемо трактувати обидві як вершини у графі. Питання продовжує пояснювати, як GroupKeys 1–3 мають той самий SupergroupKey. Це можна побачити як кластер ліворуч, з'єднаний тонкими лініями. На малюнку також показані два інших компоненти (SupergroupKey), сформовані за вихідними даними.

SQL Server має деякі можливості обробки графіків, вбудовані в T-SQL. На даний момент це досить мізерно, але не корисно з цією проблемою. SQL Server також має можливість виклику R та Python, а також багатий та надійний набір пакетів, доступних для них. Одним із таких є іграф . Він написаний для "швидкої обробки великих графіків, з мільйонами вершин і ребер ( посилання )".

Використовуючи R і igraph, я зміг обробити один мільйон рядків за 2 хвилини 22 секунди в локальному тестуванні 1 . Ось як це порівнюється з поточним найкращим рішенням:

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.

Під час обробки 1М рядків 1m40 були використані для завантаження та обробки графіка та оновлення таблиці. 42-х потрібно було заповнити таблицю результатів SSMS результатом.

Спостереження диспетчера завдань під час обробки 1М рядків свідчить про необхідність близько 3 ГБ робочої пам'яті. Це було доступно в цій системі без підказок.

Я можу підтвердити оцінку Ypercube щодо рекурсивного підходу CTE. За допомогою декількох сотень ключів запису він спожив 100% процесора та всієї доступної оперативної пам’яті. Врешті-решт tempdb виріс до понад 80 ГБ, і SPID зазнав аварії.

Я використав таблицю Павла зі стовпцем SupergroupKey, щоб було справедливе порівняння між рішеннями.

Чомусь Р заперечив наголос на Пуанкаре. Змінення його на звичайне "е" дозволило йому працювати. Я не досліджував, оскільки це не є проблемою. Я впевнений, що є рішення.

Ось код

-- 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-код

  • @input_data_1 це те, як SQL Server передає дані з таблиці в R-код і переводить їх у рамку даних R, що називається InputDataSet.

  • library(igraph) імпортує бібліотеку в середовище виконання R.

  • df.g <- graph.data.frame(d = InputDataSet, directed = FALSE)завантажте дані в об’єкт igraph. Це непрямий графік, оскільки ми можемо переходити посилання з групи для запису або запису до групи. InputDataSet - це ім'я SQL Server за замовчуванням для набору даних, надісланого R.

  • cpts <- components(df.g, mode = c("weak")) обробити графік, щоб знайти дискретні під графіки (компоненти) та інші заходи.

  • OutputDataSet <- data.frame(cpts$membership)SQL Server очікує повернення кадру даних від R. Його ім'я за замовчуванням - OutputDataSet. Компоненти зберігаються у векторі, який називається "членство". Це твердження переводить вектор у кадр даних.

  • OutputDataSet$VertexName <- V(df.g)$nameV () - вектор вершин у графіку - список GroupKeys та RecordKeys. Це копіює їх у кадр даних про вихід, створюючи новий стовпець під назвою VertexName. Це ключ, який використовується для узгодження з вихідною таблицею для оновлення SupergroupKey.

Я не експерт з питань R Ймовірно, це можна оптимізувати.

Дані тесту

Дані ОП були використані для перевірки. Для масштабних тестів я використовував наступний сценарій.

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

Я тільки що зрозумів, що отримав коефіцієнти неправильним шляхом із визначення ОП. Я не вірю, що це вплине на терміни. Записи та групи симетричні цьому процесу. Для алгоритму всі вони просто вузли в графіку.

При тестуванні даних незмінно формується один компонент. Я вважаю, що це пов'язано з рівномірним розподілом даних. Якби замість статичного співвідношення 1: 8, жорстко зафіксованого в режимі генерації, я дозволив би співвідношенню змінюватися там, швидше за все, були б інші компоненти.



1 Технічні характеристики: Microsoft SQL Server 2017 (RTM-CU12), версія для розробників (64-розрядні), Windows 10 Home. 16 Гб оперативної пам’яті, SSD, 4-ядерний гіпертоковий i7, номінал 2,8 ГГц. Тести були єдиними запущеними на той час елементами, крім нормальної системної активності (близько 4% процесора).


6

Рекурсивний метод CTE - це, ймовірно, буде неефективно у великих таблицях:

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.