Click here to monitor SSC
Av rating:
Total votes: 16
Total comments: 36


Joe Celko
Celko's SQL Stumper: Eggs in one Basket
29 March 2010

Joe Celko reveals the winner of his Easter Stumper: the puzzle of designing an apparently simple database to deal with the process of packing eggs into cartons. It wasn't quite as easy as it looked.

Don't Put All Your Eggs in One Basket - Use Cartons!

The Puzzle

You are the DBA for Farmer Brown, who runs a chicken farm. Or is it a chicken ranch? Here is Texas, that can be a legal issue. One side argues that it has animals and not plants, therefore it is ranch. The other side says it is a farm because a ranch has livestock, and livestock is defined by an animal that is herded to market. There are cattle drives, there are sheep drives but there are no chicken drives. There is a story about that disaster, but I digress.

Farmer Brown gathers eggs in baskets and the eggs are homogeneous. Each basket has an identifier which tells us where and when it was collected. Or are eggs harvested if it is a farm? But I digress again.

We do not put our eggs in one basket; we put them in dozen egg cartons. The dozen is an incredibly strong tradition. During the decimalization of the UK a dairy offered 'decimal eggs'- a ten-pack of eggs. People would not buy it; it was just too weird, even when the cost per egg was less. I have a theory about the chickens being taught to lay eggs that way. However, Brits were happier to consider adjusting to metric beer when it was explained that a liter is more beer than a quart, before dismissing the idea as a Napoleonic trick.

