쿼리 챌린지 : 행 개수가 아닌 측정 값을 기반으로 균일 한 크기의 버킷 생성


12

고정 수량의 트럭에 주문을 가능한 한 균등하게 적재하는 문제에 대해 설명하겠습니다.

입력 :

@TruckCount - the number of empty trucks to fill

세트:

OrderId, 
OrderDetailId, 
OrderDetailSize, 
TruckId (initially null)

Orders하나 이상으로 구성 OrderDetails됩니다.

여기서 과제는 TruckId각 레코드에 a 를 할당하는 것입니다.

단일 주문은 트럭간에 분할 할 수 없습니다.

트럭은으로 측정하여 가능한 한 고르게 적재해야합니다 * sum(OrderDetailSize).

* 균등하게 : 가장 적게 적재 된 트럭과 가장 많이 적재 된 트럭 사이에서 달성 가능한 가장 작은 델타. 이 정의에 따르면 1,2,3은 1,1,4보다 균등하게 분포됩니다. 도움이된다면 통계 알고리즘 인 척하여 높이 히스토그램을 생성하십시오.

최대 트럭 적재량에 대한 고려 사항은 없습니다. 이들은 마법의 탄성 트럭입니다. 그러나 트럭 수는 고정되어 있습니다.

분명히 라운드 로빈 할당 명령을 반복하는 솔루션이 있습니다.

그러나 설정 기반 논리로 수행 할 수 있습니까?

저의 주요 관심사는 SQL Server 2014 이상입니다. 그러나 다른 플랫폼을위한 세트 기반 솔루션도 흥미로울 수 있습니다.

Itzik Ben-Gan 영토처럼 느껴집니다 :)

내 실제 응용 프로그램은 논리 CPU 수와 일치하도록 처리 작업 부하를 여러 버킷으로 분산시킵니다. 따라서 각 버킷에는 최대 크기가 없습니다. 특히 업데이트를 통계합니다. 난 그냥 문제를 골라내는 방법으로 문제를 트럭으로 요약하는 것이 더 재미 있다고 생각했다.

CREATE TABLE #OrderDetail (
OrderId int NOT NULL,
OrderDetailId int NOT NULL PRIMARY KEY,
OrderDetailSize tinyint NOT NULL,
TruckId tinyint NULL)

-- Sample Data

INSERT #OrderDetail (OrderId, OrderDetailId, OrderDetailSize)
VALUES
(1  ,100    ,75 ),
(2  ,101    ,5  ),
(2  ,102    ,5  ),
(2  ,103    ,5  ),
(2  ,104    ,5  ),
(2  ,105    ,5  ),
(3  ,106    ,100),
(4  ,107    ,1  ),
(5  ,108    ,11 ),
(6  ,109    ,21 ),
(7  ,110    ,49 ),
(8  ,111    ,25 ),
(8  ,112    ,25 ),
(9  ,113    ,40 ),
(10 ,114    ,49 ),
(11 ,115    ,10 ),
(11 ,116    ,10 ),
(12 ,117    ,15 ),
(13 ,118    ,18 ),
(14 ,119    ,26 )
--> YOUR SOLUTION HERE

-- After assigning Trucks, Measure delta between most and least loaded trucks.
-- Zero is perfect score, however the challenge is a set based solution that will scale, and produce good results, rather
-- than iterative solution that will produce perfect results by exploring every possibility.

