Giải pháp gán giá trị duy nhất cho các hàng có khoảng cách cộng tác hữu hạn


9

Tôi có một bảng có thể được tạo và điền với mã sau đây:

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

Đối với tất cả các hàng có khoảng cách cộng tác hữu hạn dựa trên RecordKeymột hàng khác, tôi muốn chỉ định một giá trị duy nhất mà tôi không quan tâm làm thế nào hoặc loại dữ liệu nào có giá trị duy nhất.

Một tập kết quả chính xác đáp ứng những gì tôi yêu cầu có thể được tạo bằng truy vấn sau:

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;

Để hỗ trợ tốt hơn những gì tôi đang hỏi, tôi sẽ giải thích lý do tại sao GroupKey1 13 có cùng SupergroupKey:

  • GroupKey1 chứa RecordKeyEuler mà lần lượt được chứa trong GroupKey2; do đó GroupKeys 1 và 2 phải có cùng SupergroupKey.
  • Vì Gauss được chứa trong cả GroupKeys 2 và 3, nên chúng cũng phải có cùng SupergroupKey. Điều này dẫn đến GroupKeys 1 Vang3 có cùng SupergroupKey.
  • GroupKey1 1 3 không chia sẻ bất kỳ RecordKeys nào với các GroupKeys còn lại , nên chúng là những người duy nhất được gán SupergroupKeygiá trị 1.

Tôi nên thêm rằng giải pháp cần phải chung chung. Bảng trên và tập kết quả chỉ là một ví dụ.

Phụ lục

Tôi loại bỏ yêu cầu cho giải pháp là không lặp lại. Mặc dù tôi thích một giải pháp như vậy, tôi tin rằng đó là một hạn chế không hợp lý. Thật không may, tôi không thể sử dụng bất kỳ giải pháp dựa trên CLR nào; nhưng nếu bạn muốn bao gồm một giải pháp như vậy, hãy thoải mái. Tôi có thể sẽ không chấp nhận nó như một câu trả lời mặc dù.

Số lượng hàng trong bảng thực của tôi lên tới 5 triệu, nhưng có những ngày số lượng hàng "chỉ" là khoảng mười nghìn. Trung bình có 8 RecordKeys mỗi GroupKeyvà 4 GroupKeys mỗi RecordKey. Tôi tưởng tượng rằng một giải pháp sẽ có độ phức tạp theo thời gian theo cấp số nhân, nhưng dù sao tôi cũng quan tâm đến một giải pháp.

Câu trả lời:


7

Đây là một giải pháp T-SQL lặp để so sánh hiệu suất.

Nó giả định rằng một cột bổ sung có thể được thêm vào bảng để lưu trữ khóa siêu nhóm và việc lập chỉ mục có thể được thay đổi:

Thiết lập

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

Nếu bạn có thể đảo ngược thứ tự khóa của khóa chính hiện tại, chỉ mục duy nhất bổ sung sẽ không được yêu cầu.

Đề cương

Cách tiếp cận của giải pháp này là:

  1. Đặt id nhóm siêu thành 1
  2. Tìm khóa nhóm chưa được đánh số thấp nhất
  3. Nếu không tìm thấy, thoát
  4. Đặt siêu nhóm cho tất cả các hàng bằng khóa nhóm hiện tại
  5. Đặt siêu nhóm cho tất cả các hàng liên quan đến các hàng trong nhóm hiện tại
  6. Lặp lại bước 5 cho đến khi không có hàng nào được cập nhật
  7. Tăng id nhóm siêu hiện tại
  8. Chuyển đến bước 2

Thực hiện

Bình luận nội tuyến:

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

Kế hoạch thực hiện

Đối với bản cập nhật chính:

Cập nhật kế hoạch

Kết quả

Trạng thái cuối cùng của bảng là:

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

Bản trình diễn: db <> fiddle

Kiểm tra hiệu năng

Sử dụng bộ dữ liệu thử nghiệm mở rộng được cung cấp trong câu trả lời của Michael Green , thời gian trên máy tính xách tay của tôi * là:

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

* Microsoft SQL Server 2017 (RTM-CU13), Phiên bản dành cho nhà phát triển (64-bit), Windows 10 Pro, RAM 16 GB, SSD, i7 siêu nhân 4 nhân, 2,4 GHz danh nghĩa.


Đây là một câu trả lời tuyệt vời. Như đã báo trước trong câu hỏi của tôi, nó quá chậm cho "những ngày trọng đại"; nhưng nó thật tuyệt cho những ngày nhỏ hơn của tôi. Mất khoảng 5 giờ để chạy trên bàn của tôi 2,5 triệu hàng.
Basketballfan22

10

Vấn đề này là về các liên kết sau đây giữa các mục. Điều này đặt nó trong lĩnh vực đồ thị và xử lý đồ thị. Cụ thể, toàn bộ dữ liệu tạo thành một biểu đồ và chúng tôi đang tìm kiếm các thành phần của biểu đồ đó. Điều này có thể được minh họa bằng một biểu đồ của dữ liệu mẫu từ câu hỏi.

nhập mô tả hình ảnh ở đây

