Query challenge: creazione di bucket di dimensioni pari, basate su una misura e non sul conteggio delle righe


12

Descriverò il problema in termini di caricamento di un numero fisso di camion con ordini, nel modo più uniforme possibile.

ingressi:

@TruckCount - the number of empty trucks to fill

Un set:

OrderId, 
OrderDetailId, 
OrderDetailSize, 
TruckId (initially null)

Orderssono composti da uno o più OrderDetails.

La sfida qui è assegnare un TruckIda ciascun record.

Un singolo ordine non può essere suddiviso tra camion.

I camion dovrebbero essere caricati * nel modo più uniforme possibile, misurato da sum(OrderDetailSize).

* In modo uniforme: il delta più piccolo raggiungibile tra il camion meno carico e il camion più carico. Con questa definizione, 1,2,3 è distribuito in modo più uniforme di 1,1,4. Se aiuta, fai finta di essere un algoritmo di statistiche, creando anche istogrammi di altezza.

Non viene preso in considerazione il carico massimo del carrello. Questi sono magici camion elastici. Il numero di camion è comunque fisso.

C'è ovviamente una soluzione che è iterativa: il round robin assegna gli ordini.

Ma può essere fatto come una logica basata su set?

Il mio interesse principale è per SQL Server 2014 o versioni successive. Ma potrebbero essere interessanti anche soluzioni basate su set per altre piattaforme.

Sembra il territorio Itzik Ben-Gan :)

La mia applicazione nel mondo reale sta distribuendo un carico di lavoro di elaborazione in un numero di bucket per corrispondere al numero di CPU logiche. Quindi ogni secchio non ha dimensioni massime. Aggiornamenti delle statistiche, in particolare. Ho solo pensato che fosse più divertente sottrarre il problema ai camion come mezzo per inquadrare la sfida.

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
Questo sembra essere il classico problema di imballaggio del cestino .
Dan Guzman,

1
Anche Hugo Kornelis ha lavorato bene.
Erik Darling,

Tutti i valori OrderDetailSize saranno uguali per un determinato OrderId o è solo una coincidenza nei dati campione?
youcantryreachingme,

@youcantryreachingme Ah, buon posto ... no, questa è solo una coincidenza nei dati del campione.
Paul Holmes,

Risposte:


5

Il mio primo pensiero è stato

select
    <best solution>
from
    <all possible combinations>

La parte "migliore soluzione" è definita nella domanda: la più piccola differenza tra i camion più caricati e meno caricati. L'altro bit, tutte le combinazioni, mi ha fatto riflettere.

Considera una situazione in cui abbiamo tre ordini A, B e C e tre camion. Le possibilità sono

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.

Molti di questi sono simmetrici. Le prime sei file, ad esempio, differiscono solo in quale camion viene inserito ciascun ordine. Poiché i camion sono fungibili, questi arrangemets produrranno lo stesso risultato. Per ora lo ignorerò.

Esistono query note per la produzione di permutazioni e combinazioni. Tuttavia, questi produrranno accordi all'interno di un singolo secchio. Per questo problema ho bisogno di accordi su più secchi.

Osservando l'output della query standard "tutte le combinazioni"

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

Ho notato che i risultati formavano lo stesso schema della Tabella A. Facendo il salto congnativo di considerare ogni colonna come un Ordine 1 , i valori per dire quale camion conterrà quell'Ordine e una fila per essere una disposizione di Ordini all'interno dei camion. La query diventa quindi

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.

Espandendo questo per coprire i quattordici Ordini nei dati di esempio e semplificando i nomi otteniamo questo:

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

Ho scelto di conservare i risultati intermedi in tabelle temporanee per comodità.

I passaggi successivi saranno molto più semplici se i dati vengono prima PIPATI.

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.

I pesi possono essere introdotti unendosi alla tabella degli ordini.

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

Ora è possibile rispondere alla domanda trovando gli accordi che hanno la differenza più piccola tra i camion più caricati e quelli meno caricati

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

Discussione

Ci sono molti problemi con questo. Innanzitutto è un algoritmo a forza bruta. Il numero di file nelle tabelle di lavoro è esponenziale nel numero di camion e ordini. Il numero di righe in #Arrangements è (numero di camion) ^ (numero di ordini). Questo non si ridimensionerà bene.