SELECT max(TruckOrderDetailSize) - MIN(TruckOrderDetailSize) AS TruckMinMaxDelta
FROM 
(SELECT SUM(OrderDetailSize) AS TruckOrderDetailSize FROM #OrderDetail GROUP BY TruckId) AS Truck


DROP TABLE #OrderDetail



주어진 OrderId에 대해 모든 OrderDetailSize 값이 같습니까? 아니면 샘플 데이터의 동시 발생률입니까?
youcantryreachingme

@youcantryreachingme 아, 좋은 점은 ... 아니, 그건 단지 샘플 데이터에서 우연의 일치 일뿐입니다.
Paul Holmes

답변:


5

내 첫 생각은

select
    <best solution>
from
    <all possible combinations>

"최고의 솔루션"부분은 가장 많이 적재 된 트럭과 가장 적게 적재 된 트럭 사이의 가장 작은 차이라는 질문에 정의되어 있습니다. 다른 비트-모든 조합-생각을 멈추게했습니다.

3 개의 주문 A, B 및 C와 3 대의 트럭이있는 상황을 고려하십시오. 가능성은

Truck 1 Truck 2 Truck 3
------- ------- -------
A       B       C
A       C       B
B       A       C
B       C       A
C       A       B
C       B       A
AB      C       -
AB      -       C
C       AB      -
-       AB      C
C       -       AB
-       C       AB
AC      B       -
AC      -       B
B       AC      -
-       AC      B
B       -       AC
-       B       AC
BC      A       -
BC      -       A
A       BC      -
-       BC      A
A       -       BC
-       A       BC
ABC     -       -
-       ABC     -
-       -       ABC

Table A: all permutations.

이들 중 다수는 대칭입니다. 예를 들어, 처음 6 행은 각 주문이 수행되는 트럭 만 다릅니다. 트럭은 가독성이 있기 때문에 이러한 배치는 동일한 결과를 낳습니다. 지금은 이것을 무시하겠습니다.

순열 및 조합 생성에 대한 알려진 쿼리가 있습니다. 그러나 이것들은 단일 버킷 내에서 배열을 생성합니다. 이 문제를 해결하려면 여러 버킷에서 배열이 필요합니다.

표준 "모든 조합"쿼리의 결과보기

;with Numbers as
(
    select n = 1
    union
    select 2
    union
    select 3
)
select
    a.n,
    b.n,
    c.n
from Numbers as a
cross join Numbers as b
cross join Numbers as c
order by 1, 2, 3;


  n   n   n
--- --- ---
  1   1   1
  1   1   2
  1   1   3
  1   2   1
 <snip>
  3   2   3
  3   3   1
  3   3   2
  3   3   3

Table B: cross join of three values.

결과는 표 A와 동일한 패턴을 형성했습니다. 각 을 Order 1 로 간주하는인지적인 도약 , 어느 Order가 해당 Order를 보유 할 것인지를 나타내는 및 Truck 내의 Order를 배열 하는 을 나타냅니다. 그러면 쿼리가됩니다

select
    Arrangement             = ROW_NUMBER() over(order by (select null)),
    First_order_goes_in     = a.TruckNumber,
    Second_order_goes_in    = b.TruckNumber,
    Third_order_goes_in     = c.TruckNumber
from Trucks a   -- aka Numbers in Table B
cross join Trucks b
cross join Trucks c

Arrangement First_order_goes_in Second_order_goes_in Third_order_goes_in
----------- ------------------- -------------------- -------------------
          1                   1                    1                   1
          2                   1                    1                   2
          3                   1                    1                   3
          4                   1                    2                   1
  <snip>

Query C: Orders in trucks.

예제 데이터에서 14 개의 주문을 다루기 위해 이것을 확장하고 우리가 얻는 이름을 단순화하십시오.

;with Trucks as
(
    select * 
    from (values (1), (2), (3)) as T(TruckNumber)
)
select
    arrangement = ROW_NUMBER() over(order by (select null)),
    First       = a.TruckNumber,
    Second      = b.TruckNumber,
    Third       = c.TruckNumber,
    Fourth      = d.TruckNumber,
    Fifth       = e.TruckNumber,
    Sixth       = f.TruckNumber,
    Seventh     = g.TruckNumber,
    Eigth       = h.TruckNumber,
    Ninth       = i.TruckNumber,
    Tenth       = j.TruckNumber,
    Eleventh    = k.TruckNumber,
    Twelth      = l.TruckNumber,
    Thirteenth  = m.TruckNumber,
    Fourteenth  = n.TruckNumber
into #Arrangements
from Trucks a
cross join Trucks b
cross join Trucks c
cross join Trucks d
cross join Trucks e
cross join Trucks f
cross join Trucks g
cross join Trucks h
cross join Trucks i
cross join Trucks j
cross join Trucks k
cross join Trucks l
cross join Trucks m
cross join Trucks n;

Query D: Orders spread over trucks.

편의상 임시 테이블에 중간 결과를 유지하기로 선택합니다.

데이터가 처음 unpivoted 경우 후속 단계가 훨씬 쉬워집니다.

select
    Arrangement,
    TruckNumber,
    ItemNumber  = case NewColumn
                    when 'First'        then 1
                    when 'Second'       then 2
                    when 'Third'        then 3
                    when 'Fourth'       then 4
                    when 'Fifth'        then 5
                    when 'Sixth'        then 6
                    when 'Seventh'      then 7
                    when 'Eigth'        then 8
                    when 'Ninth'        then 9
                    when 'Tenth'        then 10
                    when 'Eleventh'     then 11
                    when 'Twelth'       then 12
                    when 'Thirteenth'   then 13
                    when 'Fourteenth'   then 14
                    else -1
                end
into #FilledTrucks
from #Arrangements
unpivot
(
    TruckNumber
    for NewColumn IN 
    (
        First,
        Second,
        Third,
        Fourth,
        Fifth,
        Sixth,
        Seventh,
        Eigth,
        Ninth,
        Tenth,
        Eleventh,
        Twelth,
        Thirteenth,
        Fourteenth
    )
) as q;

Query E: Filled trucks, unpivoted.

주문 테이블에 조인하여 가중치를 도입 할 수 있습니다.

select
    ft.arrangement,
    ft.TruckNumber,
    TruckWeight = sum(i.Size)
into #TruckWeights
from #FilledTrucks as ft
inner join #Order as i
    on i.OrderId = ft.ItemNumber
group by
    ft.arrangement,
    ft.TruckNumber;

Query F: truck weights

가장 많이 적재 된 트럭과 가장 적게 적재 된 트럭 사이의 차이가 가장 작은 배치를 찾아 질문에 대답 할 수 있습니다.

select
    Arrangement,
    LightestTruck   = MIN(TruckWeight),
    HeaviestTruck   = MAX(TruckWeight),
    Delta           = MAX(TruckWeight) - MIN(TruckWeight)
from #TruckWeights
group by
    arrangement
order by
    4 ASC;

Query G: most balanced arrangements

토론

이것에는 많은 문제가 있습니다. 먼저 무차별 알고리즘입니다. 작업 테이블의 행 수는 트럭 및 주문 수의 지수입니다. #Arrangements의 행 수는 (트럭 수) ^ (주문 수)입니다. 이것은 잘 확장되지 않습니다.

두 번째는 SQL 쿼리에 포함 된 주문 수가 있습니다. 이를 해결하는 유일한 방법은 자체 SQL에 문제가있는 동적 SQL을 사용하는 것입니다. 주문 수가 수천 개이면 생성 된 SQL이 너무 길어질 수 있습니다.

셋째, 협정의 중복성이다. 이로 인해 중간 테이블이 팽창하여 런타임이 크게 증가합니다.

넷째, #Arrangements의 많은 행은 하나 이상의 트럭을 비 웁니다. 최적의 구성이 아닐 수 있습니다. 작성시 이러한 행을 쉽게 필터링 할 수 있습니다. 코드를 단순하고 집중적으로 유지하기 위해 그렇게하지 않기로 선택했습니다.

기업이 채워진 헬륨 baloons를 선적하기 시작하면 위쪽에 이것은 부정적인 무게를 처리합니다!

생각

트럭 및 주문 목록에서 #FilledTrucks를 직접 채울 수있는 방법이 있다면 이러한 문제 중 최악의 상황은 관리가 가능할 것입니다. 슬프게도 저의 상상력은 그 장애물에 걸려 넘어졌습니다. 저의 희망은 미래의 공헌자가 나를 피할 수있는 것을 제공 할 수 있기를 바랍니다.




1 주문에 대한 모든 품목이 동일한 트럭에 있어야한다고 말합니다. 이는 할당 원자가 OrderDetail이 아니라 Order임을 의미합니다. 테스트 데이터에서 다음을 생성했습니다.

select
    OrderId,
    Size = sum(OrderDetailSize)
into #Order
from #OrderDetail
group by OrderId;

문제의 항목에 'Order'또는 'OrderDetail'이라는 레이블을 지정하더라도 솔루션은 동일하게 유지됩니다.


4

실제 요구 사항을 살펴보십시오 (필자는 CPU 세트에서 워크로드 균형을 조정하려고한다고 가정합니다) ...

프로세스를 특정 버킷 / cpus에 사전 할당해야하는 이유가 있습니까? [ 실제 요구 사항 을 이해하려고 시도 중 ]

'통계 업데이트'의 예에서 특정 작업에 얼마나 오래 걸립니까? 주어진 작업이 예상치 못한 지연으로 진행되는 경우 (예를 들어, 계획된 것보다 많은 테이블 / 인덱스 조각화, 장기 실행 사용자 txn은 '통계 업데이트'작업을 차단 함)?


로드 밸런싱 목적으로 나는 일반적으로 작업 목록 (예 : 통계를 업데이트 할 테이블 목록)을 생성하고 해당 목록을 (임시 / 스크래치) 테이블에 배치합니다.

다음과 같이 요구 사항에 따라 테이블 구조를 수정할 수 있습니다.

create table tasks
(id        int             -- auto-increment?

,target    varchar(1000)   -- 'schema.table' to have stats updated, or perhaps ...
,command   varchar(1000)   -- actual command to be run, eg, 'update stats schema.table ... <options>'

,priority  int             -- provide means of ordering operations, eg, maybe you know some tasks will run really long so you want to kick them off first
,thread    int             -- identifier for parent process?
,start     datetime        -- default to NULL
,end       datetime        -- default to NULL
)

다음으로 X 개의 동시 프로세스를 시작하여 실제 'stats 업데이트'작업을 수행하고 각 프로세스는 다음을 수행합니다.

  • tasks테이블 에 독점 잠금을 설정하십시오 (두 개 이상의 프로세스가 작업을 선택하지 않도록하십시오; 상대적으로 수명이 짧은 잠금이어야 함)
  • 'first'행을 찾습니다 start = NULL( 'first'는 사용자에 의해 결정됩니다 (예 : order by priority?)).
  • 행 세트 업데이트 start = getdate(), thread = <process_number>
  • 커밋 업데이트 (및 독점 잠금 해제)
  • 메모 id하고 target/command가치
  • 원하는 작업 target(대체로 실행 command)을 수행하고 완료되면 ...
  • 갱신 tasksend = getdate() where id = <id>
  • 더 이상 수행 할 작업이 없을 때까지 위의 반복

위의 디자인으로 이제는 동적으로 (대부분) 균형 잡힌 작업을 수행했습니다.

노트:

  • 우선 순위가 높은 방법을 제공하여 더 오래 실행되는 작업을 미리 시작할 수 있습니다. 몇 개의 프로세스가 더 오래 실행되는 작업을 수행하는 동안 다른 프로세스는 더 짧은 실행중인 작업 목록을 통해 이탈 할 수 있습니다.
  • 프로세스가 계획되지 않은 지연 (예 : 장기 실행, 사용자 txn 차단)으로 실행되는 경우 다른 프로세스는 '사용 가능한 다음'작업을 계속 끌어 와서 여유 시간을 늘릴 수 있습니다. tasks
  • tasks테이블 디자인은 다른 이점을 제공해야합니다 (예 : 향후 참조를 위해 아카이브 할 수있는 런타임 히스토리, 우선 순위를 수정하는 데 사용할 수있는 런타임 히스토리, 현재 조작 상태 제공 등).
  • '배타적 잠금' tasks이 약간 과도하게 보일 수 있지만 , 동일한 시간에 새 작업을 얻으려고 시도하는 2 개 이상의 프로세스의 잠재적 문제를 계획 해야하므로 작업을 보장해야합니다. 하나의 프로세스에만 지정됩니다 (그렇습니다. RDBMS의 SQL 언어 기능에 따라 콤보 'update / select'문으로 동일한 결과를 얻을 수 있습니다). 새로운 '작업'을 얻는 단계는 빨라야합니다. 즉, '독점 잠금'은 수명이 짧아야하며 실제로 프로세스는 tasks상당히 임의의 방식 으로 충돌 하므로 어쨌든 거의 차단되지 않습니다.

개인적으로, 나는이 tasks테이블 중심 프로세스가 작업 / 프로세스 매핑을 사전에 할당하려고하는 (보통)보다 복잡한 프로세스와는 반대로 ymmv와 같이 구현하고 유지하기가 조금 더 쉽다는 것을 안다.


당신은 너무 분명 당신의 메이크업을 위해, 당신은 다음 주문 / 창고 분포에 다시가는 당신의 트럭을 가질 수 없습니다 예를 생각 해야합니다 (UPS가 / 페덱스 / 등도한다는 것을 염두에두고 다양한 트럭에 주문을 미리 할당 배송 시간과 가스 사용량을 줄이기 위해 배송 경로를 기준으로 할당).

그러나 실제 예 ( '통계 업데이트')에서는 작업 / 프로세스 할당을 동적으로 수행 할 수없는 이유가 없으므로 워크로드 균형을 잡을 수있는 더 나은 기회를 보장합니다 (CPU 전체 및 전체 런타임 감소 측면에서). .

참고 : 나는 실제로 해당 작업을 실행하기 전에 작업을 (로드 밸런싱의 형태로) 미리 할당하려고하는 (IT) 사람들을 보게되며 모든 경우에 미리 할당 프로세스를 지속적으로 조정하여 끊임없이 변화하는 작업 문제 (예 : 테이블 / 인덱스의 조각화 수준, 동시 사용자 활동 등)를 고려합니다.


먼저 'order'를 테이블로, 'orderdetail'을 테이블의 특정 통계로 생각하면 분할하지 않는 이유는 경쟁 버킷 간의 잠금 대기를 피하는 것입니다. Traceflag 7471은이 문제를 해결하도록 설계되었지만 테스트에서 여전히 잠금 문제가있었습니다.
Paul Holmes

나는 원래 매우 가벼운 솔루션을 만들고 싶었습니다. 버킷을 단일 다중 문 SQL 블록으로 생성 한 다음 자체 파괴 SQL 에이전트 작업을 사용하여 각각을 '해고 잊어 버리십시오'. 즉, 큐 관리 작업이 없습니다. 그러나 결과적으로 통계 당 작업량을 쉽게 측정 할 수 없다는 것을 알았습니다. 행 수가 줄어 들지 않았습니다. 행 개수가 한 테이블에서 실제로는 정적 테이블에서 다음 테이블로 IO의 양에 선형으로 매핑되지 않는다는 점을 고려할 때 실제로 놀라운 것은 아닙니다. 따라서이 응용 프로그램의 경우 실제로 제안한대로 활성 대기열 관리를 추가하여 자체 균형을 유지할 수 있습니다.
Paul Holmes

첫 번째 의견에 ... 예, 명령의 세분성에 대한 (명백한) 결정이 있습니다 ... 및 동시성 문제 : 일부 명령을 병렬로 실행하고 결합 된 디스크 읽기의 이점을 얻을 수 있습니까? (약간 가벼운) 동적 대기열 관리는 사전 할당 버킷보다 약간 효율적입니다 .-) 작업 할 수있는 좋은 답변 / 아이디어가 있습니다 ... 제공하는 솔루션을 너무 어렵지 않아야합니다. 적절한로드 밸런싱.
markp-fuso

