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.
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)$name
V () 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).