Pilih data yang dibagi dalam kelompok yang didistribusikan secara merata berdasarkan nilai

8

Saya ingin memilih ke dalam 4 kelompok data dari sebuah tabel yang memiliki jumlah nilai dalam kelompok yang terdistribusi secara merata. Saya yakin bahwa saya tidak menjelaskannya dengan cukup jelas sehingga saya akan mencoba memberikan contoh.

Di sini saya menggunakan NTILE (4) untuk membuat 4 grup:

SELECT Time, NTILE(4) OVER (ORDER BY Time DESC) AS N FROM TableX

Time -  N
-------------
10  -   1
 9  -   2
 8  -   3
 7  -   4
 6  -   1
 5  -   2
 4  -   3
 3  -   4
 2  -   1
 1  -   2

Dalam kueri dan hasil di atas, kolom lainnya telah dihilangkan karena singkatnya.

Jadi Anda dapat melihat grup juga sebagai berikut:

  1    2    3    4
---  ---  ---  ---
 10    9    8    7
  6    5    4    3
  2    1    
---  ---  ---  ---
 18   15   12   10  Sum Totals of Time

Perhatikan bahwa Jumlah Total Waktu menggunakan NTile tidak benar-benar seimbang antara kelompok. Distribusi nilai Waktu yang lebih baik misalnya:

  1    2    3    4
---  ---  ---  ---
 10    9    8    7
  3    5    4    6
  1         2
---  ---  ---  ---
 14   14   14   13  Sum Totals of Time

Di sini Jumlah Total Waktu lebih merata di 4 kelompok.

Bagaimana saya bisa melakukan ini melalui pernyataan TSQL?

Selanjutnya saya harus mengatakan bahwa saya menggunakan SQL Server 2012. Jika Anda memiliki sesuatu yang dapat membantu saya, beri tahu saya.

Semoga hari Anda menyenangkan.

Stan

iStan
sumber
Apakah nilai Anda selalu bilangan bulat? Jika demikian, apakah mereka dalam rangkaian berkelanjutan atau dapatkah ada kesenjangan? Nilai unik?
Daniel Hutmacher
Hai, ya mereka bilangan bulat, dan tidak mereka tidak kontinu, beberapa mungkin kesenjangan ganda dan yakin di antara mereka. Bayangkan mereka bahwa ini adalah waktu yang diperlukan untuk melakukan operasi untuk item tertentu (item tertentu adalah kolom yang dihilangkan).
iStan

Jawaban:

14

Berikut adalah bacokan pada suatu algoritma. Itu tidak sempurna, dan tergantung pada berapa banyak waktu yang ingin Anda habiskan untuk memurnikannya, mungkin ada beberapa keuntungan kecil yang akan dibuat.

Anggap Anda memiliki daftar tugas yang harus dilakukan oleh empat antrian. Anda tahu jumlah pekerjaan yang terkait dengan melakukan setiap tugas, dan Anda ingin keempat antrian mendapatkan jumlah pekerjaan yang hampir sama, sehingga semua antrian akan selesai pada waktu yang sama.

Pertama, saya akan mempartisi tugas menggunakan modulous, dipesan berdasarkan ukurannya, dari kecil ke besar.

SELECT [time], ROW_NUMBER() OVER (ORDER BY [time])%4 AS grp, 0

The ROW_NUMBER()pesanan setiap baris dengan ukuran, kemudian memberikan nomor baris, mulai dari 1. nomor baris ini diberikan sebuah "kelompok" (the grpkolom) secara round-robin. Baris pertama adalah grup 1, baris kedua adalah grup 2, lalu 3, keempat mendapat grup 0, dan seterusnya.

time ROW_NUMBER() grp
---- ------------ ---
   1            1   1
  10            2   2
  12            3   3
  15            4   0
  19            5   1
  22            6   2
...

Untuk kemudahan penggunaan, saya menyimpan timedan grpkolom dalam variabel tabel bernama @work.

Sekarang, kita dapat melakukan beberapa perhitungan pada data ini:

WITH cte AS (
    SELECT *, SUM([time]) OVER (PARTITION BY grp)
             -SUM([time]) OVER (PARTITION BY (SELECT NULL))/4 AS _grpoffset
    FROM @work)
...

Kolom _grpoffsetadalah jumlah total timeper grpberbeda dari rata-rata "ideal". Jika total timesemua tugas adalah 1000 dan ada empat kelompok, idealnya ada total 250 dalam setiap kelompok. Jika grup berisi total 268, grup itu _grpoffset=18.

Idenya adalah untuk mengidentifikasi dua baris terbaik, satu di kelompok "positif" (dengan terlalu banyak pekerjaan) dan satu di kelompok "negatif" (dengan terlalu sedikit pekerjaan). Jika kita dapat bertukar grup pada dua baris itu, kita dapat mengurangi absolut _grpoffsetkedua grup.

Contoh:

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

Dengan total total 727, setiap kelompok harus memiliki skor sekitar 182 untuk distribusinya menjadi sempurna. Perbedaan antara skor grup dan 182 adalah apa yang kami tempatkan di _grpoffsetkolom.

Seperti yang Anda lihat sekarang, di dunia terbaik, kita harus memindahkan sekitar 40 poin nilai baris dari grup 1 ke grup 2 dan sekitar 24 poin dari grup 3 ke grup 0.

Berikut kode untuk mengidentifikasi baris kandidat tersebut:

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

Saya menggabungkan diri dengan ekspresi tabel umum yang kami buat sebelumnya,: cteDi satu sisi, grup dengan positif _grpoffset, di sisi lain grup dengan yang negatif. Untuk lebih lanjut menyaring baris mana yang seharusnya cocok satu sama lain, swap dari baris sisi positif dan negatif harus ditingkatkan _grpoffset, yaitu membuatnya lebih dekat ke 0.

The TOP 1dan ORDER BYmemilih "terbaik" pertandingan swap pertama.

Sekarang, yang perlu kita lakukan hanyalah menambahkan UPDATE, dan memutarnya sampai tidak ada lagi optimasi yang ditemukan.

TL; DR - inilah pertanyaannya

Berikut kode lengkapnya:

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);
Daniel Hutmacher
sumber