1

원하는대로 숫자 테이블을 작성하고 채 웁니다. 이것은 한 번만 작성합니다.

 create table tblnumber(number int not null)

    insert into tblnumber (number)
    select ROW_NUMBER()over(order by a.number) from master..spt_values a
    , master..spt_values b

    CREATE unique clustered index CI_num on tblnumber(number)

트럭 테이블 생성

CREATE TABLE #PaulWhiteTruck (
Truckid int NOT NULL)

insert into #PaulWhiteTruck
values(113),(203),(303)

declare @PaulTruckCount int
Select @PaulTruckCount= count(*) from #PaulWhiteTruck

CREATE TABLE #OrderDetail (
id int identity(1,1),
OrderId int NOT NULL,
OrderDetailId int NOT NULL PRIMARY KEY,
OrderDetailSize int NOT NULL,
TruckId int NULL
)

INSERT
#OrderDetail (OrderId, OrderDetailId, OrderDetailSize)
VALUES
(
1 ,100 ,75 ),(2 ,101 ,5 ),
(2 ,102 ,5 ),(2 ,103 ,5 ),
(2 ,104 ,5 ),(2 ,105 ,5 ),
(3 ,106 ,100),(4 ,107 ,1 ),
(5 ,108 ,11 ),(6 ,109 ,21 ),
(7 ,110 ,49 ),(8 ,111 ,25 ),
(8 ,112 ,25 ),(9 ,113 ,40 ),
(10 ,114 ,49 ),(11 ,115 ,10 ),
(11 ,116 ,10 ),(12 ,117 ,15 ),
(13 ,118 ,18 ),(14 ,119 ,26 )

