Головоломка: равномерное распределение чисел по группам
это скорее головоломка на самом деле. Его, вероятно, спрашивали в другом месте раньше, но я ничего не мог найти, поэтому решил поделиться вопросом.
Я пытаюсь реализовать какую-то балансировку нагрузки в приложении и уменьшил проблему до того, что я считаю простым упражнением TSQL (приложение преимущественно находится в домене SQL Server (SQL Server 2008 R2)).
в принципе у меня есть таблица с двумя целыми числами; уникальный порядковый идентификатор и не уникальное значение. Таблица может содержать любое количество записей, и я хотел бы создать таблицу данных, в которой первые n самых больших значений разбиваются на отдельные "группировки", а затем второй набор n самых больших значений разбивается на отдельные "группировки".
у меня есть первый проект, работающий ниже, но я считаю, что его можно улучшить...
DECLARE @GroupCount INT = 5
-- Set up the test data
DECLARE @test TABLE (Id INT IDENTITY(1, 1), Value INT)
INSERT @Test (Value)
VALUES (100), (456), (121), (402), (253), (872), (765), (6529), (1029), (342), (98), (1), (0), (4), (46), (23), (456), (416), (2323), (4579)
--Order by Value descending
;WITH cte AS
(
SELECT *
,ROW_NUMBER() OVER (ORDER BY Value DESC) RowNum
FROM @Test
)
--use modulus to split into grouping
, cte2 AS
(
SELECT *
,ROW_NUMBER() OVER (PARTITION BY RowNum % @GroupCount ORDER BY RowNum DESC) Rnk
FROM cte
)
SELECT ROW_NUMBER() OVER (PARTITION BY Rnk ORDER BY Value DESC) AS 'Grouping'
,Value
,Id
FROM cte2
ORDER BY [Grouping], Value ASC
это работает и создает следующий набор данных:
Grouping, Value, Id
======== ===== ==
1 46 15
1 342 10
1 765 7
1 6529 8
2 23 16
2 253 5
2 456 2
2 4579 20
3 4 14
3 121 3
3 456 17
3 2323 19
4 1 12
4 100 1
4 416 18
4 1029 9
5 0 13
5 98 11
5 402 4
5 872 6
возвращаемый набор данных верен в том, что первое n наибольшие значения разбиваются на отдельные группы и т. д., но общие значения в каждой группе сильно отличаются в группе 1 по сравнению с группой 5 (например).
когда сгруппированы и суммированы, мы можем видеть un-even spread:
Grouping, SummedValues
======== ============
1 7682
2 5311
3 2904
4 1546
5 1372
как можно меньше строк, как я могу лучше сбалансировать значения, чтобы общие значения в каждой группе распределялись более равномерно?
5 ответов
это неправильно,но не страшно для данных примера. Ваш пробег может отличаться.
declare @groupcount int = 5;
create table t (id int identity(1, 1), value int);
insert t values
(100),(456),(121),(402),(253),(872),(765),(6529),(1029),(342)
, (98),(1),(0),(4),(46),(23),(456),(416),(2323),(4579);
;with cte as (
select *
, rn = row_number() over (order by value asc)
, pct = value/sum(value+.0) over()
, target = 1.0 / @groupcount
from t
)
, remaining as (
select id, value, rn
, grp = convert(int,(sum(value) over (order by rn)/sum(value+.0) over())*@groupCount)+1
from cte
)
select
grp = row_number() over (order by sum(value) desc)
, sumValue = sum(value)
from remaining
group by grp
rextester demo:http://rextester.com/UNV61100
результаты:
+-----+----------+
| grp | sumValue |
+-----+----------+
| 1 | 6529 |
| 2 | 4579 |
| 3 | 3483 |
| 4 | 2323 |
| 5 | 1901 |
+-----+----------+
Совместимая версия Sql Server 2008:
declare @groupcount int = 5;
create table t (id int identity(1, 1), value int);
insert t values
(100),(456),(121),(402),(253),(872),(765),(6529),(1029),(342)
, (98),(1),(0),(4),(46),(23),(456),(416),(2323),(4579);
;with cte as (
select *
, rn = row_number() over (order by value asc)
, pct = value/tv.TotalValue
, target = 1.0 / @groupcount
from t
cross join (select TotalValue = sum(value+.0) from t) tv
)
, remaining as (
select id, value, rn
, grp = convert(int,((x.sumValueOver/TotalValue)*@groupcount)+1)
from cte
outer apply (
select sumValueOver = sum(value)
from cte i
where i.rn <= cte.rn
) x
)
select
grp = row_number() over (order by sum(value) desc)
, sumValue = sum(value)
from remaining
group by grp
rextester demo:http://rextester.com/DEUDJ77007
возвращает:
+-----+----------+
| grp | sumValue |
+-----+----------+
| 1 | 6529 |
| 2 | 4579 |
| 3 | 3483 |
| 4 | 2323 |
| 5 | 1901 |
+-----+----------+
здесь NTILE
функция в sql server может вам помочь.
DECLARE @GroupCount INT = 5
-- Set up the test data
DECLARE @test TABLE (Id INT IDENTITY(1, 1), Value INT)
INSERT @Test (Value)
SELECT 100
UNION ALL
SELECT 456
UNION ALL
SELECT 121
UNION ALL
SELECT 402
UNION ALL
SELECT 253
UNION ALL
SELECT 872
UNION ALL
SELECT 765
UNION ALL
SELECT 6529
UNION ALL
SELECT 1029
UNION ALL
SELECT 342
UNION ALL
SELECT 98
UNION ALL
SELECT 1
UNION ALL
SELECT 0
UNION ALL
SELECT 4
UNION ALL
SELECT 46
UNION ALL
SELECT 23
UNION ALL
SELECT 456
UNION ALL
SELECT 416
UNION ALL
SELECT 2323
UNION ALL
SELECT 4579
;With cte
AS
(
SELECT *, NTILE(@GroupCount) OVER(ORDER BY Value DESC) AS GroupNo FROM @Test
)
SELECT GroupNo, SUM(Value) AS SummedValues FROM cte
GROUP BY GroupNo
и я получаю такой результат.
GroupNo SummedValues
--------------------
1 14460
2 2549
3 1413
4 365
5 28
немного лучший способ сделать это было бы "змея" выбор. Вы выстраиваетесь в очередь на 1 - й, 6-й, 11-й самый высокий-конечно, это намного выше, чем 5-й, 10-й, 15-й.
лучше будет 1-й, 10-й, 11-й, против 5-го, 6-го, 15-го. Все еще не идеально, и с вашими конкретными данными все еще очень плохо, но немного лучше, чем ваше.
DECLARE @GroupCount INT = 5
-- Set up the test data
DECLARE @test TABLE (Id INT IDENTITY(1, 1), Value INT)
INSERT @Test (Value)
SELECT 100
UNION ALL
SELECT 456
UNION ALL
SELECT 121
UNION ALL
SELECT 402
UNION ALL
SELECT 253
UNION ALL
SELECT 872
UNION ALL
SELECT 765
UNION ALL
SELECT 6529
UNION ALL
SELECT 1029
UNION ALL
SELECT 342
UNION ALL
SELECT 98
UNION ALL
SELECT 1
UNION ALL
SELECT 0
UNION ALL
SELECT 4
UNION ALL
SELECT 46
UNION ALL
SELECT 23
UNION ALL
SELECT 456
UNION ALL
SELECT 416
UNION ALL
SELECT 2323
UNION ALL
SELECT 4579
--Order by Value descending
;WITH cte AS
(
SELECT *
,ROW_NUMBER() OVER (ORDER BY Value DESC) RowNum
FROM @Test
)
--use modulus to split into grouping
, cte2 AS
(
SELECT *
,ROW_NUMBER() OVER (PARTITION BY RowNum % (@GroupCount*2 ) ORDER BY RowNum DESC) Rnk
FROM cte
)
select [Grouping], SUM(value) from (
SELECT floor(abs(@GroupCount - (ROW_NUMBER() OVER (PARTITION BY Rnk ORDER BY Value DESC) - 0.5)) + 0.5) AS 'Grouping'
,Value
,Id
FROM cte2
--ORDER BY [Grouping], Value ASC
) a group by [Grouping]
order by [Grouping] ASC
в конечном счете, хотя я думаю, что случайное назначение, вероятно, лучше, чем это, может быть, случайное назначение в то время как проверка того, что сумма еще не равна 2*(1/группировка * итого).
действительно, я думаю, что это не проблема, хорошо решаемая TSQL или любым SQL; языки, которые могут управлять потоком по строкам, лучше послужат вам. Python, C#, SAS, любой другой инструмент, который находится в вашем наборе инструментов. (PL / SQL-это единственное место, куда я хотел бы пойти здесь...)
все, что позволит вам сказать на уровне строк: "отслеживая то, что я назначил до сих пор, назначьте этот конкретный случай ведро с самым низким номером до сих пор " действительно будет работать лучше.
Grouping Summed Values
---------------------
1 1781
2 1608
3 2904
4 5249
5 7273
С помощью ntile
и row_number
окно функционирует вместе, чтобы не только разделить его на четные группы (даже по количеству, а не суммировать), но и принять лучшее решение о том, какие значения включать в каждую группу, чтобы выровнять общую сумму в каждой группе как можно больше.
ответ:
select case b.grp_split when 1 then b.grp_split_rnk_desc else grp_split_rnk_asc end as [grouping]
, b.value
, b.id
from (
select a.id
, a.value
, a.grp_split
, row_number() over (partition by a.grp_split order by a.value desc) grp_split_rnk_desc
, row_number() over (partition by a.grp_split order by a.value asc) grp_split_rnk_asc
from (
select t.id
, t.value
, ntile(@ntile_cnt) over (order by t.value desc) as grp_split
from @test as t
) as a
) as b
order by case b.grp_split when 1 then b.grp_split_rnk_desc else grp_split_rnk_asc end asc
, b.value asc
результаты:
не идеально, но немного ближе.
Group Total
1 7029
2 5096
3 2904
4 1761
5 2025
результат является первичным, определяемым первыми наибольшими значениями. Таким образом, вы можете попробовать заказать все остальное в обратном порядке
WITH cte AS
(
SELECT *
,ROW_NUMBER() OVER (ORDER BY Value DESC) RowNum
FROM @Test
)
--use modulus to split into grouping
, cte2 AS
(
SELECT *
,ROW_NUMBER() OVER (PARTITION BY RowNum % @GroupCount ORDER BY RowNum ) Rnk
FROM cte
)
,cte3 AS
(SELECT ROW_NUMBER() OVER (PARTITION BY Rnk ORDER BY case rnk when 1 then Value else -Value end DESC) AS [Grouping]
,Value
,Id
FROM cte2
)
select [Grouping],sum(value)
from cte3
group by [Grouping]
order by [Grouping];
результат
Grouping (No column name)
1 1 7029
2 2 5096
3 3 2904
4 4 1761
5 5 2025