Sorgu zorluğu: Satır sayısı değil bir hesaplamaya dayalı olarak boyutlandırılmış kovalar oluşturma


12

Sorunu olabildiğince eşit bir şekilde siparişlerle sabit sayıda kamyon yükleme açısından anlatacağım.

girişler:

@TruckCount - the number of empty trucks to fill

Bir set:

OrderId, 
OrderDetailId, 
OrderDetailSize, 
TruckId (initially null)

Ordersbir veya daha fazlasından oluşur OrderDetails.

Buradaki zorluk TruckId, her kayda bir a atamaktır .

Tek bir sipariş kamyonlara bölünemez.

Kamyonlar ölçüldüğünde mümkün olduğunca eşit olarak * yüklenmelidir sum(OrderDetailSize).

* Eşit: En az yüklü kamyon ile en yüklü kamyon arasında ulaşılabilecek en küçük delta. Bu tanıma göre 1,2,3 1,1,4'ten daha eşit bir şekilde dağılmıştır. Eğer yardımcı olursa, istatistik algoritması gibi davranın ve hatta yükseklik histogramları oluşturun.

Maksimum kamyon yükü dikkate alınmaz. Bunlar sihirli elastik kamyonlar. Ancak kamyon sayısı sabittir.

Açıkça yinelenen bir çözüm var - robin tahsis emirleri.

Ama set tabanlı mantık olarak yapılabilir mi?

Ana ilgi alanım SQL Server 2014 veya üzeri. Ancak diğer platformlar için set tabanlı çözümler de ilginç olabilir.

Bu Itzik Ben-Gan bölgesi gibi geliyor :)

Gerçek dünyadaki uygulamam, mantıksal CPU'ların sayısıyla eşleşmesi için bir işleme iş yükünü birkaç bölüme dağıtıyor. Dolayısıyla her bir kova maksimum boyuta sahip değildir. İstatistikleri özellikle günceller. Sorunu çerçeveyi çerçevelemenin bir yolu olarak kamyonlara soyutlamanın daha eğlenceli olduğunu düşündüm.

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

7
Bu klasik çöp kutusu paketleme problemi gibi görünüyor .
Dan Guzman

1
Hugo Kornelis'in üzerinde de iyi bir çalışma var.
Erik Darling

Tüm OrderDetailSize değerleri belirli bir OrderId için eşit mi olacak, yoksa bu sadece örnek verilerinizde ortak olay mıdır?
youcantryreachingme

@youcantryreachingme Ah, iyi bir nokta ... hayır bu sadece örnek verilerdeki ortak insidans.
Paul Holmes

Yanıtlar:


5

İlk düşüncem

select
    <best solution>
from
    <all possible combinations>

"En iyi çözüm" kısmı soruda tanımlanmıştır - en çok yüklü ve en az yüklü kamyonlar arasındaki en küçük fark. Diğer bit - tüm kombinasyonlar - düşünceyi duraklatmama neden oldu.

Üç A, B ve C siparişimiz ve üç kamyonumuz olduğu bir durumu düşünün. Olanaklar

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.

Bunların çoğu simetriktir. Örneğin, ilk altı sıra yalnızca her siparişin hangi kamyonda verildiği konusunda farklılık gösterir. Kamyonlar çalıştırılabilir olduğundan, bu düzenlemeler aynı sonucu verecektir. Şimdilik bunu görmezden geleceğim.

Permütasyonlar ve kombinasyonlar üretmek için bilinen sorgular vardır. Ancak, bunlar tek bir kova içinde düzenlemeler üretecektir. Bu sorun için birden fazla kova üzerinde düzenlemelere ihtiyacım var.

Çıktıya standart "tüm kombinasyonlar" sorgusundan bakma

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

Ben Sonuçlar her dikkate ait congnitive sıçrama yaparak Tablo A'da aynı model oluşturduk kaydetti sütun bir Sipariş olmak 1 , değerler Sipariş ve bu tutacaktır hangi kamyon söylemek satır kamyonların içinde Siparişlerin bir düzenleme olması. Sorgu daha sonra

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.

Bunu örnek verilerdeki on dört Siparişi kapsayacak şekilde genişleterek ve adları basitleştirdiğimizde:

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

Kolaylık sağlamak için ara sonuçları geçici tablolarda tutmayı tercih ederim.

Veriler önce UNPIVOTED ise sonraki adımlar çok daha kolay olacaktır.

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.

Ağırlıklar Siparişler tablosuna katılarak tanıtılabilir.

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

Soru artık en çok yüklenen ve en az yüklenen kamyonlar arasında en küçük farkın bulunduğu düzenlemeler bulunarak cevaplanabilir.

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

Tartışma

Bununla ilgili çok fazla sorun var. İlk olarak bir kaba kuvvet algoritmasıdır. Çalışma tablolarındaki satır sayısı kamyon ve sipariş sayısında üsteldir. #Arrangements içindeki satır sayısı (kamyon sayısı) ^ (sipariş sayısı). Bu iyi ölçeklenmeyecek.