Câu hỏi cho biết chúng ta có thể theo dõi GroupKey hoặc RecordKey để tìm các hàng khác có chung giá trị đó. Vì vậy, chúng ta có thể coi cả hai là đỉnh trong biểu đồ. Câu hỏi tiếp tục giải thích làm thế nào GroupKeys 1 phép3 có cùng SupergroupKey. Điều này có thể được xem như cụm bên trái được nối bằng các đường mỏng. Hình ảnh cũng cho thấy hai thành phần khác (SupergroupKey) được hình thành bởi dữ liệu gốc.

SQL Server có một số khả năng xử lý đồ thị được tích hợp trong T-SQL. Tại thời điểm này, nó khá ít ỏi, tuy nhiên, và không hữu ích với vấn đề này. SQL Server cũng có khả năng gọi ra R và Python và bộ gói phong phú và mạnh mẽ có sẵn cho chúng. Một trong số đó là igraph . Nó được viết để "xử lý nhanh các đồ thị lớn, với hàng triệu đỉnh và cạnh ( liên kết )."

Sử dụng R và igraph tôi có thể xử lý một triệu hàng trong 2 phút 22 giây trong thử nghiệm cục bộ 1 . Đây là cách nó so sánh với giải pháp tốt nhất hiện tại:

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.

Khi xử lý các hàng 1M, 1m40 đã được sử dụng để tải và xử lý biểu đồ và cập nhật bảng. 42 giây được yêu cầu để đưa vào bảng kết quả SSMS với đầu ra.

Việc quan sát Trình quản lý tác vụ trong khi các hàng 1M được xử lý cho thấy cần khoảng 3 GB bộ nhớ làm việc. Điều này đã có sẵn trên hệ thống này mà không cần phân trang.

Tôi có thể xác nhận đánh giá của Ypercube về phương pháp CTE đệ quy. Với vài trăm phím ghi, nó tiêu thụ 100% CPU và tất cả RAM có sẵn. Cuối cùng tempdb đã tăng lên hơn 80GB và SPID bị sập.

Tôi đã sử dụng bảng của Paul với cột SupergroupKey để có sự so sánh công bằng giữa các giải pháp.

Vì một số lý do, R đã phản đối việc nhấn mạnh vào Poincaré. Thay đổi nó thành một "e" đơn giản cho phép nó chạy. Tôi đã không điều tra vì nó không phải là nguyên nhân của vấn đề. Tôi chắc chắn có một giải pháp.

Đây là mã

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

Đây là những gì mã R làm

  • @input_data_1 là cách SQL Server chuyển dữ liệu từ một bảng sang mã R và dịch nó sang một khung dữ liệu R được gọi là InputDataSet.

  • library(igraph) nhập thư viện vào môi trường thực thi R.

  • df.g <- graph.data.frame(d = InputDataSet, directed = FALSE)tải dữ liệu vào một đối tượng igraph. Đây là một biểu đồ vô hướng vì chúng ta có thể theo các liên kết từ nhóm để ghi hoặc ghi vào nhóm. InputDataSet là tên mặc định của SQL Server cho tập dữ liệu được gửi đến R.

  • cpts <- components(df.g, mode = c("weak")) xử lý biểu đồ để tìm các biểu đồ con rời rạc (các thành phần) và các biện pháp khác.

  • OutputDataSet <- data.frame(cpts$membership)SQL Server mong đợi một khung dữ liệu trở lại từ R. Tên mặc định của nó là OutputDataSet. Các thành phần được lưu trữ trong một vectơ gọi là "thành viên". Câu lệnh này dịch vector vào khung dữ liệu.

  • OutputDataSet$VertexName <- V(df.g)$nameV () là một vectơ của các đỉnh trong biểu đồ - một danh sách GroupKeys và RecordKeys. Điều này sao chép chúng vào khung dữ liệu ouput, tạo một cột mới gọi là VertexName. Đây là khóa được sử dụng để khớp với bảng nguồn để cập nhật SupergroupKey.

Tôi không phải là chuyên gia R. Có khả năng điều này có thể được tối ưu hóa.

Kiểm tra dữ liệu

Dữ liệu của OP đã được sử dụng để xác nhận. Đối với các bài kiểm tra quy mô, tôi đã sử dụng kịch bản sau đây.

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

Bây giờ tôi mới nhận ra mình đã hiểu sai về định nghĩa của OP. Tôi không tin rằng điều này sẽ ảnh hưởng đến thời gian. Hồ sơ & Nhóm là đối xứng với quá trình này. Theo thuật toán, tất cả chúng chỉ là các nút trong biểu đồ.

Trong thử nghiệm, dữ liệu luôn luôn hình thành một thành phần duy nhất. Tôi tin rằng điều này là do sự phân phối thống nhất của dữ liệu. Nếu thay vì tỷ lệ 1: 8 tĩnh được mã hóa cứng thành thói quen tạo thế hệ, tôi đã cho phép tỷ lệ thay đổi , nhiều khả năng sẽ có thêm các thành phần.



1 Thông số máy: Microsoft SQL Server 2017 (RTM-CU12), Phiên bản dành cho nhà phát triển (64-bit), Windows 10 Home. RAM 16 GB, SSD, i7 siêu nhân 4 nhân, tốc độ danh nghĩa 2,8 GHz. Các bài kiểm tra là các mục duy nhất chạy vào thời điểm đó, ngoài hoạt động bình thường của hệ thống (khoảng 4% CPU).


6

Một phương pháp CTE đệ quy - có khả năng kém hiệu quả khủng khiếp trong các bảng lớn:

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

Đã thử nghiệm trong dbfiddle.uk

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.