Đây là một cú đâm vào một thuật toán. Nó không hoàn hảo, và tùy thuộc vào thời gian bạn muốn dành cho việc tinh chỉnh nó, có thể có một số lợi ích nhỏ hơn nữa sẽ được thực hiện.
Giả sử bạn có một bảng các nhiệm vụ được thực hiện bởi bốn hàng đợi. Bạn biết khối lượng công việc liên quan đến việc thực hiện từng nhiệm vụ và bạn muốn cả bốn hàng đợi có được khối lượng công việc gần như bằng nhau, vì vậy tất cả các hàng đợi sẽ hoàn thành cùng một lúc.
Trước hết, tôi phân vùng các tác vụ bằng cách sử dụng một cách hợp lý, được sắp xếp theo kích thước của chúng, từ nhỏ đến lớn.
SELECT [time], ROW_NUMBER() OVER (ORDER BY [time])%4 AS grp, 0
Các ROW_NUMBER()
đơn hàng mỗi hàng theo kích thước, sau đó gán một số hàng, bắt đầu từ 1. Số hàng này được gán một "nhóm" ( grp
cột) trên cơ sở luân chuyển vòng. Hàng đầu tiên là nhóm 1, hàng thứ hai là nhóm 2, sau đó 3, hàng thứ tư là nhóm 0, v.v.
time ROW_NUMBER() grp
---- ------------ ---
1 1 1
10 2 2
12 3 3
15 4 0
19 5 1
22 6 2
...
Để dễ sử dụng, tôi đang lưu trữ các cột time
và grp
cột trong một biến bảng được gọi @work
.
Bây giờ, chúng ta có thể thực hiện một vài tính toán trên dữ liệu này:
WITH cte AS (
SELECT *, SUM([time]) OVER (PARTITION BY grp)
-SUM([time]) OVER (PARTITION BY (SELECT NULL))/4 AS _grpoffset
FROM @work)
...
Cột _grpoffset
là tổng số time
trên mỗi grp
khác với trung bình "lý tưởng". Nếu tổng số time
của tất cả các nhiệm vụ là 1000 và có bốn nhóm, lý tưởng nhất là có tổng số 250 trong mỗi nhóm. Nếu một nhóm chứa tổng cộng 268, thì nhóm đó _grpoffset=18
.
Ý tưởng là xác định hai hàng tốt nhất, một trong nhóm "tích cực" (có quá nhiều công việc) và một trong nhóm "tiêu cực" (với quá ít công việc). Nếu chúng ta có thể hoán đổi các nhóm trên hai hàng đó, chúng ta có thể giảm tuyệt đối _grpoffset
của cả hai nhóm.
Thí dụ:
time grp total _grpoffset
---- --- ----- ----------
3 1 222 40
46 1 222 40
73 1 222 40
100 1 222 40
6 2 134 -48
52 2 134 -48
76 2 134 -48
11 3 163 -21
66 3 163 -21
86 3 163 -21
45 0 208 24
71 0 208 24
92 0 208 24
----
=727
Với tổng cộng 727, mỗi nhóm nên có số điểm khoảng 182 để phân phối trở nên hoàn hảo. Sự khác biệt giữa điểm số của nhóm và 182 là những gì chúng tôi đưa vào _grpoffset
cột.
Như bạn có thể thấy bây giờ, trong thế giới tốt nhất, chúng ta nên di chuyển khoảng 40 điểm giá trị của các hàng từ nhóm 1 sang nhóm 2 và khoảng 24 điểm từ nhóm 3 sang nhóm 0.
Đây là mã để xác định các hàng ứng cử viên đó:
SELECT TOP 1 pos._row AS _pos_row, pos.grp AS _pos_grp,
neg._row AS _neg_row, neg.grp AS _neg_grp
FROM cte AS pos
INNER JOIN cte AS neg ON
pos._grpoffset>0 AND
neg._grpoffset<0 AND
--- To prevent infinite recursion:
pos.moved<4 AND
neg.moved<4
WHERE --- must improve positive side's offset:
ABS(pos._grpoffset-pos.[time]+neg.[time])<=pos._grpoffset AND
--- must improve negative side's offset:
ABS(neg._grpoffset-neg.[time]+pos.[time])<=ABS(neg._grpoffset)
--- Largest changes first:
ORDER BY ABS(pos.[time]-neg.[time]) DESC
) AS x ON w._row IN (x._pos_row, x._neg_row);
Tôi tự tham gia biểu thức bảng chung mà chúng ta đã tạo trước đây cte
: Một mặt, các nhóm có mặt tích cực _grpoffset
, mặt khác là các nhóm âm. Để tiếp tục lọc ra những hàng nào được cho là khớp với nhau, sự hoán đổi của các mặt tích cực và tiêu cực phải được cải thiện _grpoffset
, tức là làm cho nó gần hơn với 0.
Các TOP 1
và ORDER BY
lựa chọn phù hợp "tốt nhất" để trao đổi đầu tiên.
Bây giờ, tất cả những gì chúng ta cần là thêm UPDATE
và lặp lại cho đến khi không còn tìm thấy tối ưu hóa nữa.
TL; DR - đây là truy vấn
Đây là mã hoàn chỉnh:
DECLARE @work TABLE (
_row int IDENTITY(1, 1) NOT NULL,
[time] int NOT NULL,
grp int NOT NULL,
moved tinyint NOT NULL,
PRIMARY KEY CLUSTERED ([time], _row)
);
WITH cte AS (
SELECT 0 AS n, CAST(1+100*RAND(CHECKSUM(NEWID())) AS int) AS [time]
UNION ALL
SELECT n+1, CAST(1+100*RAND(CHECKSUM(NEWID())) AS int) AS [time]
FROM cte WHERE n<100)
INSERT INTO @work ([time], grp, moved)
SELECT [time], ROW_NUMBER() OVER (ORDER BY [time])%4 AS grp, 0
FROM cte;
WHILE (@@ROWCOUNT!=0)
WITH cte AS (
SELECT *, SUM([time]) OVER (PARTITION BY grp)
-SUM([time]) OVER (PARTITION BY (SELECT NULL))/4 AS _grpoffset
FROM @work)
UPDATE w
SET w.grp=(CASE w._row
WHEN x._pos_row THEN x._neg_grp
ELSE x._pos_grp END),
w.moved=w.moved+1
FROM @work AS w
INNER JOIN (
SELECT TOP 1 pos._row AS _pos_row, pos.grp AS _pos_grp,
neg._row AS _neg_row, neg.grp AS _neg_grp
FROM cte AS pos
INNER JOIN cte AS neg ON
pos._grpoffset>0 AND
neg._grpoffset<0 AND
--- To prevent infinite recursion:
pos.moved<4 AND
neg.moved<4
WHERE --- must improve positive side's offset:
ABS(pos._grpoffset-pos.[time]+neg.[time])<=pos._grpoffset AND
--- must improve negative side's offset:
ABS(neg._grpoffset-neg.[time]+pos.[time])<=ABS(neg._grpoffset)
--- Largest changes first:
ORDER BY ABS(pos.[time]-neg.[time]) DESC
) AS x ON w._row IN (x._pos_row, x._neg_row);