İkincisi, SQL sorgularının içinde gömülü Emir sayısı vardır. Bunun tek yolu, kendine özgü sorunları olan dinamik SQL kullanmaktır. Siparişlerin sayısı binlerdeyse, oluşturulan SQL'in çok uzun olduğu bir zaman gelebilir.

Üçüncüsü, düzenlemelerdeki fazlalıktır. Bu, ara tabloları şişirerek çalışma süresini büyük ölçüde artırır.

Dördüncü olarak, # Düzenlemelerdeki birçok satır bir veya daha fazla kamyonu boş bırakır. Bu muhtemelen en uygun yapılandırma olamaz. Oluşturulduktan sonra bu satırları filtrelemek kolay olurdu. Kodu daha basit ve odaklanmış tutmak için yapmamayı seçtim.

Yukarı tarafta, kuruluşunuz dolu helyum balonları göndermeye başlarsa, negatif ağırlıkları idare eder!

Düşünceler

#FilledTrucks'u doğrudan kamyonlar ve Siparişler listesinden doldurmanın bir yolu olsaydı, bu endişelerin en kötüsünün yönetilebilir olacağını düşünüyorum. Ne yazık ki, bu engelim tökezledi. Ümit ediyorum ki gelecekteki bazı katılımcılar beni kaçıranları temin edebilir.




1 Sipariş için tüm ürünlerin aynı kamyonda olması gerektiğini söylüyorsunuz. Bu, atama atomunun OrderDetail değil Order olduğu anlamına gelir. Bunları test verilerinizden bu şekilde oluşturdum:

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

Bununla birlikte, söz konusu öğeleri 'Sipariş' veya 'SiparişDetay' olarak etiketlememiz fark etmez, çözüm aynı kalır.


4

Gerçek dünya gereksiniminize bakmak (ki iş yükünüzü bir dizi cpus arasında dengelemeye çalıştığını varsayıyorum) ...

İşlemleri belirli kovalara / cpus'a önceden atamanızın bir nedeni var mı? [ Gerçek gereksinimlerinizi anlamaya çalışmak ]

'İstatistik güncellemeleri' örneğiniz için, belirli bir işlemin ne kadar süreceğini nasıl bilebilirsiniz? Belirli bir işlem beklenmedik bir gecikmeyle karşılaşırsa (ör. Tablo / dizinin planlanandan fazla / aşırı parçalanması, uzun süredir devam eden kullanıcı txn 'istatistik güncelleme' işlemini engeller) ne olur?


Yük dengeleme amacıyla, genellikle görevlerin listesini (örneğin, istatistiklerin güncellenmesi için tabloların listesi) oluşturur ve adı geçen listeyi (geçici / çizik) bir tabloya yerleştiririm.

Tablonun yapısı gereksinimlerinize göre değiştirilebilir, örneğin:

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
)

Daha sonra, gerçek 'istatistik güncellemeleri' işlemlerini gerçekleştirmek için X sayısıyla eşzamanlı işlem başlatıyorum ve her işlem aşağıdakileri gerçekleştiriyor:

  • tasksmasaya özel kilit yerleştirin (hiçbir görevin birden fazla işlem tarafından alınmamasını sağlar; nispeten kısa ömürlü kilit olmalıdır)
  • 'ilk' satırını bulun start = NULL('ilk' sizin tarafınızdan belirlenir, örneğin, siparişiniz priority?)
  • satır kümesini güncelle start = getdate(), thread = <process_number>
  • güncelleme yap (ve özel kilidi aç)
  • idve target/commanddeğerleri not edin
  • istenilen işlemi target(alternatif olarak, koşma command) ve tamamlandığında gerçekleştirin ...
  • güncelleme tasksileend = getdate() where id = <id>
  • gerçekleştirilecek başka görev kalmayıncaya kadar yukarıda tekrarlayın

Yukarıdaki tasarım ile artık dinamik (çoğunlukla) dengeli bir operasyona sahibim.