하나의 OrderSummary테이블 을 만들었습니다

create table #orderSummary(id int identity(1,1),OrderId int ,TruckOrderSize int
,bit_value AS
CONVERT
(
integer,
POWER(2, id - 1)
)
PERSISTED UNIQUE CLUSTERED)
insert into #orderSummary
SELECT OrderId, SUM(OrderDetailSize) AS TruckOrderSize
FROM #OrderDetail GROUP BY OrderId

DECLARE @max integer =
POWER(2,
(
SELECT COUNT(*) FROM #orderSummary 
)
) - 1
declare @Delta int
select @Delta= max(TruckOrderSize)-min(TruckOrderSize)   from #orderSummary

델타 값을 확인하고 잘못된 지 알려주십시오.

;WITH cte 
     AS (SELECT n.number, 
                c.* 
         FROM   dbo.tblnumber AS N 
                CROSS apply (SELECT s.orderid, 
                                    s.truckordersize 
                             FROM   #ordersummary AS s 
                             WHERE  n.number & s.bit_value = s.bit_value) c 
         WHERE  N.number BETWEEN 1 AND @max), 
     cte1 
     AS (SELECT c.number, 
                Sum(truckordersize) SumSize 
         FROM   cte c 
         GROUP  BY c.number 
        --HAVING sum(TruckOrderSize) between(@Delta-25) and (@Delta+25) 
        ) 
SELECT c1.*, 
       c.orderid 
FROM   cte1 c1 
       INNER JOIN cte c 
               ON c1.number = c.number 
ORDER  BY sumsize 

DROP TABLE #orderdetail 

DROP TABLE #ordersummary 

DROP TABLE #paulwhitetruck 

CTE1의 결과를 확인할 수 있으며 모든 것이 가능합니다 Permutation and Combination of order along with their size.

여기까지 나의 접근 방식이 정확하다면 누군가 도움이 필요합니다.

보류중인 작업 :

각 그룹에서 고유하고 각 부분 T 가 델타에 가까워 지도록 필터 및 나누기 결과 CTE1를 3 개 부분 ( Truck count) 으로 나눕니다.OrderidruckOrderSize


내 최신 답변을 확인하십시오. 게시하는 동안 하나의 쿼리가 누락되어 아무도 실수하지 않았습니다. 붙여 넣기 및 실행
KumarHarsh
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.