Il secondo è che nelle query SQL è incorporato il numero di ordini. L'unico modo per aggirare questo è utilizzare SQL dinamico, che ha problemi propri. Se il numero di ordini è in migliaia, potrebbe arrivare un momento in cui l'SQL generato diventa troppo lungo.

Terzo è la ridondanza negli accordi. Questo gonfia le tabelle intermedie aumentando enormemente il tempo di esecuzione.

Quarto, molte file in #Arrangements lasciano vuoti uno o più camion. Questa non può essere la configurazione ottimale. Sarebbe facile filtrare queste righe al momento della creazione. Ho scelto di non farlo per mantenere il codice più semplice e mirato.

Sul lato positivo, questo gestisce pesi negativi, nel caso in cui la tua azienda dovesse mai iniziare a spedire baloons pieni di elio!

Pensieri

Se ci fosse un modo per popolare #FilledTrucks direttamente dall'elenco dei camion e degli ordini, penso che il peggio di queste preoccupazioni sarebbe gestibile. Purtroppo la mia immaginazione è inciampata su quell'ostacolo. La mia speranza è che un futuro collaboratore possa essere in grado di fornire ciò che mi ha eluso.




1 Dici che tutti gli articoli per un ordine devono essere sullo stesso camion. Ciò significa che l'atomo di assegnazione è l'Ordine, non il OrderDetail. Ho generato questi dai tuoi dati di test così:

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

Tuttavia, non fa alcuna differenza se etichettiamo gli articoli in questione "Ordine" o "Ordine dettagli", la soluzione rimane la stessa.


4

Guardando il tuo requisito del mondo reale (che presumo sia il tentativo di bilanciare il tuo carico di lavoro attraverso un set di cpus) ...

C'è un motivo per cui è necessario pre-assegnare processi a bucket / cpus specifici? [Cercare di capire le tue reali esigenze]

Per il tuo esempio di "aggiornamento delle statistiche", come fai a sapere quanto tempo impiegherà una determinata operazione? Che cosa succede se una determinata operazione si imbatte in un ritardo imprevisto (ad esempio, frammentazione più che pianificata / eccessiva di tabella / indice, utente txn di lunga durata blocca un'operazione di "aggiornamento delle statistiche")?


Ai fini del bilanciamento del carico in genere generi l'elenco di attività (ad esempio, un elenco di tabelle per le statistiche aggiornate) e inserisco tale elenco in una tabella (temporanea / scratch).

La struttura della tabella può essere modificata in base alle proprie esigenze, ad esempio:

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
)

Successivamente, inizio il numero X di processi simultanei per eseguire le effettive operazioni di "aggiornamento delle statistiche", con ciascun processo che esegue quanto segue:

  • posiziona il blocco esclusivo sul taskstavolo (assicura che nessuna attività venga raccolta da più di un processo; dovrebbe essere un blocco di breve durata)
  • trova la 'prima' riga dove start = NULL('primo' sarebbe determinato da te, ad esempio, ordina per priority?)
  • aggiorna il set di righe start = getdate(), thread = <process_number>
  • commit aggiornamento (e rilascio blocco esclusivo)
  • prendere nota ide target/commandvalori
  • eseguire l'operazione desiderata contro target(alternativamente, eseguire command) e al termine ...
  • aggiorna tasksconend = getdate() where id = <id>
  • ripetere sopra fino a quando non saranno più necessarie altre attività

Con il disegno sopra ho ora un'operazione bilanciata dinamicamente (principalmente).

APPUNTI:

  • Cerco di fornire una sorta di metodo di definizione delle priorità in modo da poter avviare in anticipo le attività più lunghe; mentre un paio di processi stanno lavorando sulle attività più lunghe, gli altri processi possono sfogliare l'elenco delle attività più brevi
  • se un processo si imbatte in un ritardo non pianificato (ad esempio, esecuzione prolungata, blocco dell'utente txn), altri processi possono "colmare il gioco" continuando a estrarre l'operazione "disponibile successiva" da tasks
  • la progettazione della taskstabella dovrebbe fornire altri vantaggi, ad esempio una cronologia dei tempi di esecuzione che è possibile archiviare per riferimento futuro, una cronologia dei tempi di esecuzione che possono essere utilizzati per modificare le priorità, fornire uno stato delle operazioni correnti, ecc.
  • mentre il "blocco esclusivo" taskspuò sembrare un po 'eccessivo, tieni presente che dobbiamo pianificare il potenziale problema di 2 (o più) processi che tentano di ottenere un nuovo compito nello stesso momento esatto , quindi dobbiamo garantire un compito è assegnato a un solo processo (e sì, è possibile ottenere gli stessi risultati con un'istruzione combinata 'update / select' - a seconda delle capacità del linguaggio SQL di RDBMS); il passaggio per ottenere un nuovo "compito" dovrebbe essere rapido, vale a dire che il "blocco esclusivo" dovrebbe essere di breve durata e, in realtà, i processi colpiranno tasksin modo abbastanza casuale quindi saranno comunque poco bloccati