NOTLAR:

  • Bir tür önceliklendirme yöntemi sağlamaya çalışıyorum, böylece daha uzun süre çalışan görevleri başlatabilirim; birkaç işlem daha uzun çalışan görevler üzerinde çalışırken, diğer işlemler daha kısa çalışan görevler listesinde dolaşabilir
  • bir süreç planlanmamış bir gecikmeye maruz kalırsa (örn. uzun süreli, kullanıcı txn'in engellenmesi), diğer işlemler 'bir sonraki uygun' işlemi çekerek 'gevşekliği' kaldırabilir tasks
  • taskstablonun tasarımı diğer faydaları sağlamalıdır, örneğin gelecekteki referans için arşivleyebileceğiniz çalışma sürelerinin geçmişi, öncelikleri değiştirmek, mevcut işlemlerin durumunu sağlamak için kullanılabilecek çalışma sürelerinin geçmişi vb.
  • 'özel kilit' tasksbiraz aşırı görünse de, aynı anda yeni bir görev almaya çalışan 2 (veya daha fazla) sürecin potansiyel sayısını planlamamız gerektiğini unutmayın , bu yüzden bir görevi garanti etmeliyiz yalnızca bir işleme atanır (ve evet, RDBMS'nizin SQL dil özelliklerine bağlı olarak aynı sonuçları bir 'update / select' ifadesiyle elde edebilirsiniz); yeni bir 'görev' edinme adımı hızlı olmalı, yani 'özel kilit' kısa ömürlü olmalı ve gerçekte süreçler tasksoldukça rastgele bir şekilde vuracaktır , bu yüzden zaten biraz engelleme olacak

Şahsen, bu taskstablo güdümlü sürecin uygulanması ve bakımı biraz daha kolay buluyorum ... (genellikle) daha karmaşık bir görev / süreç eşlemesi atamaya çalışmak yerine ... ymmv.


Bunu Açıkçası sizin makyaj için, bir sonraki sipariş için / depo dağılımına geri gidiyor kamyon olamaz örneği inanmak gerek (UPS / Fedex / vs de var akılda tutarak çeşitli kamyonlara siparişlerinizi önceden atamak teslimat sürelerini ve gaz kullanımını azaltmak için dağıtım yollarına göre tayin edin).

Ancak, gerçek dünya örneğinizde ('istatistik güncellemesi') görev / süreç atamalarının dinamik olarak yapılamamasının bir nedeni yoktur, böylece iş yükünü dengelemek için daha iyi bir şans sağlar (cpus genelinde ve toplam çalışma süresini azaltmak açısından) .

NOT: Gerçekte söz konusu görevleri çalıştırmadan önce görevlerini (yük dengeleme biçimi olarak) önceden atamaya çalışan (BT) insanları görüyorum ve her durumda, almak için ön atama sürecini sürekli değiştirmek zorunda kalıyor sürekli değişen görev konularını göz önünde bulundurmak (örneğin, tablo / dizindeki parçalanma seviyesi, eşzamanlı kullanıcı etkinliği, vb.).


Birincisi, 'sipariş'i tablo olarak ve' sipariş detayını 'tablodaki belirli bir istatistik olarak düşünürsek, o zaman bölünmemenin sebebi rakip kovalar arasında kilit beklemekten kaçınmaktır. Traceflag 7471, bu sorunu ortadan kaldırmak için tasarlanmıştır, ancak testlerimde hala kilitleme sorunları vardı.
Paul Holmes

Başlangıçta çok hafif bir çözüm yapmayı umuyordum. Kovaları tekil çok aşamalı SQL blokları olarak oluşturun ve sonra kendi kendini yok eden SQL Agent işlerini kullanarak her birini 'ateşleyin ve unutun'. yani Kuyruk yönetimi işi yok. Ancak, daha sonra istatistik başına iş hacmini kolayca ölçemediğimi buldum - satır sayısı kesmedi. Satır sayısının, bir tablodan diğerine veya gerçekten de durağan olan IO miktarıyla doğrusal olarak eşleşmediği göz önüne alındığında, şaşırtıcı değil. Yani evet, bu uygulama için, aslında önerdiğiniz gibi bazı aktif kuyruk yönetimi eklenmesi ile öz dengesi olabilir.
Paul Holmes

İlk yorumunuzda ... evet, komutların ayrıntı düzeyi hakkında hala (açık) bir karar var ... ve eşzamanlılık sorunları gibi: bazı komutlar paralel olarak çalıştırılabilir ve kombine disk okumalarından yararlanabilir, vb. (biraz hafif) dinamik kuyruk yönetimi, kovaları önceden atamaktan biraz daha verimli :-) Çalışmak için iyi bir cevap / fikir setiniz var ... sağlayan bir çözüm bulmak çok zor olmamalı bazı iyi yük dengeleme.
markp-fuso

1

sayı tablosunu istediğiniz gibi oluşturun ve doldurun.

 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)

Kamyon masası oluşturuldu

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 )

Bir OrderSummarytablo oluşturdum

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

Lütfen Delta değerimi kontrol et ve yanlış olup olmadığını bana bildir

;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'in sonucunu kontrol edebilirsiniz, hepsi mümkün Permutation and Combination of order along with their size.

Eğer yaklaşımım buraya kadar doğruysa, o zaman birinin yardımına ihtiyacım var.

Bekleyen Görev:

her grup arasında benzersiz olan ve her bir parça T Delta'ya yakın olacak CTE1şekilde 3 parçaya ( Truck count) kadar olan sonucu filtreleyin ve bölün .OrderidruckOrderSize


Gönderme, kimse benim mistake.Copy yapıştırın ve çalıştırın işaret ederken benim son answer.I özledim bir sorgu edin
KumarHarsh
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.