We need table(s) for the baskets and the cartons. A basket comes in, and it is split into cartons with 12 or fewer eggs. This means that we will have one or zero cartons with fewer than 12 eggs in it. Example: the basket with 144 eggs becomes 12 cartons. The Basket with 255 eggs becomes 21 full cartons and one with 3 eggs.

  1. Your first job is to design the table(s)! Yes, this is a puzzle where you have to write DDL. Just doing SELECT/UPDATE/INSERT/DELETE is only part of SQL; most of the real work is in DDL. A bad schema design will force bad DML. A carton, for example,  must have CHECK (COUNT(egg_cnt BETWEEN 1 AND 12) constraint.
  2. Split the baskets of eggs into cartons, each carton having its own identifier. This can be a statement or a stored procedure. Call this procedure or function SplitBaskets().
  3. Once the baskets are split into cartons, write a procedure to split a carton. You will need parameters for the source carton and the number of eggs moved. You need to create a new carton. Call this procedure or function SplitCarton(). You need to watch the size rules.
  4. Once the baskets are split into cartons, write a procedure to consolidate two cartons. You will need parameters for the source carton and the destination carton. Assume that the destination carton gets filled and the source carton decremented by the appropriate amount. I.e. (c1 = 10, c2 = 4) => (c1 = 12, c2 = 2). Call this procedure or function ConsolidateCartons(). Watch out for the empty carton problem.

The answer needed include the DDL as well as the DML, as  the code is easier if you get the schema design  right.

The competitors had to...

  1. Solve the problem -- Duh!
  2. Avoid proprietary features in SQL Server that will not port or be good across all releases, present and future.
  3. Use Standard features in SQL Server that will be good across all releases, present and future. Extra points for porting code.
  4. Be clever but not obscure.
  5. Explain what they were doing

A discussion about the Stumper

I really liked my Egg Puzzle because it was a chance to show off a lot of newer programming tricks. I usually write ANSI Standard SQL and then translate it into local dialect, so bear with me.

Begin by modeling Baskets of eggs. We can assume that there is a basket identifier of some kind (basket_id) and that it has a certain number of eggs in it. The collection date and other information is not needed for the puzzle, but put him in there to remind us about that data.

CREATE TABLE Baskets

(basket_id INTEGER NOT NULL PRIMARY KEY,

collection_date DATE DEFAULT CURRENT_DATE NOT NULL,

egg_cnt SMALLINT NOT NULL CHECK (egg_cnt >= 0),

dozen_cnt GENERATED ALWAYS AS (FLOOR(egg_cnt/12)),

partial_cnt GENERATED ALWAYS AS (MOD(egg_cnt, 12))

);

or in SQL Server

CREATE TABLE Baskets

  (basket_id INTEGER NOT NULL PRIMARY KEY,

   collection_date DATE DEFAULT GETDATE() NOT NULL,

   egg_cnt SMALLINT NOT NULL CHECK (egg_cnt>=0),

   dozen_cnt AS (FLOOR(egg_cnt/12)) PERSISTED,

   partial_cnt AS (egg_cnt%12) PERSISTED

  ) ;

The number of possible full cartons is a computed value, as its the size of the partial, possibly empty, carton. But where do you compute these values? I think that Dkorzennik was correct to do this job in the table where the parameter lives. They can done with the new computed column feature; Standard SQL uses “GENERATED ALWAYS AS” instead of the “PERSISTED” syntax of SQL Server.

Integer math is preferred over using FLOOR() and CEILING() functions on decimal or floating point expressions.

Next, we need to model the cartons. I am making the assumption that a carton is identified by (basket_id, carton_id) because that is the pattern required by law for chickens, swine, shrimp, etc:  RFID tags. Basically, animals are sold in 'lots' and not individually identified.

CREATE TABLE Cartons

(basket_id INTEGER NOT NULL

REFERENCES Baskets (basket_id)

ON UPDATE CASCADE,

carton_id INTEGER NOT NULL,

PRIMARY KEY (basket_id, carton_id),

egg_cnt SMALLINT CHECK (egg_cnt BETWEEN 0 AND 12));

Notice the DRI action on the key and a CHECK() on the egg count in each carton. The CHECK() might look redundant at first because the computed columns assure that dozen_cnt and partial_cnt are always between 0 and 12 when we get to the procedure to pack the cartons. Wrong. Firstly, this constraint assures that nobody can subvert the data. Secondly, the optimizer can use it, but cannot use the computations.

Now, let's pack those cartons. Carton #0 will always be the partial carton from its basket that we cannot ship. That is a handy thing for a lot of queries, where we need to look at partial and full cartons. The parameter for the procedure is just the basket identifier; we have the other data we need already computed in the Baskets table. SQL Server people will need to turn the IN keyword into a @ prefix.

CREATE PROCEDURE Split_Cartons

(IN in_basket_id INTEGER)

INSERT INTO Cartons (basket_id, carton_id, egg_cnt)

SELECT in_basket_id, 0, partial_cnt

FROM Baskets

WHERE basket_id = in_basket_id

UNION ALL

SELECT in_basket_id, S.seq, 12

FROM Series AS S, Baskets AS B

WHERE B/basket_id = in_basket_id

AND S.seq <= dozen_cnt;

Or in SQL Server...

CREATE PROCEDURE Split_Cartons

  (@in_basket_id INTEGER)

AS

INSERT INTO Cartons (basket_id, carton_id, egg_cnt)

  SELECT @in_basket_id, 0, partial_cnt

  FROM Baskets

  WHERE basket_id=@in_basket_id

  UNION ALL

  SELECT @in_basket_id, S.seq, 12

  FROM Series AS S,Baskets AS B

  WHERE B.basket_id=@in_basket_id

    AND S.seq<=dozen_cnt ;

I assume we have the usual auxiliary table with a series of integers from 1 to (n). The first term of the UNION ALL created the partial, possibly empty, carton. I don't want to have empty cartons, I can add a DELETE FROM statement after this or add a “.. AND partial_cnt > 0” predicate to the first SELECT.

The second SELECT returns the full cartons numbered 1 to dozen_cnt within the basket.

Here is a characteristic for good SQL programming: do as much work as you can in ONE statement, so the optimizer can have as much information as possible.

Now, the next procedure is supposed to move eggs from a source carton to the destination carton. Because SQL is a data language, I like data-driven solutions over computational ones. This is a state-change problem. There are “conservation of eggs” laws that have to be met:

  1. I cannot pack or unpack more than 12 eggs total to either carton.
  2. I cannot move more eggs than there are in the source carton.
  3. The total number of eggs is constant after they change locations.

So, we build a classic state-change table, with precondition and post-condition states.

CREATE TABLE Consolidations

(pre_dest_egg_cnt INTEGER NOT NULL CHECK (pre_dest_egg_cnt BETWEEN 0 AND 12),

pre_src_egg_cnt INTEGER NOT NULL CHECK (pre_src_egg_cnt BETWEEN 0 AND 12),

 

post_dest_egg_cnt INTEGER NOT NULL CHECK (post_dest_egg_cnt BETWEEN 0 AND 12),

post_src_egg_cnt INTEGER NOT NULL CHECK (post_src_egg_cnt BETWEEN 0 AND 12),

 

CONSTRAINT conservation_of_eggs

CHECK (pre_dest_egg_cnt + pre_src_egg_cnt

= post_dest_egg_cnt + post_src_egg_cnt),

PRIMARY KEY (pre_dest_egg_cnt, pre_src_egg_cnt,

post_dest_egg_cnt, post_src_egg_cnt)

);

Or, in SQL Server...

CREATE TABLE Consolidations

(pre_dest_egg_cnt INTEGER NOT NULL CHECK (pre_dest_egg_cnt BETWEEN 0 AND 12),

pre_src_egg_cnt INTEGER NOT NULL CHECK (pre_src_egg_cnt BETWEEN 0 AND 12),

post_dest_egg_cnt INTEGER NOT NULL CHECK (post_dest_egg_cnt BETWEEN 0 AND 12),

post_src_egg_cnt INTEGER NOT NULL CHECK (post_src_egg_cnt BETWEEN 0 AND 12),

CONSTRAINT conservation_of_eggs

CHECK (pre_dest_egg_cnt + pre_src_egg_cnt

= post_dest_egg_cnt + post_src_egg_cnt),

constraint unique_consolidation PRIMARY KEY (pre_dest_egg_cnt, pre_src_egg_cnt,

post_dest_egg_cnt, post_src_egg_cnt)

);

The first thought to use the Series table and load the table like this:

INSERT INTO Consolidations

(pre_dest_egg_cnt, pre_src_egg_cnt,

post_dest_egg_cnt, post_src_egg_cnt)

SELECT S1.seq, S2.seq, S3.seq, S4.seq

FROM Series AS S1, Series AS S2, Series AS S3, Series AS S4

WHERE S1.seq <= 12

AND S2.seq <= 12

AND S3.seq <= 12

AND S4.seq <= 12

AND (S1.seq + S2.seq) = (S3.seq + S4.seq);

Nice, but it does not work! We need to allow for cartons that have or will have zero eggs during consolidation. We could use a CTE that UNIONs a zero the to Series table, but it is just as easy to do a little math and subtract one to adjust the range.

INSERT INTO Consolidations

(pre_dest_egg_cnt, pre_src_egg_cnt,

post_dest_egg_cnt, post_src_egg_cnt)

SELECT S1.seq-1, S2.seq-1, S3.seq-1, S4.seq-1

FROM Series AS S1, Series AS S2, Series AS S3, Series AS S4

WHERE S1.seq <= 13

AND S2.seq <= 13

AND S3.seq <= 13

AND S4.seq <= 13

AND (S1.seq + S2.seq) = (S3.seq + S4.seq);

This will give us 169 rows to use. It might throw a new SQL programmer when he sees the two-part keys, but Standard SQL has row constructors; SQL Server programmers will have expand them. What we are going to do is find the egg count in the source table, the egg count in the destination table and use them to do a fancy look-up in the Consolidations. The MERGE statement is made for this.

CREATE PROCEDURE Consolidate_Cartons

(IN in_src_basket_id INTEGER, IN in_src_carton_id INTEGER,

IN in_dest_basket_id INTEGER, IN in_dest_carton_id INTEGER)

 

MERGE INTO Cartons

USING Consolidations

ON (SELECT egg_cnt

FROM Cartons -- AS Source

WHERE (in_src_basket_id, in_src_carton_id)

= (basket_id, carton_id))

= pre_src_egg_cnt

AND (SELECT egg_cnt

FROM Cartons –- AS Destination

WHERE (in_dest_basket_id, in_dest_carton_id)

= (basket_id, carton_id))

= pre_dest_egg_cnt

 

WHEN MATCHED

THEN UPDATE

SET egg_cnt

= CASE WHEN (basket_id, carton_id) = (in_src_basket_id, in_src_carton_id)

THEN post_src_egg_cnt

WHEN (basket_id, carton_id) = (in_dest_basket_id, in_dest_carton_id)

THEN post_dest_egg_cnt

ELSE egg_cnt END;

In SQL Server, this would probably look like this...

CREATE PROCEDURE Consolidate_Cartons

  (@in_src_basket_id INTEGER,

   @in_src_carton_id INTEGER,

   @in_dest_basket_id INTEGER,

   @in_dest_carton_id INTEGER

  )

as

MERGE INTO Cartons

USING Consolidations

ON (SELECT egg_cnt

    FROM  Cartons -- AS Source

    WHERE

      @in_src_basket_id=basket_ID

      and @in_src_carton_id=carton_id

   )=pre_src_egg_cnt

  AND (SELECT egg_cnt

       FROM Cartons

       WHERE @in_dest_basket_id=basket_id

        and @in_dest_carton_id=carton_id

      )=pre_dest_egg_cnt     

 

WHEN MATCHED

THEN UPDATE

SET egg_cnt

=CASE WHEN (basket_id=@in_src_basket_id

         and carton_id=@in_src_carton_id) THEN post_src_egg_cnt

   WHEN (basket_id=@in_dest_basket_id

         and carton_id=@in_dest_carton_id) THEN post_dest_egg_cnt

   ELSE egg_cnt END ;

The CASE expression puts the right post-condition numbers in the appropriate rows. If we cannot find our situation the Consolidations table, we do nothing because there are no matches.

You could make Consolidations into a derived table to get this into one statement. I am not sure that it is worth it; as the table is very small but since you can index a base table, there might be a slight performance boost.

Another point; this code was written in straight ANSI SQL without any proprietary features. Do you think that you would have any problems porting it to SQL Server? DB2? Oracle? Any SQL that has the SQL-99 standards or better?

And the winner is...

I was impressed by the ingenuity and energy of the contestants. As always, it is difficult to choose a winner because almost every entry had something of merit to offer. I  liked Peso's ingenoius entry, which would probably take the speed award, if there was one. I am a fan of his -- his stuff is usually clean and solid code.  When you earn a living fixing disasters, you really like to see that. However,  Phil and I both voted to give this one to Dkorzennik in the end. He actually does this in the Real World. His code was pretty clean. But he have IDENTITY when it was not needed, a WHILE which is evil and some minor dialect that a Petty Printer would clean up (INSERT instead of INSERT INTO, DELETE instead of DELETE FROM, etc).

What I liked was seeing computed columns for computed VALUES. Nobody else got that and this was a DDL problem more than anything else.

Putting it into ISO-11179 data element names and adding constraints, he had:

CREATE TABLE Baskets

(basket_id INTEGER NOT NULL PRIMARY KEY,

collection_date DATE DEFAULT CURRENT_DATE NOT NULL,

egg_cnt SMALLINT NOT NULL

CHECK (egg_cnt >= 0)

dozen_cnt AS FLOOR(egg_cnt/12) PERSISTED,

partial_cnt AS MOD (egg_cnt, 12) PERSISTED);

 

CREATE TABLE Cartons

(basket_id INTEGER NOT NULL

REFERENCES Baskets (basket_id),

carton_id INTEGER NOT NULL,

PRIMARY KEY (basket_id, carton_id),

egg_cnt SMALLINT CHECK (egg_cnt <= 12));

The trigger and its loop can be replaced by a one statement procedure. The partial carton is always #0.

CREATE PROCEDURE Split_Cartons

(@i_basket_id INTEGER, @n_dozen_cnt SMALLINT, @n_partial_cnt SMALLINT)

INSERT INTO Cartons (basket_id, carton_id, egg_cnt)

SELECT (basket_id, carton_id, egg_cnt)

FROM (VALUES(@in_basket_id, 0, @in_partial_cnt))

UNION ALL

SELECT @in_basket_id, seq, 12

FROM Series -- a table of numbers from 1 to (n)

WHERE seq <= @in_dozen_cnt;



This article has been viewed 6879 times.
Joe Celko

Author profile: Joe Celko

Joe Celko is one of the most widely read of all writers about SQL, and was the winner of the DBMS Magazine Reader's Choice Award four consecutive years. He is an independent consultant living in Austin, TX. He has taught SQL in the US, UK, the Nordic countries, South America and Africa.
He served 10 years on ANSI/ISO SQL Standards Committee and contributed to the SQL-89 and SQL-92 Standards.
He has written over 800 columns in the computer trade and academic press, mostly dealing with data and databases. He is the author of eight books on SQL for Morgan-Kaufmann, including the best selling SQL FOR SMARTIES.
Joe is a well-known figure on Newsgroups and Forums, and he is famous for his his dry wit. He is also interested in Science Fiction.

Search for other articles by Joe Celko

Rate this article:   Avg rating: from a total of 16 votes.


Poor

OK

Good

Great

Must read
 
Have Your Say
Do you have an opinion on this article? Then add your comment below:
You must be logged in to post to this forum

Click here to log in.


Subject: Easter Eggs
Posted by: Peso (view profile)
Posted on: Friday, April 02, 2010 at 3:40 AM
Message:

Here is my stab it the problem. My approach was to make it concurrent safe, ie many concurrent executions.
So there is one table, Cartons. And there is three procedures; SplitBaskets, SplitCarton and ConsolidateCartons. Each and one of the procedures uses a single statement for transactional consistency. However, I use some proprietary code which will not port at the moment.


---------------------------------------------
CREATE TABLE  Cartons
  
(
    
carton_id INTEGER IDENTITY(1,1) NOT NULL PRIMARY KEY,
    
carton_egg_cnt INTEGER NOT NULL,
    
CONSTRAINT ck_cartons_egg_cnt CHECK
    
(
      
carton_egg_cnt BETWEEN 1 AND 12
    
)
   )

CREATE PROCEDURE SplitBaskets
(
@basket_egg_cnt INTEGER
)
AS

DECLARE  
@carton_cnt INTEGER

SET  @carton_cnt = CEILING(@basket_egg_cnt / 12.0)

;
WITH  n0(p) AS (SELECT 1 UNION ALL SELECT 1),
n1(p) AS (SELECT 1 FROM n0 AS a CROSS JOIN n0 AS b),
n2(p) AS (SELECT 1 FROM n1 AS a CROSS JOIN n1 AS b),
n3(p) AS (SELECT 1 FROM n2 AS a CROSS JOIN n2 AS b),
n4(p) AS (SELECT 1 FROM n3 AS a CROSS JOIN n3 AS b),
n5(p) AS (SELECT 1 FROM n4 AS a CROSS JOIN n4 AS b)

INSERT INTO Cartons
  
(
    
carton_egg_cnt
  
)
SELECT   CASE
    
WHEN 12 * d.Number &amp;gt; @basket_egg_cnt THEN 12 + @basket_egg_cnt - 12 * d.Number
    
ELSE 12
  
END AS carton_egg_cnt
FROM   (
    
SELECT  TOP (@carton_cnt)
      
ROW_NUMBER() OVER (ORDER BY p) AS Number
    
FROM  n5
  
) AS d

CREATE PROCEDURE SplitCarton
(
@carton_id INTEGER,
@carton_egg_cnt INTEGER
)
AS

INSERT INTO
Cartons
  
(
    
carton_egg_cnt
  
)
SELECT   12 - @carton_egg_cnt
FROM   (
    
UPDATE  Cartons
    
SET carton_egg_cnt = carton_egg_cnt - @carton_egg_cnt
    
OUTPUT  INSERTED.carton_egg_cnt
    
WHERE carton_id = @carton_id
  
) AS d

CREATE PROCEDURE ConsolidateCartons
(
@destination_carton_id INTEGER,
@source_carton_id INTEGER,
@carton_egg_cnt INTEGER
)
AS

MERGE  Cartons AS tgt
USING  
(
  
SELECT @destination_carton_id, @carton_egg_cnt UNION ALL
  
SELECT @source_carton_id, -@carton_egg_cnt
) AS src(carton_id, carton_egg_cnt) ON src.carton_id = tgt.carton_id
WHEN MATCHED AND tgt.carton_egg_cnt + src.carton_egg_cnt = 0
  
THEN  DELETE
WHEN
MATCHED
  
THEN  UPDATE
     SET
tgt.carton_egg_cnt = tgt.carton_egg_cnt + src.carton_egg_cnt;


Subject: Solution
Posted by: kirtangor (view profile)
Posted on: Monday, April 05, 2010 at 9:43 AM
Message:

I was unable to top the solution for SplitCarton & ConsolidateCartons given by Peso; but, you can find the table design and the procedure for splitting baskets under -

CREATE TABLE [dbo].[Baskets](
[BasketID] [int] IDENTITY(1,1) NOT NULL,
[EggCount] [int] NOT NULL,
[CollectedFrom] [nvarchar](50) NOT NULL,
[CollectionTime] [datetime] NOT NULL,
[IsSplit] [bit] NOT NULL,
CONSTRAINT [PK_Baskets] PRIMARY KEY CLUSTERED
(
[BasketID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[Baskets]  WITH CHECK ADD  CONSTRAINT [CK_Baskets] CHECK  (([EggCount]>(0)))
GO

ALTER TABLE [dbo].[Baskets] CHECK CONSTRAINT [CK_Baskets]
GO

ALTER TABLE [dbo].[Baskets] ADD  CONSTRAINT [DF_Baskets_Split]  DEFAULT ((0)) FOR [IsSplit]
GO

CREATE TABLE [dbo].[Cartons](
[CartonID] [int] IDENTITY(1,1) NOT NULL,
[EggCount] [int] NOT NULL,
CONSTRAINT [PK_Cartons] PRIMARY KEY CLUSTERED
(
[CartonID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[Cartons]  WITH CHECK ADD  CONSTRAINT [CK_Cartons] CHECK  (([EggCount]>=(1) AND [EggCount]<=(12)))
GO

ALTER TABLE [dbo].[Cartons] CHECK CONSTRAINT [CK_Cartons]
GO



ALTER PROCEDURE [dbo].[SplitBaskets]
(
@BasketID INT
)
AS
BEGIN
DECLARE
@EggCount INT
SELECT
@EggCount = ISNULL(EggCount, 0) FROM [dbo].[Baskets] WHERE [BasketID] = @BasketID AND [IsSplit] = 0;

IF @EggCount > 0
BEGIN
   BEGIN TRANSACTION
;

  
WITH I(Total, Rem) AS
  
(
    
SELECT @EggCount, CASE WHEN @EggCount > 12 THEN 12 ELSE @EggCount END
     UNION
ALL
    
SELECT (Total - 12), CASE WHEN Total > 24 THEN 12 ELSE Total - 12 END FROM I WHERE ((Total - 12) > 0)
   )
  
INSERT INTO [dbo].[Cartons] ([EggCount])
  
SELECT [Rem] FROM I

  
UPDATE [dbo].[Baskets] SET [IsSplit] = 1 WHERE [BasketID] = @BasketID;

  
COMMIT;
END;
END;


Subject: First thoughts
Posted by: Joe Celko (not signed in)
Posted on: Monday, April 05, 2010 at 4:30 PM
Message:

Wow, two answers came back pretty fast and I got one in a private email.

  1. Assume that you have a table Series(seq), which has the integers 1 to (n). This is a common trick, and will keep us from getting into the best way to generate such a table as a CTE or derived table. As an aside, I used to call this table Sequence, but SEQUENCE is now a reserved word in ANSI/ISI Standard SQL, so I moved over to Series.
  2. Fill in the usual Celko rant about using IDENTITY(1,1) for a key. Then ignore me if it makes problems in getting a quick answer. It is just a puzzle and not production work.
  3. Cartons are filled with eggs from Baskets, but how do I trace a carton back to its basket? For a real DB, the addition of basket sources and timestamps was nice, but not needed for a puzzle.
  4. Are Baskets and Cartons egg counts the same data element or not? Do they need two different names? Trickier design question: is a basket (carton) an entity or a unit of measurement? Both? Neither?
  5. SET @carton_cnt = CEILING(@basket_egg_cnt / 12.0);
    Why force decimal math with 12.0 instead of using integer math with 12, integer division and mod functions?
  6. We have table constructors now, so things like:
    (SELECT @destination_carton_id, @carton_egg_cnt
    UNION ALL
    SELECT @source_carton_id, -@carton_egg_cnt
    ) AS SRC(carton_id, carton_egg_cnt)


    can be written as:

    SELECT *
    FROM (VALUES (@destination_carton_id, @carton_egg_cnt),
    (@source_carton_id, -@carton_egg_cnt)
    ) AS SRC(carton_id, carton_egg_cnt)
  7. I like the way that people are getting the hang of the new MERGE INTO statement.
  8. Why pass an egg count from the source carton to the destination carton? The destination is always brought to 12 or as close as possible. But you cannot move more eggs than the source has, and you have to decrement the source by only what is used.
  9. we don't have a policy about empty cartons. These can occur after a consolidation or we might allow when splitting baskets. How do you want to do it?

    MERGE Cartons AS TGT
    USING (SELECT @destination_carton_id, @carton_egg_cnt
    UNION ALL
    SELECT @source_carton_id, -@carton_egg_cnt
    ) AS SRC(carton_id, carton_egg_cnt)
    ON SRC.carton_id = TGT.carton_id
    WHEN MATCHED
    AND TGT.carton_egg_cnt + SRC.carton_egg_cnt = 0
    THEN DELETE
    WHEN MATCHED
    THEN UPDATE
    SET TGT.carton_egg_cnt = TGT.carton_egg_cnt + SRC.carton_egg_cnt;

Subject: Quick secondary puzzle
Posted by: Joe Celko (not signed in)
Posted on: Monday, April 05, 2010 at 5:19 PM
Message: Another quick puzzle:

Let's make a table of the possible consolidations since there are 144 of them. The pre-consolidation source and destination counts are in the first two columns and the post-consolidation source and destination counts are in the last two columns.

CREATE TABLE Consolidations
(pre_dest_egg_cnt INTEGER NOT NULL CHECK (pre_dest_egg_cnt BETWEEN 0 AND 12),
pre_src_egg_cnt INTEGER NOT NULL CHECK (post_src_egg_cnt BETWEEN 0 AND 12),
post_dest_egg_cnt INTEGER NOT NULL CHECK (pre_dest_egg_cnt BETWEEN 0 AND 12),
post_src_egg_cnt INTEGER NOT NULL CHECK (post_src_egg_cnt BETWEEN 0 AND 12),
CONSTRAINT conservation_of_eggs
CHECK (pre_dest_egg_cnt + pre_src_egg_cnt
= post_dest_egg_cnt + post_src_egg_cnt));

INSERT INTO Consolidations
VALUES
(0, 0, 0, 0), –- nothing to move
(0, 1, 1, 0),
..
(0, 12, 12, 0),
..
(8, 5, 12, 1), -- something in between
..
(12, 12, 12, 12); –- both are full

 

  1. Can you write a query to generate this table?
  2. Is a table look-up faster than doing the math?
  3. How would an UPDATE (or MERGE) use such a table?

Subject: I am an egg farmer and DBA!
Posted by: Dkorzennik (view profile)
Posted on: Tuesday, April 06, 2010 at 4:33 AM
Message: Hehehe. How funny is this. I've been doin free range eggs for about 5 years now, supply the leading organic market in Johannesburg, South Africa, and DBA work for about 10, at present for the largest brewer of beer in the world, SAB Miller. So yes, I keep all my farm production, orders, sales, purchases and profits in a SQL database. I also run projections and trending from my db. I'll have a read through the article in detail and post my answers. Regards. Farmer Dave. :-).

Subject: 144?
Posted by: Peso (view profile)
Posted on: Tuesday, April 06, 2010 at 6:40 AM
Message: 169! Include the zero's.


Subject: Answer to Celko "5)" above
Posted by: Peso (view profile)
Posted on: Tuesday, April 06, 2010 at 6:45 AM
Message: CEILING is easier to read, in my opinion. Using integer math and modula makes the formula look like this

SET @carton_cnt = @basket_egg_cnt / 12 + SIGN(@basket_egg_cnt % 12)

or this

IF @basket_egg_cnt % 12 = 0
SET @carton_cnt = @basket_egg_cnt / 12
ELSE
SET @carton_cnt = 1 + @basket_egg_cnt / 12

I went for this

SET @carton_cnt = CEILING(@basket_egg_cnt / 12.0)

It's only a matter of taste.

Subject: British beer in quarts???
Posted by: ryebank (not signed in)
Posted on: Tuesday, April 06, 2010 at 7:21 AM
Message: "However, Brits were happier to consider adjusting to metric beer when it was explained that a liter is more beer than a quart"

Not sure where you got this from - we drink in pints and a the metricated "pint" (which thankfully has only caught on for bottles rather than in pubs) is 500ml - an unfair whack less that 568ml of a British pint!

Subject: Quarts
Posted by: Anonymous (not signed in)
Posted on: Tuesday, April 06, 2010 at 7:34 AM
Message: "However, Brits were happier to consider adjusting to metric beer when it was explained that a liter is more beer than a quart, before dismissing the idea as a napoleonic trick."

Even though a British fluid ounce is smaller than an American one (approx 96% of the volume) there are 40 of them in a British quart. Consequently a British quart is about 1.14 (as they are spelt in Britain) litres.

Planes have fallen out of the sky because people have not considered this.

Subject: Why wait?
Posted by: Mark Fitzgerald (not signed in)
Posted on: Tuesday, April 06, 2010 at 11:31 AM
Message:

Why wait to consolidate the cartons? Why not write the procedures to split the extra eggs after the allocation of complete cartons? If done correctly only 1 part carton will exist ever.


CREATE DATABASE Celko_Eggs;
GO

USE Celko_Eggs;
GO


CREATE TABLE Baskets(
BasketID INT IDENTITY(1,1) PRIMARY KEY,
Egg_Cnt INT
)

CREATE TABLE Cartons(
CartonID INT IDENTITY(1,1) PRIMARY KEY NONCLUSTERED,
Egg_Cnt INT CHECK(egg_cnt >= 1 AND egg_cnt <=12),
SourceInfo VARCHAR(1000)
)

CREATE TABLE CartonCounter(
CartonCountID INT
)
GO

DECLARE @counter INT
SET
@counter = 1
WHILE @counter < 101
BEGIN
   INSERT INTO
CartonCounter VALUES (@counter)
  
SET @counter = @counter + 1
END

SELECT
* FROM CartonCounter
GO

----------------------Add complete cartons
ALTER PROC SplitBaskets(@BasketID INT)
AS
BEGIN
   INSERT INTO
Cartons (Egg_Cnt,SourceInfo)
    
SELECT 12, '[B'+CONVERT(VARCHAR,BasketID)+']'
    
FROM Baskets,CartonCounter
    
WHERE CartonCountID <= (Egg_Cnt/12) AND BasketID = @BasketID
END
GO

----------------------Add extra eggs to existing cartons and add part carton if necessary
ALTER PROC SplitCartons(@BasketID INT)
AS
BEGIN
   SET NOCOUNT ON
   DECLARE
@eggs_to_allocate INT
   DECLARE
@DestinationCartonID INT
   DECLARE
@EggsToAllocateToCarton INT
  
   SELECT
@eggs_to_allocate = egg_cnt%12
    
FROM Baskets
    
WHERE BasketID = @BasketID
  
WHILE @eggs_to_allocate > 0
    
BEGIN
       SET
@DestinationCartonID = NULL
      
SELECT TOP 1 @DestinationCartonID = CartonID,
          
@EggsToAllocateToCarton = CASE WHEN @eggs_to_allocate > (12-Egg_Cnt) THEN (12-Egg_Cnt) ELSE @eggs_to_allocate END
         FROM
Cartons
        
WHERE Egg_Cnt < 12
        
ORDER BY Egg_Cnt
      
IF @DestinationCartonID IS NULL
        
BEGIN
         INSERT INTO
Cartons VALUES (@eggs_to_allocate, '[B' + CONVERT(VARCHAR,@BasketID)+']('+CONVERT(VARCHAR,@eggs_to_allocate)+')')
        
SET @EggsToAllocateToCarton = @eggs_to_allocate
        
END
       ELSE
         BEGIN
           UPDATE
Cartons
            
SET  egg_cnt = (egg_cnt + @EggsToAllocateToCarton)
               ,
sourceinfo = sourceinfo +  ' + [B' + CONVERT(VARCHAR,@BasketID)+']('+CONVERT(VARCHAR,@EggsToAllocateToCarton)+')'
            
WHERE CartonID = @DestinationCartonID
        
END
         SET
@eggs_to_allocate = @eggs_to_allocate - @EggsToAllocateToCarton
    
END
END
GO

DELETE FROM Cartons



DECLARE @newBasketID INT
INSERT INTO
Baskets VALUES (30)
SET @newBasketID = IDENT_CURRENT('Baskets')
EXEC SplitBaskets @newBasketID
EXEC SplitCartons @newBasketID


DECLARE @newBasketID INT
INSERT INTO
Baskets VALUES (50)
SET @newBasketID = IDENT_CURRENT('Baskets')
EXEC SplitBaskets @newBasketID
EXEC SplitCartons @newBasketID

GO

SELECT * FROM Cartons

Fitz


Subject: Silly response...
Posted by: puzsol (view profile)
Posted on: Tuesday, April 06, 2010 at 7:01 PM
Message: First of all, let me say I agree with Mark F... the statement of the question doesn't seem to match the real world... though Dkorzennik might be better placed to comment.

I think my solution below, only highlights that you can take normalisation too far.

@Peso... I think your split and merge functions are a little dodgy, though I couldn't test them as I don't have SQL 2008 installed (sad I know)... I think if a carton has less than 12 when you split it, it will cause an error.. or more exacly, the creation of eggs from thin air? Similarly, if you remove more eggs from the src carton than it holds, will it now hold negative eggs (and not be deleted)?

@Joe, re First Thoughts:
4 - You made cartons an entity, when you specified that they needed to be split and merged.. if not, then Mark's answer might have been even simpler, with only Baskets being tracked... which personally I think is still too granular for the business needs... how about just eggs collected today, cartons packed today, it's not like the little darlings come with an individual barcode.
6 & 7 - The syntax is new to 2008 isn't it... Ok so it's the new standard, but is that portable for people using 2000?


@Joe, re: Another Quick Puzzle
Seems to me that you are wanting to split multiple baskets, then merge multiple cartons as quickly as possible... is that just over-complicating things a little? (yeah I know I can talk).. I'm firmly with Mark F on this one.

Having said all that, here is the silly solution... hope the formatting comes through.

-- Clean up code.... always first
DROP VIEW CartonSummary

IF OBJECT_ID ('SplitBasket', 'P') IS NOT NULL
    
DROP PROCEDURE SplitBasket;
IF OBJECT_ID ('SplitCarton', 'P') IS NOT NULL
    
DROP PROCEDURE SplitCarton;
IF OBJECT_ID ('ConsolidateCartons', 'P') IS NOT NULL
    
DROP PROCEDURE ConsolidateCartons;

ALTER TABLE Egg
DROP CONSTRAINT carton_limit

DROP TABLE Egg
DROP FUNCTION fnCartonCount
DROP TABLE Basket
DROP TABLE Carton



Subject: Silly response pt 2
Posted by: puzsol (view profile)
Posted on: Tuesday, April 06, 2010 at 7:01 PM
Message:

-- The Basket table is used to track our collection of eggs
-- This allows for two separate processes - collection and packing
-- to run independently (though they are merged at the moment)
CREATE TABLE Basket
( basket_id INT NOT NULL IDENTITY(1,1) CONSTRAINT Basket_pk PRIMARY KEY
, eggs INT NOT NULL CONSTRAINT Basket_eggs_input CHECK (eggs > 0)
,
collected DATETIME NOT NULL CONSTRAINT Basket_collection_def DEFAULT GETDATE()
)

GO
-- The Carton table is used to track our shipping of eggs
-- We know when the carton was created (packed), and when it was
-- sent from the warehouse (never at the moment)
CREATE TABLE Carton
( carton_id INT NOT NULL IDENTITY(1,1) CONSTRAINT Carton_pk PRIMARY KEY
, packed DATETIME NOT NULL CONSTRAINT Carton_packed_def DEFAULT GETDATE()
,
shipped DATETIME NULL
)

GO
-- The Egg table, tracks individual eggs so we can tell when they were collected
-- And which basket they are in
CREATE TABLE Egg
( basket_id INT NOT NULL CONSTRAINT egg_in_basket REFERENCES Basket(basket_id)
,
carton_id INT NOT NULL CONSTRAINT egg_in_carton REFERENCES Carton(carton_id)
)

GO
-- This is a function used to enforce the carton limit constraints and
CREATE FUNCTION fnCartonCount
( @carton_id INT
) RETURNS INT
AS
BEGIN
RETURN
(
  
SELECT COUNT(*)
  
FROM Egg
  
WHERE carton_id = @carton_id
)
END

GO
-- Add the constraint to stop more than 12 eggs being placed in a carton
ALTER TABLE Egg
ADD CONSTRAINT carton_limit CHECK (dbo.fnCartonCount(carton_id) BETWEEN 1 AND 12)

GO
-- Create a view to provide a simple way to view the number of eggs in a carton
CREATE VIEW CartonSummary
AS
SELECT
c.carton_id, COUNT(*) AS eggs, c.packed, c.shipped
FROM Carton c
INNER JOIN Egg e ON e.carton_id = c.carton_id
GROUP BY c.carton_id, c.packed, c.shipped

GO
-- Procedure to process a basket of eggs into cartons
-- This could be modified to split an already existing basket record
-- to separate collection and packing
CREATE PROCEDURE dbo.SplitBasket
( @eggs INT
) AS
BEGIN
-- Create a record of the basket we collected
DECLARE @basket_id INT

INSERT INTO
Basket (eggs)
VALUES (@eggs)

SELECT @basket_id = SCOPE_IDENTITY()

-- Determine the split-up of the basket into carton sized amounts
DECLARE @cc TABLE (idx INT IDENTITY(1,1), eggs INT)

INSERT INTO @cc
SELECT eggs
FROM (
  
SELECT 12 AS eggs FROM Series WHERE seq <= (@eggs / 12)
  
UNION ALL
  
SELECT (@eggs % 12) AS eggs
) AS cx
WHERE cx.eggs > 0

-- Create a carton record for each of the carton amounts
DECLARE @c TABLE (idx INT IDENTITY(1,1), carton_id INT)

INSERT INTO Carton (packed)
output inserted.carton_id INTO @c
SELECT GETDATE()
FROM @cc

-- Put the eggs from the basket into the cartons
INSERT INTO Egg (basket_id, carton_id)
SELECT @basket_id, c.carton_id
FROM @c AS c
INNER JOIN @cc AS cc ON cc.idx = c.idx
CROSS apply
(
  
SELECT 1 AS val FROM Series WHERE seq <= cc.eggs
) AS cx
END

GO
-- Procedure to take part of a carton and put them in a new one
CREATE PROCEDURE dbo.SplitCarton
( @carton_id INT
, @eggs_to_move INT
) AS
BEGIN
-- Take the eggs out of the current carton, keeping track of where they came from
DECLARE @b TABLE (basket_id INT, carton_id INT)

DELETE TOP (@eggs_to_move)
FROM e
output deleted.basket_id
, deleted.carton_id INTO @b
FROM Egg e
WHERE e.carton_id = @carton_id
AND EXISTS (
  
-- Don't allow the unpacking of a carton that has already shipped
   -- or doesn't have enough eggs to fill the request
  
SELECT *
  
FROM CartonSummary
  
WHERE carton_id = @carton_id
  
AND shipped IS NULL
   AND
eggs > @eggs_to_move
)

-- If we took some eggs out of the specified carton, then put them in a new one
IF EXISTS (SELECT * FROM @b)
BEGIN
   DECLARE
@new_carton_id INT    

   INSERT INTO
Carton (packed)
  
SELECT GETDATE()

  
SELECT @new_carton_id = SCOPE_IDENTITY()

  
INSERT INTO Egg (basket_id, carton_id)
  
sellect b.basket_id, @new_carton_id
  
FROM @b b
END
END


GO
-- Procedure to merge two cartons, eggs are taken from the src to fill the dst
CREATE PROCEDURE dbo.ConsolidateCartons
( @src_carton_id INT
, @dst_carton_id INT
) AS
BEGIN
-- Determine the amount of eggs we can move
DECLARE @src_eggs INT

SELECT
@src_eggs = eggs FROM CartonSummary
WHERE carton_id = @src_carton_id
AND shipped IS NULL

-- Determine how many eggs there are
DECLARE @dst_eggs INT

SELECT
@dst_eggs = eggs FROM CartonSummary
WHERE carton_id = @dst_carton_id
AND shipped IS NULL

-- If we have eggs, the carton hasn't shipped and the destination is not full...
IF (NOT @dst_eggs IS NULL AND NOT @src_eggs IS NULL AND @dst_eggs < 12)
BEGIN
  
-- Calculate how many eggs we want to take from the src
  
DECLARE @eggs_to_move INT

   SELECT
@eggs_to_move = CASE WHEN @dst_eggs + @src_eggs > 12 THEN 12 - @dst_eggs ELSE @src_eggs END

  
-- Take the eggs out of the src
  
DECLARE @b TABLE (basket_id INT, carton_id INT)
  
DELETE TOP (@eggs_to_move)
  
FROM e
   output deleted.basket_id
, deleted.carton_id INTO @b
  
FROM Egg e
  
WHERE e.carton_id = @src_carton_id

  
-- If we removed any eggs, then put them in the destination carton
  
IF EXISTS (SELECT * FROM @b)
  
BEGIN
     INSERT INTO
Egg (basket_id, carton_id)
    
SELECT b.basket_id, @dst_carton_id
    
FROM @b b
  
END

  
-- If we removed all the eggs from the src, then destroy the carton
  
IF @src_eggs = @eggs_to_move
  
BEGIN
     DELETE FROM
Carton
    
WHERE carton_id = @src_carton_id
  
END
END
END

GO
/*
-- Test script (on a freshly created table)
exec SplitBasket 99
exec SplitBasket 144
exec SplitBasket 169


begin try
-- Try to insert just one more egg...
insert into Egg values (1, 1)
select 'Constraint failed!!!!'
end try
begin catch
select 'Constraint seccessfully tested'
end catch

select * from CartonSummary
where eggs < 12

exec SplitCarton 35, 6

select * from CartonSummary
where eggs < 12

exec SplitCarton 37, 4

select * from CartonSummary
where eggs < 12

exec ConsolidateCartons 36, 9

select * from CartonSummary
where eggs < 12

exec ConsolidateCartons 37, 9

select * from CartonSummary
where eggs < 12

exec ConsolidateCartons 38, 9

select * from CartonSummary
where eggs < 12

exec ConsolidateCartons 35, 9

select * from CartonSummary
where eggs < 12 or carton_id = 9
*/





Subject: Farmer Dave's solution
Posted by: Dkorzennik (view profile)
Posted on: Wednesday, April 07, 2010 at 12:24 AM
Message: Hi guys

The hamster was sprinting last night. ;-). Here's what he came up with. Basically I've got 2 tables and 1 trigger, which contains all the code necessary to do what Joe wants. All you do is insert into the baskets table and the trigger takes care of splitting the basket into cartons, and consolidating odd lots back up into cartons.

The nice thing here is that this maps to a business process, capture production for a day per basket. In my realworld farm scenario a basket is analogous to a coup. In each coup I have between 100 - 150 hens. I want to track each coups production over time and work out the lay rate as a percentage. So capturing the production (basket) per day works. Also, as a business, I'm not interested in splitting and joining cartons manually, that must just happen!

Herewith the code:


CREATE TABLE baskets
(
ID INT IDENTITY(1,1),
Basket_ID INT,
Date_collected DATETIME,
Egg_Cnt SMALLINT,
Dozen_Cnt AS FLOOR(Egg_Cnt/12) PERSISTED,
Eggs_Left AS (Egg_Cnt%12) PERSISTED
)

CREATE TABLE cartons
(
Carton_ID INT IDENTITY(1,1),
Basket_ID INT,
Egg_Cnt SMALLINT CHECK (Egg_cnt <= 12)
)
GO

ALTER TRIGGER cartonsSplit ON dbo.Baskets
AFTER
INSERT
AS
BEGIN
DECLARE
@BasketID INT
DECLARE
@Dozen_Cnt SMALLINT
DECLARE @Eggs_Left SMALLINT

SET @BasketID = (SELECT Basket_ID FROM INSERTED)
SET @Dozen_Cnt = (SELECT Dozen_Cnt FROM INSERTED)
SET @Eggs_Left = (SELECT Eggs_Left FROM INSERTED)

WHILE @Dozen_Cnt > 0
BEGIN
INSERT
Cartons VALUES(@BasketID, 12)
SET @Dozen_Cnt = @Dozen_Cnt - 1
END
INSERT
Cartons VALUES(@BasketID, @Eggs_Left)

DECLARE @Egg_CntSum TINYINT
SET @Egg_CntSum = (SELECT SUM(Egg_Cnt) FROM cartons WHERE Egg_Cnt < 12)
IF @Egg_CntSum >= 12
BEGIN
INSERT
Cartons VALUES(0, 12)
DELETE FROM Cartons WHERE Egg_Cnt < 12
SET @Egg_CntSum = @Egg_CntSum - 12
INSERT Cartons VALUES(0, @Egg_CntSum)
DELETE Cartons WHERE Basket_ID = 0 AND Egg_Cnt = 0
END
END


Tada! Simple and elegant. And it works. The entries in the cartons table with zero in the Basket_ID column indicate odd lots that have been consolidated. There will only ever be whole cartons and cartons totalling less than 12 eggs in the cartons table.

Triggers are SQL:1999 complaint, so any decent RDBMS which complies to this standard can be used to port the above code to.

Subject: Testing my solution
Posted by: Dkorzennik (view profile)
Posted on: Wednesday, April 07, 2010 at 12:37 AM
Message: Sorry, should have added the insert statements to test the above. Its straight forward, an insert for each basket (coup) per day, assuming I have 5 coups:

INSERT Baskets VALUES(1, '2010/04/07', 26)
INSERT Baskets VALUES(2, '2010/04/07', 31)
INSERT Baskets VALUES(3, '2010/04/07', 25)
INSERT Baskets VALUES(4, '2010/04/07', 44)
INSERT Baskets VALUES(5, '2010/04/07', 57)


Subject: To Puzsol
Posted by: Peso (view profile)
Posted on: Wednesday, April 07, 2010 at 1:46 AM
Message: The check constraint will fire an error if the number of carton eggs is less than 1, or more than 12, so the operation will automatically roll-back.
And I decided to not have a Baskets table, because it is equally easy to call a stored procedure directly with the number of eggs in the basket as a parameter, as it is to store the basket in a table.

Subject: Help!
Posted by: Pierre (view profile)
Posted on: Wednesday, April 07, 2010 at 4:38 AM
Message: I can't seem to post my solution as it says it's too long!

According to the field validator, this text box should allow 8,000 chars and mine is ~7,000 but it still won't let me post it :o(

Subject: Part 1 of 3
Posted by: Pierre (view profile)
Posted on: Wednesday, April 07, 2010 at 5:29 AM
Message:

CREATE TABLE [Location] (
  
[LocationId] INT NOT NULL IDENTITY (1, 1),
  
[Location] VARCHAR(255) NOT NULL,
  
CONSTRAINT [PK_Location] PRIMARY KEY CLUSTERED ([LocationId])
)
GO

INSERT INTO [Location] ([Location])
SELECT 'Joe''s Barn' UNION
SELECT
'Celko''s Yard'
GO

CREATE TABLE [Basket] (
  
[BasketId] INT NOT NULL IDENTITY (1, 1),
  
[LocationId] INT NOT NULL,
  
[CollectedWhen] DATETIME NOT NULL,
  
CONSTRAINT [PK_Basket] PRIMARY KEY CLUSTERED ([BasketId]),
  
CONSTRAINT [FK_Basket_Location] FOREIGN KEY ([LocationId]) REFERENCES [Location] ([LocationId]),
  
CONSTRAINT [CK_Basket_CollectedWhen] CHECK ([CollectedWhen] <= GETDATE())
)
GO

CREATE TABLE [Egg] (
  
[EggId] INT NOT NULL IDENTITY (1, 1),
  
[BasketId] INT NOT NULL,
  
[BasketNum] TINYINT NOT NULL,
  
[BasketPrevNum] TINYINT NULL,
  
CONSTRAINT [PK_Egg] PRIMARY KEY CLUSTERED ([EggId])
)
GO

CREATE UNIQUE INDEX [IX_Egg_BasketNum] ON [Egg] ([BasketId], [BasketNum])
GO

ALTER TABLE [Egg] ADD
CONSTRAINT
[FK_Egg_Egg] FOREIGN KEY ([BasketId], [BasketPrevNum]) REFERENCES [Egg] ([BasketId], [BasketNum]),
CONSTRAINT [CK_Egg_BasketNum] CHECK ([BasketNum] = ISNULL([BasketPrevNum], 0) + 1)
GO

CREATE TABLE [Carton] (
  
[CartonId] INT NOT NULL IDENTITY (1, 1),
  
[PackagedWhen] DATETIME NOT NULL,
  
CONSTRAINT [PK_Carton] PRIMARY KEY CLUSTERED ([CartonId]),
  
CONSTRAINT [CK_Carton_PackagedWhen] CHECK ([PackagedWhen] <= GETDATE())
)
GO

CREATE TABLE [CartonContent] (
  
[CartonId] INT NOT NULL,
  
[EggId] INT NOT NULL,
  
[CartonNum] TINYINT NOT NULL,
  
[CartonPrevNum] TINYINT NULL,
  
CONSTRAINT [PK_CartonContent] PRIMARY KEY CLUSTERED ([CartonId], [CartonNum]),
  
CONSTRAINT [FK_CartonContent_Carton] FOREIGN KEY ([CartonId]) REFERENCES [Carton] ([CartonId]),
  
CONSTRAINT [FK_CartonContent_Egg] FOREIGN KEY ([EggId]) REFERENCES [Egg] ([EggId]),
  
CONSTRAINT [FK_CartonContent_CartonContent] FOREIGN KEY ([CartonId], [CartonPrevNum]) REFERENCES [CartonContent] ([CartonId], [CartonNum]),
  
CONSTRAINT [CK_CartonContent_CartonNum] CHECK ([CartonNum] = ISNULL([CartonPrevNum], 0) + 1 AND [CartonNum] <= 12)
)
GO

CREATE UNIQUE INDEX [IX_CartonContent_EggId] ON [CartonContent] ([EggId])
GO


Subject: Part 2 of 3
Posted by: Pierre (view profile)
Posted on: Wednesday, April 07, 2010 at 5:30 AM
Message:

CREATE PROCEDURE [LogBasketCollection] (@LocationId INT, @NumberOfEggs TINYINT, @CollectedWhen DATETIME = NULL)
AS
BEGIN
SET NOCOUNT ON
DECLARE
@BasketId INT
INSERT INTO
[Basket] ([LocationId], [CollectedWhen]) VALUES (@LocationId, ISNULL(@CollectedWhen, GETDATE()))
SET @BasketId = SCOPE_IDENTITY()
INSERT INTO [Egg] ([BasketId], [BasketNum], [BasketPrevNum])
SELECT @BasketId, [number], NULLIF([number] - 1, 0)
FROM [master]..[spt_values]
WHERE [type] = 'P'
  
AND [number] BETWEEN 1 AND @NumberOfEggs
RETURN @BasketId
END
GO

CREATE PROCEDURE [SplitBaskets]
AS
BEGIN
SET NOCOUNT ON
DECLARE
@CartonId INT
WHILE
EXISTS (
  
SELECT 1
  
FROM [Egg] e
    
LEFT JOIN [CartonContent] cc ON e.[EggId] = cc.[EggId]
  
WHERE cc.[CartonId] IS NULL
  )
BEGIN
  INSERT INTO
[Carton] ([PackagedWhen]) VALUES (GETDATE())
  
SET @CartonId = SCOPE_IDENTITY()
  ;
WITH [Eggs] ([EggId], [CartonNum]) AS (
    
SELECT e.[EggId], ROW_NUMBER() OVER (ORDER BY b.[CollectedWhen], e.[BasketId], e.[BasketNum])
    
FROM [Egg] e
    
JOIN [Basket] b ON e.[BasketId] = b.[BasketId]
    
LEFT JOIN [CartonContent] cc ON e.[EggId] = cc.[EggId]
    
WHERE cc.[CartonId] IS NULL
   )
  
INSERT INTO [CartonContent] ([CartonId], [EggId], [CartonNum], [CartonPrevNum])
  
SELECT @CartonId, [EggId], [CartonNum], NULLIF([CartonNum] - 1, 0)
  
FROM [Eggs]
  
WHERE [CartonNum] <= 12
END
END
GO

CREATE PROCEDURE [SplitCarton](@CartonId INT, @NumberOfEggs TINYINT)
AS
BEGIN
SET NOCOUNT ON
IF
NOT EXISTS (SELECT 1 FROM [Carton] WHERE [CartonId] = @CartonId) BEGIN
  RAISERROR
('The specified carton does not exist!', 16, 1)
  
RETURN
END
IF
EXISTS (SELECT 1 FROM [CartonContent] WHERE [CartonId] = @CartonId GROUP BY [CartonId] HAVING COUNT(*) <= @NumberOfEggs) BEGIN
  RAISERROR
('The number of eggs to be moved must be less than the number of eggs in the carton!', 16, 1)
  
RETURN
END
DECLARE
@NewCartonId INT
INSERT INTO
[Carton] ([PackagedWhen]) VALUES (GETDATE())
SET @NewCartonId = SCOPE_IDENTITY()
;
WITH [EggsToMove] ([CartonId], [EggId], [CartonNum], [CartonPrevNum]) AS (
  
SELECT
    
@NewCartonId,
    
[EggId],
    
ROW_NUMBER() OVER (ORDER BY cc.[CartonNum]),
    
NULLIF(ROW_NUMBER() OVER (ORDER BY cc.[CartonNum]) - 1, 0)
  
FROM [CartonContent] cc
    
JOIN (
      
SELECT [CartonId], COUNT(*) AS [EggsInCarton]
      
FROM [CartonContent]
      
GROUP BY [CartonId]
    
) eic ON cc.[CartonId] = eic.[CartonId]
  
WHERE cc.[CartonId] = @CartonId
    
AND cc.[CartonNum] > eic.[EggsInCarton] - @NumberOfEggs
  
)
UPDATE cc
SET
  
[CartonId] = etm.[CartonId],
  
[CartonNum] = etm.[CartonNum],
  
[CartonPrevNum] = etm.[CartonPrevNum]
FROM [EggsToMove] etm
  
JOIN [CartonContent] cc ON etm.[EggId] = cc.[EggId]
RETURN
END
GO

CREATE PROCEDURE [ConsolidateCartons](@SourceCartonId INT, @DestinationCartonId INT)
AS
BEGIN
SET NOCOUNT ON
IF
NOT EXISTS (SELECT 1 FROM [Carton] WHERE [CartonId] = @SourceCartonId) BEGIN
  RAISERROR
('The specified source carton does not exist!', 16, 1)
  
RETURN
END
IF
NOT EXISTS (SELECT 1 FROM [Carton] WHERE [CartonId] = @SourceCartonId) BEGIN
  RAISERROR
('The specified destination carton does not exist!', 16, 1)
  
RETURN
END
IF
EXISTS (SELECT 1 FROM [CartonContent] WHERE [CartonId] IN (@SourceCartonId, @DestinationCartonId) HAVING COUNT(*) <= 12) BEGIN
  RAISERROR
('The consolidation would result in the source carton being empty!', 16, 1)
  
RETURN
END
;WITH [EggsToMove] ([CartonId], [EggId], [CartonNum], [CartonPrevNum]) AS (
  
SELECT
    
@DestinationCartonId,
    
cc.[EggId],
    
ROW_NUMBER() OVER (ORDER BY cc.[CartonNum]) + dc.[EggsInDestinationCarton],
    
NULLIF(ROW_NUMBER() OVER (ORDER BY cc.[CartonNum]) + dc.[EggsInDestinationCarton] - 1, 0)
  
FROM [CartonContent] cc
    
JOIN (
      
SELECT [CartonId], COUNT(*) AS [EggsInSourceCarton]
      
FROM [CartonContent]
      
GROUP BY [CartonId]
    
) sc ON cc.[CartonId] = sc.[CartonId]
    
CROSS JJOIN (
      
SELECT COUNT(*) AS [EggsInDestinationCarton]
      
FROM [CartonContent]
      
WHERE [CartonId] = @DestinationCartonId
    
) dc
  
WHERE cc.[CartonId] = @SourceCartonId
    
AND cc.[CartonNum] > sc.[EggsInSourceCarton] - (12 - dc.[EggsInDestinationCarton])
  )
UPDATE cc
SET
  
[CartonId] = etm.[CartonId],
  
[CartonNum] = etm.[CartonNum],
  
[CartonPrevNum] = etm.[CartonPrevNum]
FROM [EggsToMove] etm
  
JOIN [CartonContent] cc ON etm.[EggId] = cc.[EggId]
RETURN
END
GO

CREATE VIEW [Cartons]
AS
SELECT
  
c.[CartonId],
  
c.[PackagedWhen],
  
COUNT(cc.[EggId]) AS [NumberOfEggs],
  
s.[Source],
  
MIN(b.[CollectedWhen]) AS [OldestEgg],
  
MAX(b.[CollectedWhen]) AS [NewestEgg]
FROM [Carton] c
  
LEFT JOIN [CartonContent] cc
    
JOIN [Egg] e ON cc.[EggId] = e.[EggId]
    
JOIN [Basket] b ON e.[BasketId] = b.[BasketId]
  
ON c.[CartonId] = cc.[CartonId]
  
OUTER APPLY (
    
SELECT [Source] = (
      
SELECT CASE WHEN ROW_NUMBER() OVER (ORDER BY COUNT(*) DESC, l.[Location]) > 1 THEN ', ' ELSE '' END
      
+ CONVERT(VARCHAR(10), COUNT(*)) + ' egg(s) from ' + l.[Location]
      
FROM [CartonContent] cc
      
JOIN [Egg] e ON cc.[EggId] = e.[EggId]
      
JOIN [Basket] b ON e.[BasketId] = b.[BasketId]
      
JOIN [Location] l ON b.[LocationId] = l.[LocationId]
      
WHERE cc.[CartonId] = c.[CartonId]
      
GROUP BY l.[Location]
      
ORDER BY COUNT(*) DESC, l.[Location]
      
FOR XML PATH('')
     )
   )
s
GROUP BY c.[CartonId], c.[PackagedWhen], s.[Source]
GO


Subject: Part 3 of 3
Posted by: Pierre (view profile)
Posted on: Wednesday, April 07, 2010 at 5:30 AM
Message:

EXEC [LogBasketCollection] 1, 29, '2010-04-01'
EXEC [LogBasketCollection] 2, 53, '2010-04-02'
EXEC [SplitBaskets]
SELECT * FROM [Cartons] ORDER BY [CartonId]
EXEC [SplitCarton] 2, 4
SELECT * FROM [Cartons] ORDER BY [CartonId]
EXEC [ConsolidateCartons] 7, 8
SELECT * FROM [Cartons] ORDER BY [CartonId]


Subject: My Solution
Posted by: Pierre (view profile)
Posted on: Wednesday, April 07, 2010 at 5:37 AM
Message: The solution above may be a bit of overkill, but it allows you to track the eggs back to their original basket/location.

Apologies for the formatting. Can anyone tell me the pseudo-tags to format a post?

Subject: Not so silly
Posted by: puzsol (view profile)
Posted on: Wednesday, April 07, 2010 at 9:18 AM
Message: Ok, so here's a "solution" that does what I threatened earler... not tracking cartons at all!

Technically, it doesn't meet the question requirements, but hey, I like it better than my first attempt.

Change the assumptions that we never split a carton unless the eggs are no good (they failed the UV test and had a chick in them, or they were too round or something.) In that case, you can remove as many as you like from a basket (till 0)

Make the assumption that as long as the basket is still in date (last 24 hours), that we can use them first to pack the next basket.

Then add the feature of being able to have different carton sized... I don't know about where you guys are from, but around here you can go to the market and get a tray of eggs (36 I think).

Anyway, make those changes and the solution looks like this:

IF OBJECT_ID ('Basket', 'U') IS NOT NULL
    
DROP TABLE Basket;

GO
CREATE TABLE Basket
( id INT NOT NULL IDENTITY(1,1) CONSTRAINT Basket_pk PRIMARY KEY
, eggs INT NOT NULL
,
carton_size INT NOT NULL
,
cartons AS (eggs / carton_size)
,
eggs_remaining AS (eggs % carton_size)
,
collected DATETIME NOT NULL CONSTRAINT Basket_collected_def DEFAULT GETDATE()
)

GO
IF OBJECT_ID ('SplitBasket', 'P') IS NOT NULL
    
DROP PROCEDURE SplitBasket;

GO
CREATE PROCEDURE SplitBasket
( @eggs INT
, @carton_size INT = 12
) AS
BEGIN
DECLARE
@r TABLE (id INT, eggs INT)

UPDATE b
SET b.eggs = b.eggs - b.eggs_remaining
output deleted.id
, deleted.eggs_remaining INTO @r
FROM Basket b
WHERE DATEDIFF(hour, b.collected, GETDATE()) <= 24
AND b.eggs_remaining > 0

DECLARE @rem TABLE (eggs INT)

INSERT INTO Basket (eggs, carton_size)
output inserted.eggs_remaining INTO @rem
SELECT ISNULL((SELECT SUM(eggs) FROM @r), 0) + @eggs, @carton_size

SELECT 'Use the ' + CAST(r.eggs AS NVARCHAR) + ' eggs from basket ' + CAST(r.id AS NVARCHAR)
FROM @r AS r
UNION ALL
SELECT 'Once finished, you should have ' + CAST((SELECT SUM(eggs) FROM @rem) AS NVARCHAR) + ' eggs remaining.'
END

GO
IF OBJECT_ID ('SplitCarton', 'P') IS NOT NULL
    
DROP PROCEDURE SplitCarton;
GO
CREATE PROCEDURE SplitCarton
( @id INT
, @eggs_to_remove INT
) AS
BEGIN
DECLARE
@rem TABLE (eggs INT, eggs_remaining INT)

UPDATE Basket
SET eggs = eggs - @eggs_to_remove
output inserted.eggs, inserted.eggs_remaining INTO @rem
WHERE id = @id
AND eggs >= @eggs_to_remove

IF EXISTS (SELECT * FROM @rem WHERE eggs = 0)
  
SELECT 'The whole basket was ruined, nothing left of basket ' + CAST(@id AS NVARCHAR) + '!'
ELSE
   SELECT
'Once finished, you should have ' + CAST((SELECT SUM(eggs) FROM @rem) AS NVARCHAR) + ' eggs in a split carton.'
END

/*
-- ye old test script

exec SplitBasket 94

select * from Basket

exec SplitBasket 144
exec SplitBasket 169

select * from Basket

update Basket
set collected = dateadd(hour, -48, collected)

select * from Basket

exec SplitBasket 48
exec SplitBasket 71
exec SplitBasket 132

select * from Basket

exec SplitCarton 5, 8

select * from Basket

exec SplitBasket 101

select * from Basket

exec SplitCarton 5, 48

select * from Basket

exec SplitBasket 169, 36
select * from Basket
*/


Subject: code formatting...
Posted by: puzsol (view profile)
Posted on: Wednesday, April 07, 2010 at 9:23 AM
Message: Sigh, posting in IE, or Chrome with the [code] tags doesn't work... how are we supposed to make our postings look nice?

Oh yeah, Peso, I'm still not sure if you answered what happens if you do this:
exec SplitCarton 1, 4
exec SplitCarton 1, 4
Do you end up with 3 cartons with 4 in them, or one with 8 and two with 4? (if a, please explain how the output clause got to the variable... if b, where did the extra eggs come from?)

Subject: update
Posted by: puzsol (view profile)
Posted on: Wednesday, April 07, 2010 at 9:34 AM
Message: Sorry... just noticed a missing use case in the SplitCarton function:

Once again, please note that in the altered model, the assumption is that the only reason eggs are removed from a carton once packed is because they are no longer any good, so they are not put back in the pile for re-use.

I might also have to update the reporting from the splitBasket function to report the basket id, for labelling purposes... but you get the idea.


GO
IF OBJECT_ID ('SplitCarton', 'P') IS NOT NULL
    
DROP PROCEDURE SplitCarton;
GO
CREATE PROCEDURE SplitCarton
( @id INT
, @eggs_to_remove INT
) AS
BEGIN
DECLARE
@rem TABLE (eggs INT, eggs_remaining INT)

UPDATE Basket
SET eggs = eggs - @eggs_to_remove
output inserted.eggs, inserted.eggs_remaining INTO @rem
WHERE id = @id
AND eggs >= @eggs_to_remove

IF EXISTS (SELECT * FROM @rem)
BEGIN
   IF
EXISTS (SELECT * FROM @rem WHERE eggs = 0)
    
SELECT 'The whole basket was ruined, nothing left of basket ' + CAST(@id AS NVARCHAR) + '!'
  
ELSE
     SELECT
'Once finished, you should have ' + CAST((SELECT SUM(eggs) FROM @rem) AS NVARCHAR) + ' eggs in a split carton.'
END
ELSE
   SELECT
'Can''t remove more eggs from a basket than it holds.'
END


Subject: My Solution: Part 1
Posted by: yellowfog (view profile)
Posted on: Wednesday, April 07, 2010 at 5:56 PM
Message: I've had a lot of fun with this! I decided early on that I was going to make the 'bold' (one of my colleagues has described it as naively stupid) choice to have a 'Carton' table with 12 egg-holding columns, each of them being a foreign key to my 'Basket' table. A non-zero entry in any one of these columns indicates the presence of an egg, and the value indicates which basket it derives from. I did it this way because I think that the basket provenance of an egg is key, and I got confused as to how I could otherwise track eggs given the ability to arbitrarily consolidate and split cartons with eggs derived from different baskets. And there's just something appealing about modelling 12 egg-holders with 12 egg-holding columns.

I also decided to do it, as per the rubric, with low tech SQL. Luckily, my tech level isn't all that high.

The main problem I had was adding more eggs to a row where that row could have arbitry egg entries. But this became easy after I could shove all the eggs to the left hand columns of the table, leaving the right hand columns free for more eggs.

Oh, and I don't think that there's an 'empty carton' problem. The possibility of emptiness is inherent in cartons.


-- TABLE DEFINITIONS

-- Holds data about baskets. Could be updated with good data about time, basket filler, etc.
CREATE TABLE [dbo].[Basket]
(
[BasketID] [int] NOT NULL,
[NumberOfEggs] [int] NOT NULL DEFAULT ((0)),
CONSTRAINT [PK_Basket] PRIMARY KEY CLUSTERED
(
[BasketID] ASC
)
)
GO
ALTER TABLE [dbo].[Basket] WITH CHECK ADD CONSTRAINT [CK_Basket] CHECK (([NumberOfEggs]>=(0)))
GO
ALTER TABLE [dbo].[Basket] CHECK CONSTRAINT [CK_Basket]


-- Holds data about Cartons.
CREATE TABLE [dbo].[Carton]
(
[CartonID] [int] IDENTITY(1,1) NOT NULL,
[Box1] [int] NOT NULL CONSTRAINT [DF_Carton_Box1] DEFAULT ((0)),
[Box2] [int] NOT NULL CONSTRAINT [DF_Carton_Box2] DEFAULT ((0)),
[Box3] [int] NOT NULL CONSTRAINT [DF_Carton_Box3] DEFAULT ((0)),
[Box4] [int] NOT NULL CONSTRAINT [DF_Carton_Box4] DEFAULT ((0)),
[Box5] [int] NOT NULL CONSTRAINT [DF_Carton_Box5] DEFAULT ((0)),
[Box6] [int] NOT NULL CONSTRAINT [DF_Carton_Box6] DEFAULT ((0)),
[Box7] [int] NOT NULL CONSTRAINT [DF_Carton_Box7] DEFAULT ((0)),
[Box8] [int] NOT NULL CONSTRAINT [DF_Carton_Box8] DEFAULT ((0)),
[Box9] [int] NOT NULL CONSTRAINT [DF_Carton_Box9] DEFAULT ((0)),
[Box10] [int] NOT NULL CONSTRAINT [DF_Carton_Box10] DEFAULT ((0)),
[Box11] [int] NOT NULL CONSTRAINT [DF_Carton_Box11] DEFAULT ((0)),
[Box12] [int] NOT NULL CONSTRAINT [DF_Carton_Box12] DEFAULT ((0)),
CONSTRAINT [PK_Carton] PRIMARY KEY CLUSTERED
(
[CartonID] ASC
)
)

GO
ALTER TABLE [dbo].[Carton] WITH CHECK ADD CONSTRAINT [FK_Box1] FOREIGN KEY([Box1])
REFERENCES [dbo].[Basket] ([BasketID])
GO
ALTER TABLE [dbo].[Carton] CHECK CONSTRAINT [FK_Box1]
GO
ALTER TABLE [dbo].[Carton] WITH CHECK ADD CONSTRAINT [FK_Box10] FOREIGN KEY([Box10])
REFERENCES [dbo].[Basket] ([BasketID])
GO
ALTER TABLE [dbo].[Carton] CHECK CONSTRAINT [FK_Box10]
GO
ALTER TABLE [dbo].[Carton] WITH CHECK ADD CONSTRAINT [FK_Box11] FOREIGN KEY([Box11])
REFERENCES [dbo].[Basket] ([BasketID])
GO
ALTER TABLE [dbo].[Carton] CHECK CONSTRAINT [FK_Box11]
GO
ALTER TABLE [dbo].[Carton] WITH CHECK ADD CONSTRAINT [FK_Box12] FOREIGN KEY([Box12])
REFERENCES [dbo].[Basket] ([BasketID])
GO
ALTER TABLE [dbo].[Carton] CHECK CONSTRAINT [FK_Box12]
GO
ALTER TABLE [dbo].[Carton] WITH CHECK ADD CONSTRAINT [FK_Box2] FOREIGN KEY([Box2])
REFERENCES [dbo].[Basket] ([BasketID])
GO
ALTER TABLE [dbo].[Carton] CHECK CONSTRAINT [FK_Box2]
GO
ALTER TABLE [dbo].[Carton] WITH CHECK ADD CONSTRAINT [FK_Box3] FOREIGN KEY([Box3])
REFERENCES [dbo].[Basket] ([BasketID])
GO
ALTER TABLE [dbo].[Carton] CHECK CONSTRAINT [FK_Box3]
GO
ALTER TABLE [dbo].[Carton] WITH CHECK ADD CONSTRAINT [FK_Box4] FOREIGN KEY([Box4])
REFERENCES [dbo].[Basket] ([BasketID])
GO
ALTER TABLE [dbo].[Carton] CHECK CONSTRAINT [FK_Box4]
GO
ALTER TABLE [dbo].[Carton] WITH CHECK ADD CONSTRAINT [FK_Box5] FOREIGN KEY([Box5])
REFERENCES [dbo].[Basket] ([BasketID])
GO
ALTER TABLE [dbo].[Carton] CHECK CONSTRAINT [FK_Box5]
GO
ALTER TABLE [dbo].[Carton] WITH CHECK ADD CONSTRAINT [FK_Box6] FOREIGN KEY([Box6])
REFERENCES [dbo].[Basket] ([BasketID])
GO
ALTER TABLE [dbo].[Carton] CHECK CONSTRAINT [FK_Box6]
GO
ALTER TABLE [dbo].[Carton] WITH CHECK ADD CONSTRAINT [FK_Box7] FOREIGN KEY([Box7])
REFERENCES [dbo].[Basket] ([BasketID])
GO
ALTER TABLE [dbo].[Carton] CHECK CONSTRAINT [FK_Box7]
GO
ALTER TABLE [dbo].[Carton] WITH CHECK ADD CONSTRAINT [FK_Box8] FOREIGN KEY([Box8])
REFERENCES [dbo].[Basket] ([BasketID])
GO
ALTER TABLE [dbo].[Carton] CHECK CONSTRAINT [FK_Box8]
GO
ALTER TABLE [dbo].[Carton] WITH CHECK ADD CONSTRAINT [FK_Box9] FOREIGN KEY([Box9])
REFERENCES [dbo].[Basket] ([BasketID])
GO
ALTER TABLE [dbo].[Carton] CHECK CONSTRAINT [FK_Box9]

Subject: My Solution: Part 2
Posted by: yellowfog (view profile)
Posted on: Wednesday, April 07, 2010 at 5:56 PM
Message: -- SUPPORT FUNCTIONALITY

-- Description: Counts the eggs in a Carton
CREATE FUNCTION [dbo].[NumberOfEggsInCarton]
(
@CartonID int
)
RETURNS int
AS
BEGIN

declare @count int
select @count =
case when Box1>0 then 1 else 0 end +
case when Box2>0 then 1 else 0 end +
case when Box3>0 then 1 else 0 end +
case when Box4>0 then 1 else 0 end +
case when Box5>0 then 1 else 0 end +
case when Box6>0 then 1 else 0 end +
case when Box7>0 then 1 else 0 end +
case when Box8>0 then 1 else 0 end +
case when Box9>0 then 1 else 0 end +
case when Box10>0 then 1 else 0 end +
case when Box11>0 then 1 else 0 end +
case when Box12>0 then 1 else 0 end
from Carton
where CartonID = @CartonID

return @count

END


-- Description: sends all the eggs in a carton to the left hand columns
-- (incidentally in order of size)
CREATE PROCEDURE [dbo].[SortCarton]
@CartonID int
AS
BEGIN

SET NOCOUNT ON;

update Carton set
Box1 = Box1Value,
Box2 = Box2Value,
Box3 = Box3Value,
Box4 = Box4Value,
Box5 = Box5Value,
Box6 = Box6Value,
Box7 = Box7Value,
Box8 = Box8Value,
Box9 = Box9Value,
Box10 = Box10Value,
Box11 = Box11Value,
Box12 = Box12Value
from
(
select
sum (case when ColumnNumber=1 then BoxValue else 0 end) as Box1Value,
sum (case when ColumnNumber=2 then BoxValue else 0 end) as Box2Value,
sum (case when ColumnNumber=3 then BoxValue else 0 end) as Box3Value,
sum (case when ColumnNumber=4 then BoxValue else 0 end) as Box4Value,
sum (case when ColumnNumber=5 then BoxValue else 0 end) as Box5Value,
sum (case when ColumnNumber=6 then BoxValue else 0 end) as Box6Value,
sum (case when ColumnNumber=7 then BoxValue else 0 end) as Box7Value,
sum (case when ColumnNumber=8 then BoxValue else 0 end) as Box8Value,
sum (case when ColumnNumber=9 then BoxValue else 0 end) as Box9Value,
sum (case when ColumnNumber=10 then BoxValue else 0 end) as Box10Value,
sum (case when ColumnNumber=11 then BoxValue else 0 end) as Box11Value,
sum (case when ColumnNumber=12 then BoxValue else 0 end) as Box12Value
from
(
select
T1.BoxValue, count(*) as ColumnNumber
from
(
select Box1 as BoxValue, 1 as Num from Carton where CartonID = @CartonID
union all
select Box2, 2 from Carton where CartonID = @CartonID
union all
select Box3, 3 from Carton where CartonID = @CartonID
union all
select Box4, 4 from Carton where CartonID = @CartonID
union all
select Box5, 5 from Carton where CartonID = @CartonID
union all
select Box6, 6 from Carton where CartonID = @CartonID
union all
select Box7, 7 from Carton where CartonID = @CartonID
union all
select Box8, 8 from Carton where CartonID = @CartonID
union all
select Box9, 9 from Carton where CartonID = @CartonID
union all
select Box10, 10 from Carton where CartonID = @CartonID
union all
select Box11, 11 from Carton where CartonID = @CartonID
union all
select Box12, 12 from Carton where CartonID = @CartonID
) T1
inner join
(
select Box1 as BoxValue, 1 as Num from Carton where CartonID = @CartonID
union all
select Box2, 2 from Carton where CartonID = @CartonID
union all
select Box3, 3 from Carton where CartonID = @CartonID
union all
select Box4, 4 from Carton where CartonID = @CartonID
union all
select Box5, 5 from Carton where CartonID = @CartonID
union all
select Box6, 6 from Carton where CartonID = @CartonID
union all
select Box7, 7 from Carton where CartonID = @CartonID
union all
select Box8, 8 from Carton where CartonID = @CartonID
union all
select Box9, 9 from Carton where CartonID = @CartonID
union all
select Box10, 10 from Carton where CartonID = @CartonID
union all
select Box11, 11 from Carton where CartonID = @CartonID
union all
select Box12, 12 from Carton where CartonID = @CartonID
) T2
on T1.BoxValue < T2.BoxValue or (T1.BoxValue=T2.BoxValue and T1.Num <= T2.Num)
group by T1.BoxValue, T1.Num
) T
) T
where CartonID = @CartonID

END

Subject: My Solution: Part 3
Posted by: yellowfog (view profile)
Posted on: Wednesday, April 07, 2010 at 5:57 PM
Message: -- THE THREE REQUIRED PROCS

-- takes the number of eggs in a basket and creates appropriate cartons
-- doesn't then zero the number of eggs in the basket, though it could
CREATE PROCEDURE [dbo].[SplitBasket]
@basketID int
AS
BEGIN

begin transaction

-- get the correct number of eggs for the basket
declare @i int
select @i = NumberOfEggs from Basket where BasketID = @BasketID

-- test for a positive number
if @i is null or @i<=0
begin
rollback transaction
return -1
end

-- insert the eggs into cartons
insert into Carton (Box1, Box2, Box3, Box4, Box5, Box6, Box7, Box8, Box9, Box10, Box11, Box12)
select
case when ((N.Num-1)*12)<@i then @basketID else 0 end as Box1,
case when ((N.Num-1)*12)+1<@i then @basketID else 0 end as Box2,
case when ((N.Num-1)*12)+2<@i then @basketID else 0 end as Box3,
case when ((N.Num-1)*12)+3<@i then @basketID else 0 end as Box4,
case when ((N.Num-1)*12)+4<@i then @basketID else 0 end as Box5,
case when ((N.Num-1)*12)+5<@i then @basketID else 0 end as Box6,
case when ((N.Num-1)*12)+6<@i then @basketID else 0 end as Box7,
case when ((N.Num-1)*12)+7<@i then @basketID else 0 end as Box8,
case when ((N.Num-1)*12)+8<@i then @basketID else 0 end as Box9,
case when ((N.Num-1)*12)+9<@i then @basketID else 0 end as Box10,
case when ((N.Num-1)*12)+10<@i then @basketID else 0 end as Box11,
case when ((N.Num-1)*12)+11<@i then @basketID else 0 end as Box12
from
Numbers N
where N.Num<=ceiling((@i+11)/12)

commit transaction

END


-- moves a certain number of eggs from a carton to another
-- assuming that this makes sense
CREATE PROCEDURE [dbo].[ConsolidateCartons]
@fromCartonID int,
@toCartonID int,
@eggCount int
AS
BEGIN
SET NOCOUNT ON;

-- check that we have a positive egg count
if @eggCount <= 0
begin
return -1
end

begin transaction

declare @numTo int
select @numTo = dbo.NumberOfEggsInCarton(@toCartonID)

-- check that the carton exists
if @numTo<0
begin
rollback transaction
return -1
end

-- check that the total number of eggs will not exceed 12
if (@numTo + @eggCount)>12
begin
rollback transaction
return -1
end

declare @numFrom int
select @numFrom = dbo.NumberOfEggsInCarton(@fromCartonID)

-- check that the carton exists
if @numFrom<0
begin
rollback transaction
return -1
end

-- check that the carton has enough eggs to give
if @numFrom < @eggCount
begin
rollback transaction
return -1
end

-- rearrange the cartons
exec dbo.SortCarton @fromCartonID
exec dbo.SortCarton @toCartonID

-- move eggs from the front of the @fromCarton to the back of the @toCarton
update Carton set
Box12 = case when @eggCount>0 then T.Box1 else Carton.Box12 end,
Box11 = case when @eggCount>1 then T.Box2 else Carton.Box11 end,
Box10 = case when @eggCount>2 then T.Box3 else Carton.Box10 end,
Box9 = case when @eggCount>3 then T.Box4 else Carton.Box9 end,
Box8 = case when @eggCount>4 then T.Box5 else Carton.Box8 end,
Box7 = case when @eggCount>5 then T.Box6 else Carton.Box7 end,
Box6 = case when @eggCount>6 then T.Box7 else Carton.Box6 end,
Box5 = case when @eggCount>7 then T.Box8 else Carton.Box5 end,
Box4 = case when @eggCount>8 then T.Box9 else Carton.Box4 end,
Box3 = case when @eggCount>9 then T.Box10 else Carton.Box3 end,
Box2 = case when @eggCount>10 then T.Box11 else Carton.Box2 end,
Box1 = case when @eggCount>11 then T.Box12 else Carton.Box1 end
from
(
select * from Carton where CartonID = @fromCartonID
) T
where Carton.CartonID = @toCartonID

update Carton set
Box1 = case when @eggCount>0 then 0 else Box1 end,
Box2 = case when @eggCount>1 then 0 else Box2 end,
Box3 = case when @eggCount>2 then 0 else Box3 end,
Box4 = case when @eggCount>3 then 0 else Box4 end,
Box5 = case when @eggCount>4 then 0 else Box5 end,
Box6 = case when @eggCount>5 then 0 else Box6 end,
Box7 = case when @eggCount>6 then 0 else Box7 end,
Box8 = case when @eggCount>7 then 0 else Box8 end,
Box9 = case when @eggCount>8 then 0 else Box9 end,
Box10 = case when @eggCount>9 then 0 else Box10 end,
Box11 = case when @eggCount>10 then 0 else Box11 end,
Box12 = case when @eggCount>11 then 0 else Box12 end
from Carton where CartonID = @fromCartonID

commit tran
END


-- takes a number of eggs from one carton and places them
-- in a new carton
CREATE PROCEDURE [dbo].[SplitCarton]
@fromCartonID int,
@eggCount int
AS
BEGIN

-- check that we have a positive egg count
if @eggCount <= 0
begin
return -1
end

begin transaction

declare @numFrom int
select @numFrom = dbo.NumberOfEggsInCarton(@fromCartonID)

-- check that the carton exists
if @numFrom<0
begin
rollback transaction
return -1
end

-- check that the carton has enough eggs to give
if @numFrom < @eggCount
begin
rollback transaction
return -1
end

-- rearrange the carton
exec dbo.SortCarton @fromCartonID

-- create a new carton with our entries
insert into Carton (Box1, Box2, Box3, Box4, Box5, Box6, Box7, Box8, Box9, Box10, Box11, Box12)
select
case when @eggCount>0 then Box1 else 0 end,
case when @eggCount>1 then Box2 else 0 end,
case when @eggCount>2 then Box3 else 0 end,
case when @eggCount>3 then Box4 else 0 end,
case when @eggCount>4 then Box5 else 0 end,
case when @eggCount>5 then Box6 else 0 end,
case when @eggCount>6 then Box7 else 0 end,
case when @eggCount>7 then Box8 else 0 end,
case when @eggCount>8 then Box9 else 0 end,
case when @eggCount>9 then Box10 else 0 end,
case when @eggCount>10 then Box11 else 0 end,
case when @eggCount>11 then Box12 else 0 end
from Carton where CartonID = @fromCartonID


-- blank eggs from the front of @fromCarton
update Carton set
Box1 = case when @eggCount>0 then 0 else Box1 end,
Box2 = case when @eggCount>1 then 0 else Box2 end,
Box3 = case when @eggCount>2 then 0 else Box3 end,
Box4 = case when @eggCount>3 then 0 else Box4 end,
Box5 = case when @eggCount>4 then 0 else Box5 end,
Box6 = case when @eggCount>5 then 0 else Box6 end,
Box7 = case when @eggCount>6 then 0 else Box7 end,
Box8 = case when @eggCount>7 then 0 else Box8 end,
Box9 = case when @eggCount>8 then 0 else Box9 end,
Box10 = case when @eggCount>9 then 0 else Box10 end,
Box11 = case when @eggCount>10 then 0 else Box11 end,
Box12 = case when @eggCount>11 then 0 else Box12 end
from Carton where CartonID = @fromCartonID

commit tran

END


Subject: Way to go yellowfog
Posted by: puzsol (view profile)
Posted on: Thursday, April 08, 2010 at 5:47 AM
Message: Let's just say, I'm really sorry for all those who came to the discussion late!

Subject: addendum
Posted by: yellowfog (view profile)
Posted on: Friday, April 09, 2010 at 3:00 AM
Message: Thanks Puz.

Oh, I also forgot the following points in my posts: I have a zero-keyed entry in the basket table to act as the foreign key reference for 'empty' boxes, and I make use of the standard table of consecutive integers which I called Numbers.

Subject: My entry
Posted by: deangc (view profile)
Posted on: Monday, April 12, 2010 at 12:07 AM
Message: I have no idea whether the formatting will survive... guess I'll see. :)

In the interests of blog comment space, I did not put in any of the stuff I would normally put in: TRY CATCH, transaction handling, parameter checking, etc. The statements in the procedures will not return errors, but they won't insert bad data either. If you enter parameters that won't work (trying to consolidate 2 cartons with more than 12 eggs between them for example) the procedure just won't do anything.

-----------
CREATE TABLE Locations
(LocationID VARCHAR(20) NOT NULL,
LocationDescription VARCHAR(100) NOT NULL,
CONSTRAINT PK_Locations PRIMARY KEY (LocationID))

CREATE TABLE Baskets
(BasketID VARCHAR(20) NOT NULL,
BasketEggCount INT NOT NULL,
CONSTRAINT PK_Baskets PRIMARY KEY (BasketID))

CREATE TABLE Cartons(
BasketID VARCHAR(20) NOT NULL,
CartonSequenceNo INT NOT NULL,
CartonEggCount TINYINT NOT NULL DEFAULT (0),
CONSTRAINT PK_Cartons PRIMARY KEY (BasketID, CartonSequenceNo),
CONSTRAINT FK_CartonsBaskets FOREIGN KEY (BasketID) REFERENCES dbo.Baskets(BasketID),
CONSTRAINT CK_CartonEggCount CHECK (CartonEggCount BETWEEN 0 AND 12)
)

CREATE TABLE BasketLocations(
LocationID VARCHAR(20) NOT NULL,
BasketID VARCHAR(20) NOT NULL,
CONSTRAINT PK_BasketLocations PRIMARY KEY (LocationID, BasketID),
CONSTRAINT FK_BasketLocations_Locations FOREIGN KEY (LocationID) REFERENCES Locations(LocationID),
CONSTRAINT FK_BasketLocations_Baskets FOREIGN KEY (BasketID) REFERENCES Baskets(BasketID)
)

CREATE TABLE Integers(I INT NOT NULL,
CONSTRAINT PK_Integers PRIMARY KEY (I))

INSERT Integers(I)
SELECT (COALESCE(MAX(I), 0) + 1)
FROM Integers
GO 500


INSERT Locations (LocationID, LocationDescription)
SELECT LOC.LocationID, LOC.LocationDescription
FROM ( VALUES ('BARN1', 'Barn #1'),
('BARN2', 'Barn #2'),
('PLANT1', 'Processing Plant #1')
) AS LOC(LocationID, LocationDescription)
WHERE NOT EXISTS(
SELECT *
FROM Locations
WHERE LocationID = LOC.LocationID)

INSERT Baskets(BasketID, BasketEggCount)
SELECT BAS.BasketID, BAS.BasketEggCount
FROM (VALUES ('BASKET1', 0),
('BASKET2', 11),
('BASKET3', 25),
('BASKET4', 144),
('BASKET5', 0)
) AS BAS (BasketID, BasketEggCount)
WHERE NOT EXISTS (
SELECT *
FROM Baskets
WHERE BasketID = BAS.BasketID)

INSERT BasketLocations(BasketID, LocationID)
SELECT B.BasketID,
L.LocationID
FROM Baskets B
CROSS JOIN Locations L
WHERE B.BasketID <> 'BASKET5'
AND L.LocationID = 'PLANT1'
AND NOT EXISTS (
SELECT *
FROM BasketLocations
WHERE BasketID = B.BasketID
AND LocationID = L.LocationID )

INSERT BasketLocations(BasketID, LocationID)
SELECT B.BasketID,
L.LocationID
FROM Baskets B
CROSS JOIN Locations L
WHERE B.BasketID = 'BASKET5'
AND L.LocationID = 'BARN1'
AND NOT EXISTS (
SELECT *
FROM BasketLocations
WHERE BasketID = B.BasketID
AND LocationID = L.LocationID )
GO

CREATE PROCEDURE dbo.SplitBaskets(
@LocationID VARCHAR(20)
)
AS
BEGIN
-- Splits all Baskets at @Location into Cartons for any Basket
-- where the BasketEggCount > 0

DECLARE @CartonSize TINYINT

SET @CartonSize = 12

INSERT Cartons(BasketID, CartonSequenceNo, CartonEggCount)
SELECT X.BasketID,
X.SequenceNo,
CASE WHEN X.CumEggs <= X.BasketEggCount THEN @CartonSize
ELSE X.Remainder END
AS CartonEggCount
FROM (-- produces a set of rows with CartonEggCount = @CartonSize
-- the last row gets CartonEggCount = Remainder
-- sequence numbers are generated
-- doesn't insert rows with BasketEggCount = 0
SELECT ROW_NUMBER() OVER (PARTITION BY B.BasketID ORDER BY B.BasketID)
AS SequenceNo,
B.BasketEggCount,
I.i AS CumEggs,
B.basketeggcount % @CartonSize
AS Remainder,
B.BasketID
FROM Baskets B
JOIN BasketLocations L
ON B.BasketID = L.BasketID
CROSS JOIN Integers I
WHERE I.I <= (B.BasketEggCount + @CartonSize)
AND I.I % @CartonSize = 0
AND L.LocationID = @LocationID
AND B.BasketEggCount > 0
AND I.I - B.BasketEggCount <> @CartonSize
)X -- X, get it? Eggs/X?
WHERE NOT EXISTS(
SELECT *
FROM Cartons
WHERE BasketID = X.BasketID
AND SequenceNo = X.SequenceNo)
END
GO

CREATE PROCEDURE dbo.SplitCarton(
@BasketID VARCHAR(20),
@CartonSequenceNo INT,
@EggsToMove INT )

AS
BEGIN
-- Takes a Carton identified by @BasketID and @CartonSequenceNo
-- and removes @EggsToMove eggs from it.
-- creates a new Carton with @EggsToMove eggs in it.

MERGE INTO Cartons C
USING ( SELECT C.BasketID,
(SELECT MAX(CartonSequenceNo) + 1
FROM Cartons
WHERE BasketID = C.BasketID) AS CartonSequenceNo,
@EggsToMove AS CartonEggCount
FROM Cartons C
WHERE C.BasketID = @BasketID
AND C.CartonSequenceNo = @CartonSequenceNo
AND C.CartonEggCount > @EggsToMove
AND @EggsToMove BETWEEN 1 AND (C.CartonEggCount - 1)
UNION ALL
SELECT BasketID,
CartonSequenceNo,
CartonEggCount - @EggsToMove AS NewCartonEggCount
FROM Cartons
WHERE BasketID = @BasketID
AND CartonSequenceNo = @CartonSequenceNo
AND CartonEggCount > @EggsToMove
AND @EggsToMove BETWEEN 1 AND (CartonEggCount - 1)
) NEW
ON C.BasketID = NEW.BasketID
AND C.CartonSequenceNo = NEW.CartonSequenceNo
WHEN MATCHED THEN UPDATE
SET C.CartonEggCount = NEW.CartonEggCount
WHEN NOT MATCHED BY TARGET THEN INSERT --(BasketID, CartonSequenceNo, CartonEggCount)
VALUES(NEW.BasketID, NEW.CartonSequenceNo, NEW.CartonEggCount)
;
END
GO

CREATE PROCEDURE dbo.ConsolidateCartons(
@BasketIDFrom VARCHAR(20),
@CartonSequenceNoFrom INT,
@BasketIDTo VARCHAR(20),
@CartonSequenceNoTo INT)
AS
BEGIN
-- Takes all the eggs from Carton identified by @BasketIDFrom and @CartonSequenceNoFrom
-- and adds them to the count for Carton identified by @BasketIDTo and @CartonSequenceNoTo
-- then deletes the empty Carton

-- If this were production code, there would be parameter checking, TRY CATCH blocks, transactions, etc.

DECLARE @CartonSize INT
SET @CartonSize = 12

MERGE INTO Cartons C
USING( -- builds a set of rows that contain what the database
-- should look like after the move
-- both parts of the query check that the sum of the two
-- egg counts do not exceed the maximum carton size - if they
-- do, this is an invalid consolidation and no rows will
-- be updated or deleted
SELECT CTo.BasketID,
CTo.CartonSequenceNo,
CTo.CartonEggCount + CFrom.CartonEggCount AS CartonEggCount
FROM Cartons CFrom
JOIN Cartons CTo
ON CFrom.BasketID = @BasketIDFrom
AND CFrom.CartonSequenceNo = @CartonSequenceNoFrom
AND CTo.BasketID = @BasketIDTo
AND CTo.CartonSequenceNo = @CartonSequenceNoTo
WHERE CTo.CartonEggCount + CFrom.CartonEggCount <= @CartonSize
UNION ALL
-- because the rule is that all eggs must be moved from the FROM
-- Carton, this set contains 0 as the CartonEggCount
SELECT CFrom.BasketID,
CFrom.CartonSequenceNo,
0 AS CartonEggCount
FROM Cartons CFrom
JOIN Cartons CTo
ON CFrom.BasketID = @BasketIDFrom
AND CFrom.CartonSequenceNo = @CartonSequenceNoFrom
AND CTo.CartonSequenceNo = @CartonSequenceNoTo
WHERE CTo.CartonEggCount + CFrom.CartonEggCount <= @CartonSize
) NEW
ON C.BasketID = NEW.BasketID
AND C.CartonSequenceNo = NEW.CartonSequenceNo
-- if NEW.CartonEggCount > 0, this must be the Carton that the eggs are moving to
WHEN MATCHED AND NEW.CartonEggCount > 0 THEN UPDATE
SET C.CartonEggCount = NEW.CartonEggCount
-- if NEW.CartonEggCount = 0, this must be the Carton that is being removed
WHEN MATCHED AND NEW.CartonEggCount = 0 THEN DELETE;

END
GO

Subject: AAAGH
Posted by: deangc (view profile)
Posted on: Monday, April 12, 2010 at 12:08 AM
Message: It's only 10:07 here!

Subject: Question for Deangc's entry
Posted by: puzsol (view profile)
Posted on: Friday, April 16, 2010 at 12:01 AM
Message: @deangc

In your solution... I am curious, I can't test it as I don't have 2008, but if I understand it correctly, there may be an error when you merge cartons from two different baskets then run split baskets again...

I think, the merge baskets function deletes the carton record once the amount of eggs is 0?
If so, then I think that the split basket function will not find the matching (basket,max carton number) pair in the not exists critera for the carton and re-create it?

Subject: One last update
Posted by: puzsol (view profile)
Posted on: Friday, April 16, 2010 at 12:06 AM
Message: I was thinking, in the not so silly solution, if allowing for different carton sizes, then when performing the split baskets function, it should only 'take' eggs from baskets with the same carton size (which would be a simple change to the where clause)... that way, you don't take 35 eggs from an almost full 36-egg carton to fill 2.916 12-egg cartons - which would be a waste of time. And you probably pack the different sizes in different locations anyway.

Subject: For Puzsol
Posted by: deangc (view profile)
Posted on: Wednesday, April 21, 2010 at 8:13 AM
Message: Yes, that's right. I wasn't sure what Joe meant with the 'empty carton problem', so I opted not to refill empty cartons, just allow the empty carton number to cease to exist. This might seem odd, but I worked in a company on a system that does exactly that.


Subject: re: one last update
Posted by: deangc (view profile)
Posted on: Wednesday, April 21, 2010 at 8:20 AM
Message: I didn't have time to write production-ready code. Your suggestion is correct, I think, but we're getting well beyond the original requirements, and I probably shouldn't have been such a smartass anyway.


Subject: Joe, your population of the consolidation table is/was wrong
Posted by: puzsol (view profile)
Posted on: Monday, May 17, 2010 at 7:29 AM
Message: Your query as written....

SELECT S1.seq-1 as pre_dest_egg_cnt
, S2.seq-1 as pre_src_egg_cnt
, S3.seq-1 as post_dest_egg_cnt
, S4.seq-1 as post_src_egg_cnt
FROM Series AS S1, Series AS S2, Series AS S3, Series AS S4
WHERE S1.seq <= 13
AND S2.seq <= 13
AND S3.seq <= 13
AND S4.seq <= 13
AND (S1.seq + S2.seq) = (S3.seq + S4.seq)

returns 1469 rows... far too many... in the style of query you wrote I had to add a few more constraints:

SELECT S1.seq-1 as pre_dest_egg_cnt
, S2.seq-1 as pre_src_egg_cnt
, S3.seq-1 as post_dest_egg_cnt
, S4.seq-1 as post_src_egg_cnt
FROM Series AS S1, Series AS S2, Series AS S3, Series AS S4
WHERE S1.seq <= 13
AND S2.seq <= 13
AND S3.seq <= 13
AND S4.seq <= 13
AND (S1.seq + S2.seq) = (S3.seq + S4.seq)
AND (S4.seq <= S2.seq)
AND (S3.seq >= S1.seq)
AND (S3.seq >= S4.seq)
AND (S3.seq = 13 or S3.seq + S4.seq < 14)
AND (S4.seq = 1 or S3.seq + S4.seq > 13);


Or you could just have written:

SELECT S1.seq-1 as pre_dest_egg_cnt
, S2.seq-1 as pre_src_egg_cnt
, case when S1.seq + S2.seq > 14 then 12 else S1.seq + S2.seq - 2 end as post_dest_egg_cnt
, case when S1.seq + S2.seq < 14 then 0 else S1.seq + S2.seq - 14 end as post_src_egg_cnt
FROM Series AS S1, Series AS S2
WHERE S1.seq <= 13
AND S2.seq <= 13
order by S2.seq desc, S1.seq desc;

Thanks

Subject: corrected consolidations data
Posted by: Pete Cox (view profile)
Posted on: Monday, May 17, 2010 at 7:35 AM
Message: The following seems to work well enough as well

With all_possible_consolidations
(
pre_dest_egg_cnt,
pre_src_egg_cnt,
post_dest_egg_cnt,
post_src_egg_cnt
)
as
(SELECT
S1.seq-1 as pre_dest_egg_cnt,
S2.seq-1 as pre_src_egg_cnt,
S3.seq-1 as post_dest_egg_cnt,
S4.seq-1 as post_src_egg_cnt
FROM
Series AS S1,
Series AS S2,
Series AS S3,
Series AS S4
WHERE
S1.seq <= 13
AND S2.seq <= 13
AND S3.seq <= 13
AND S4.seq <= 13
AND (S1.seq + S2.seq) = (S3.seq + S4.seq)
)
select
pre_dest_egg_cnt,
pre_src_egg_cnt,
max(post_dest_egg_cnt) as post_dest_egg_cnt,
min(post_src_egg_cnt) as post_src_egg_cnt
from
all_possible_consolidations
group by
pre_dest_egg_cnt,
pre_src_egg_cnt
order by
pre_dest_egg_cnt,
pre_src_egg_cnt


Thanks for a really interesting discussion

 










Phil Factor
Automated Script-generation with Powershell and SMO
 In the first of a series of articles on automating the process of building, modifying and copying SQL Server... Read more...



 View the blog
Converting String Data to XML and XML to String Data
 We all appreciate that, in general, XML documents or fragments are held in strings as text markup. In... Read more...

SQL Source Control: The Development Story
 Often, there is a huge difference between software being easy to use, and easy to develop. When your... Read more...

How to Import Data from HTML pages
 It turns out that there are plenty of ways to get data into SQL Server from websites, whether the data... Read more...

SQL Scripts Manager: An Appreciation
 SQL Scripts Manager is Simple-Talk's present to its readers. William Brewer was an enthusiastic... Read more...

Hosted Team Foundation Server 2010 Review
 Team Foundation Server (TFS) has expanded its remit to support the whole software development process,... Read more...

Beginning SQL Server 2005 Reporting Services Part 1
 Steve Joubert begins an in-depth tour of SQL Server 2005 Reporting Services with a step-by-step guide... Read more...

Ten Common Database Design Mistakes
 Database design and implementation is the cornerstone of any data centric project (read 99.9% of... Read more...

Reading and Writing Files in SQL Server using T-SQL
 SQL Server provides several "standard" techniques by which to read and write to files but, just... Read more...

Beginning SQL Server 2005 Reporting Services Part 2
 Continuing his in-depth tour of SQL Server 2005 Reporting Services, Steve Joubert demonstrates the most... Read more...

Creating CSV Files Using BCP and Stored Procedures
 Nigel Rivett demonstrates some core techniques for extracting SQL Server data into CSV files, focussing... Read more...

Over 400,000 Microsoft professionals subscribe to the Simple-Talk technical journal. Join today, it's fast, simple, free and secure.

Join Simple Talk