Personalmente, trovo questo tasksprocesso guidato da una tabella un po 'più facile da implementare e mantenere ... al contrario di un processo (di solito) più complesso di provare a pre-assegnare mappature di attività / processi ... ymmv.


Ovviamente per il tuo make credere esempio non è possibile avere il vostro camion di tornare alla distribuzione / magazzino per il prossimo ordine, quindi è necessario pre-assegnare gli ordini ai vari camion (tenendo presente che UPS / Fedex / etc devono anche assegnare in base ai percorsi di consegna al fine di ridurre i tempi di consegna e l'utilizzo del gas).

Tuttavia, nel tuo esempio del mondo reale ("aggiornamento delle statistiche") non vi è alcun motivo per cui le assegnazioni di attività / processo non possano essere eseguite in modo dinamico, garantendo così una migliore possibilità di bilanciare il carico di lavoro (su cpus e in termini di riduzione del tempo di esecuzione complessivo) .

NOTA: vedo regolarmente persone (IT) che provano a pre-assegnare i loro compiti (come una forma di bilanciamento del carico) prima di eseguire effettivamente tali compiti, e in ogni caso finisce per dover costantemente modificare il processo di pre-assegnazione per prendere in considerazione questioni di attività in costante evoluzione (ad es. livello di frammentazione nella tabella / indice, attività utente simultanea, ecc.).


In primo luogo, se pensiamo all '"ordine" come tabella e al "dettaglio ordine" come una statistica specifica sulla tabella, la ragione per non dividere è quella di evitare le attese di blocco tra i secchi concorrenti. Traceflag 7471 è progettato per eliminare questo problema, ma nei miei test avevo ancora problemi di blocco.
Paul Holmes,

Inizialmente avevo sperato di fare una soluzione molto leggera. Creare i bucket come blocchi SQL a più stadi singolari, quindi "attivarli e dimenticarli" ciascuno utilizzando processi SQL Agent autodistruggenti. cioè nessun lavoro di gestione delle code. Tuttavia, successivamente ho scoperto che non potevo facilmente misurare il volume di lavoro per statistica - il numero di righe non lo ha tagliato. Non sorprende davvero, dato che il conteggio delle righe non si associa linearmente alla quantità di IO da una tabella, o addirittura stastica, alla successiva. Quindi sì, per questa applicazione, potrebbe davvero bilanciarsi autonomamente con l'aggiunta di una gestione attiva della coda come suggerisci.
Paul Holmes,

Al tuo primo commento ... sì, c'è ancora la (ovvia) decisione sulla granularità dei comandi ... e problemi di concorrenza come: alcuni comandi possono essere eseguiti in parallelo e beneficiare delle loro letture combinate del disco, ecc. Ma trovo ancora un (piuttosto leggera) gestione dinamica delle code un po 'più efficiente rispetto ai bucket di pre-assegnazione :-) Hai un buon set di risposte / idee con cui lavorare ... non dovrebbe essere troppo difficile trovare una soluzione che fornisca un discreto bilanciamento del carico.
markp

1

crea e popola la tabella numerica come desideri. Questa è una sola creazione.

 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)

Tavolo Truck creato

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 )

Ho creato una OrderSummarytabella

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

Controlla il mio valore Delta e fammi sapere se è sbagliato

;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 

Puoi controllare il risultato di CTE1, ha tutto il possibile Permutation and Combination of order along with their size.

Se il mio approccio è corretto fino a qui, allora ho bisogno di qualcuno che mi aiuti.

Attività in sospeso:

filtro e Dividi il risultato di CTE1in a 3 part ( Truck count) in modo che Orderidsia unico tra ciascun gruppo e ogni parte T ruckOrderSizeè vicina a Delta.


Controlla la mia ultima risposta. Mi manca una query durante la pubblicazione, nessuno ha segnalato il mio errore
Copia
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.