Replace cursors with queries - sql

Let's say I have a booking covering 6 hours and 3 discounts covering 2 hours each. I want to split my booking into 3 parts so I can allocate 2 hours per discount.
It would return something like this:
BookingId 1 | DiscountId 1 | Qty 2
BookingId 1 | DiscountId 2 | Qty 2
BookingId 1 | DiscountId 3 | Qty 2
I would then insert those records this into another table.
I'm using an heavily optimized query to determine the number of hours available for each discount. However, I can't find a "good" way to allocate my booking to each discount without using a cursor.
(...)
WHILE ##FETCH_STATUS = 0
BEGIN
IF #RequiredQty = 0
RETURN
IF #RequiredQty <= #AvailableQty
BEGIN
INSERT INTO discount.Usage (DiscountId, BookingId, Quantity)
VALUES (#DiscountId, #BookingId, #RequiredQty)
SET #RequiredQty = 0
END
IF #RequiredQty > #AvailableQty
BEGIN
INSERT INTO discount.Usage (DiscountId, BookingId, Quantity)
VALUES (#DiscountId, #BookingId, #AvailableQty)
SET #RequiredQty -= #AvailableQty
END
FETCH NEXT FROM ecursor INTO #DiscountId, #AvailableQty
END
DEALLOCATE ecursor
I tried building the corresponding query but I can't select and assign variables at the same time. Using a cursor is not really a problem (besides some potential performance issues) but I was just curious to see if with the newest SQL Server we can convert our old cursors to something better?
Thanks,
Seb

You can useCTE RECURSIVE to make a Table.
like this.
DECLARE #BookingId INT = 1;
DECLARE #RequiredQty INT = 2;
DECLARE #Hours INT = 7;
CREATE TABLE #T
(
BookingId INT,
DiscountId INT,
Quantity INT
)
;WITH CTE([Count],[Quantity],Rk) AS
(
SELECT
CASE
WHEN [HOURS] - #RequiredQty > #RequiredQty THEN #RequiredQty
ELSE [HOURS] - #RequiredQty
END ,
T.HOURS,1
FROM
(
SELECT #Hours [HOURS]
) AS T
UNION ALL
SELECT CASE
WHEN CTE.[Quantity] - #RequiredQty > #RequiredQty THEN #RequiredQty
ELSE CTE.[Quantity] - #RequiredQty
END AS [Count],
CTE.[Quantity] - #RequiredQty,
RK + 1
FROM CTE
WHERE CTE.[Quantity] - #RequiredQty > 0
)
INSERT INTO #T(BookingId,DiscountId,Quantity)
SELECT #BookingId,Rk,[Count] FROM CTE
option (maxrecursion 0)
select * from #T
SQLDEMO

This is another approach, but don't know if this code has better performance than cursor.
DECLARE #DiscountStocks TABLE (Id INT IDENTITY(1,1), DiscountId INT, LastQty INT)
INSERT INTO #DiscountStocks (DiscountId, LastQty) VALUES (1, 5)
INSERT INTO #DiscountStocks (DiscountId, LastQty) VALUES (2, 2)
INSERT INTO #DiscountStocks (DiscountId, LastQty) VALUES (3, 1)
DECLARE #DiscountBookings TABLE (Id INT IDENTITY(1,1), DiscountId INT, BookingId INT, Qty INT)
DECLARE #BookingDiscount TABLE (Id INT IDENTITY(1,1), BookingId INT, DiscountId INT, Qty INT)
INSERT INTO #BookingDiscount (BookingId, DiscountId, Qty) VALUES (1, 1, 4)
INSERT INTO #BookingDiscount (BookingId, DiscountId, Qty) VALUES (1, 2, 2)
INSERT INTO #BookingDiscount (BookingId, DiscountId, Qty) VALUES (1, 3, 1)
INSERT INTO #BookingDiscount (BookingId, DiscountId, Qty) VALUES (2, 1, 1)
INSERT INTO #BookingDiscount (BookingId, DiscountId, Qty) VALUES (2, 2, 2)
SELECT BD.Id AS BDId, DS.Id AS DSId, DS.LastQty, BD.Qty
, DS.LastQty - (SELECT SUM(Qty) FROM #BookingDiscount WHERE Id <= BD.Id AND DiscountId = BD.DiscountId) AS QtyAfterSubstract
INTO #LastDiscountStock
FROM #DiscountStocks DS
INNER JOIN #BookingDiscount BD ON DS.DiscountId = BD.DiscountId
ORDER BY BD.Id, DS.Id
INSERT INTO #DiscountBookings (DiscountId, BookingId, Qty)
SELECT DSId, BDId, Qty
FROM #LastDiscountStock
WHERE QtyAfterSubstract >= 0
DROP TABLE #LastDiscountStock
SELECT * FROM #DiscountBookings

Related

is there way to apply row updates without looping

i would like to pay for different invoices using different credits i have.
drop table #InvoicesWithBalances
drop table #AvailableCredits
create table #InvoicesWithBalances
(
InvoiceKey decimal(18,0) not null,
APBalance decimal(18,6) null,
BalanceAfterCreditApplied decimal(18,6) null,
)
create table #AvailableCredits
(
credit_id int identity(1,1),
StartingBalance decimal(18,6) null,
CurrentBalance decimal(18,6) null,
)
insert into #InvoicesWithBalances values (5452, 13744.080000, 13744.080000)
insert into #InvoicesWithBalances values (7056, 13744.080000, 13744.080000)
insert into #InvoicesWithBalances values (7438, 500.000000, 500.000000 )
insert into #AvailableCredits values ( -13744.080000, -13744.080000)
insert into #AvailableCredits values ( -13700.080000, -13700.080000)
insert into #AvailableCredits values ( -500.000000, -500.000000)
insert into #AvailableCredits values ( -500.000000, -500.000000)
select * from #InvoicesWithBalances
select * from #AvailableCredits
If I was doing a looping solution I would take the largest credit and start applying it to the invoices in order of largest to smallest until the balance of the credit is zero, then I would move on to the next to the next credit until I had no credits and no invoices left.
In the example below the first 2 credits should be fully used. The third credit should be partially used and the last credit should go untouched
Any advice?
I have tried to simulate your example here:
create table InvoicesWithBalances
(
InvoiceKey int not null,
APBalance int null,
BalanceAfterCreditApplied int null,
);
create table AvailableCredits
(
credit_id int identity(1,1),
StartingBalance int null,
CurrentBalance int null,
);
insert into InvoicesWithBalances values (5452, 13744, 13744);
insert into InvoicesWithBalances values (7056, 13744, 13744);
insert into InvoicesWithBalances values (7438, 500, 500);
insert into AvailableCredits values ( -13744, -13744);
insert into AvailableCredits values ( -13700, -13700);
insert into AvailableCredits values ( -500, -500);
insert into AvailableCredits values ( -500, -500);
create table #invoice (invoice_row_num int, InvoiceKey int, APBalance int, BalanceAfterCreditApplied int);
insert into #invoice
select ROW_NUMBER() OVER (ORDER BY APBalance desc) as row_num, InvoiceKey, APBalance, BalanceAfterCreditApplied FROM InvoicesWithBalances;
create table #credits (credit_row_num int, StartingBalance int, CurrentBalance int);
insert into #credits
select ROW_NUMBER() OVER (ORDER BY StartingBalance asc) as row_num, StartingBalance, CurrentBalance FROM AvailableCredits;
create table #invoice_credit_list (invoice_credit_row_num int, init_invoice int, init_credit int);
if ((select max(invoice_row_num) from #invoice) > (select max(credit_row_num) from #credits))
insert into #invoice_credit_list
select i.invoice_row_num , i.APBalance, (-isnull(c.StartingBalance,0)) from
#invoice i
left join
#credits c
on
i.invoice_row_num = c.credit_row_num;
else
insert into #invoice_credit_list
select c.credit_row_num, isnull(i.APBalance,0), (-c.StartingBalance) from
#credits c
left join
#invoice i
on
i.invoice_row_num = c.credit_row_num;
with cte as
(
select
invoice_credit_row_num,
init_invoice,
init_credit,
case when init_invoice >= init_credit then
init_invoice - init_credit
else
0
end as 'invoice_remaining',
case when init_credit >= init_invoice then
init_credit - init_invoice
else
0
end as 'credit_remaining'
from
#invoice_credit_list i
where
i.invoice_credit_row_num = 1
UNION ALL
select
i.invoice_credit_row_num,
i.init_invoice + cte.invoice_remaining as 'init_invoice',
i.init_credit + cte.credit_remaining as 'init credit',
case when (i.init_invoice + cte.invoice_remaining) >= (i.init_credit + cte.credit_remaining ) then
(i.init_invoice + cte.invoice_remaining) - (i.init_credit + cte.credit_remaining )
else
0
end as 'invoice_remaining',
case when (i.init_credit + cte.credit_remaining) >= (i.init_invoice + cte.invoice_remaining) then
(i.init_credit + cte.credit_remaining) - (i.init_invoice + cte.invoice_remaining)
else
0
end as 'credit_remaining'
from
#invoice_credit_list i
inner join
cte
ON
i.invoice_credit_row_num - 1 = cte.invoice_credit_row_num
AND
i.invoice_credit_row_num > 1
)
select * from cte;
and you can also find this simulation with output here: https://rextester.com/SJYGV76640
The 'cte' table in the simulation will give you all the details.
Eventhough, this is now done in db side, I am not sure of its performance. So, please compare and evaluate its performance.
Note:
if this is performing faster, well and good. But, at most, don't opt for loops in T-SQL.
If this is not performing better, go for loops in any other programming language like C#, VB,.. if that is possible.
If no other option works for you, go for loops in T-SQL. But, with increasing data, I am not sure how the server will react :(
Hope this helps you :)

SQL stored procedure results solution

I have a stored procedure that I inherited. The goal is for the data in the temp table: #multi_nominees_uf to be joined with the data in #temp_reviewers_UF and assign #temp_reviewers_UF.uf_rev_id to #multi_nominees_uf.application_number where the corresponding uf_rev_id's short_plan does not match the major associated with the appNumber's major .
It is not as simple as doing a JOIN.
The following has to be in place:
short_plan can not match the major(associated with the uf_rev_id)
count of each uf_rev_id can only be in the table a certain number of times (#RevsPerRevieweruf). Also, the uf_rev_id will be in the #temp_reviewers_uf table more than once with a different short_plan, it should only be looking at DISTINCT uf_rev_id when calculating the #RevPerRevieweruf.
The way it is written now, the counts are not consistent. One uf_rev_ID may have 122 records and another may have 50 - each distinct uf_rev_id should have the same count (or very close). I have researched and tried NTILE but I couldn't figure it out.
Any ideas of the best way to accomplish this?? Any input is appreciated.
-----Sample Data -----
CREATE TABLE #mult_nominees_uf(
appnum VARCHAR(8)
,major VARCHAR(8)
,compid INT
);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('00012345','ACT',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('10002343','BBC',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('10002777','BBC',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('10000023','DED',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('23457829','AAR',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('78954321','RRE',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('90002342','ACT',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('11156726','AAR',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('88855593','RRE',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('10000001','DED',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('20000393','ACT',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('11119999','DED',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('78927626','AAR',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('67589393','RRE',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('12453647','AAR',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('00012345','ACT',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('10002343','BBC',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('10002777','BBC',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('10000023','DED`',2);
INSERT INTO #mult_nominees_uf(appnum,major,compid) VALUES ('23457829','AAR',2);
--with this sample data the #RevsPerReviewerUF count would be 4 since A5 is listed twice and we only want distinct values used
CREATE TABLE #Temp_Reviewers_uf(
uf_rev_id VARCHAR(8)
,short_plan VARCHAR(8)
,fac_emplid INTEGER
);
INSERT INTO #Temp_Reviewers_uf(uf_rev_id,short_plan,fac_emplid) VALUES ('A1','ACT',00000012);
INSERT INTO #Temp_Reviewers_uf(uf_rev_id,short_plan,fac_emplid) VALUES ('A2','BBC',00000145);
INSERT INTO #Temp_Reviewers_uf(uf_rev_id,short_plan,fac_emplid) VALUES ('A3','DED',10002934);
INSERT INTO #Temp_Reviewers_uf(uf_rev_id,short_plan,fac_emplid) VALUES ('A5','RRE',90001223);
INSERT INTO #Temp_Reviewers_uf(uf_rev_id,short_plan,fac_emplid) VALUES ('A5','ACT',90001223);
----Stored procedure -
DECLARE #Index INT
DECLARE #Num_nomineesUF INT
DECLARE #Num_reviewersUF INT
DECLARE #Num_reviewersUFDISTINCT INT
DECLARE #Num_reviews INT
DECLARE #Rev_ID nvarchar(25), #Nom_ID varchar(8), #Short_Plan varchar(8), #Major varchar(8)
DECLARE #RevsPerReviewerUF INT
SET #Num_reviews = 4
DECLARE #actualCompID int
DECLARE #UF_Flag INT
DECLARE #InsertNum int
SET #InsertNum = 1
create table #mult_nominees_UF (appNumber varchar(8), Major varchar(8), comp_id INT)
create table #TempNomineeTable (uf_rev_id varchar(8), fac_emplid varchar(9), appNumber varchar(8), Major varchar(8), short_plan varchar(8), comp_id int)
create table #Temp_Reviewers_UF (uf_rev_id varchar(8), short_plan varchar(8), fac_emplid varchar(9)) -- temp table used to hold Nom_IDs already assigned to Rev_IDs
set #actualCompID = 21
-- * * SELECT APPLICATION NUMBER & MAJOR FROM FS_RESULTS TABLE * * * * * --
select appNumber, LEFT(Major, CHARINDEX('-', Major)-1) as Major, comp_id into #Delete_nomineesUF
from FS_Results
where UF='Y'
and comp_id = #actualCompID
and nominated=1;
SET #Num_nomineesUF = ##rowcount; --GET RECORD COUNT
IF (#Num_nomineesUF > 0)
BEGIN
SET #UF_Flag = 1;
END
SET #Index = 1 ; -- reinit variable
WHILE #Index <= 4 BEGIN
if (#UF_Flag > 0)
BEGIN
INSERT into #mult_nominees_uf
select * from #Delete_nomineesUF
END
-- Create temp table for UF Reviewers
select uf_rev_id, short_plan, fac_emplid into #temp_reviewers_UF
from ReviewersID_ShortPlan
where uf_rev_id like 'UF%'
and competition_id = #actualCompID
SET #Num_reviewersUF = ##rowcount
SELECT DISTINCT UF_REV_ID FROM ReviewersID_ShortPlan WHERE UF_REV_ID like 'UF%' AND competition_id = #actualCompID
SET #Num_reviewersUFDistinct = ##rowcount
SET #RevsPerReviewerUF = (#Num_nomineesUF * #Num_reviews) / nullif(#Num_reviewersUFDistinct,0)
WITH Match_NomineesWithReviewers AS(
SELECT DISTINCT
appNumber,
RTRIM(Major) AS Major,
COUNT(1) as rowcnt,
comp_id
FROM #mult_nominees_uf
GROUP BY appNumber,
RTRIM(Major),
comp_id
)
, rownum_matches AS (
SELECT m.appNumber,
m.Major,
t.short_plan,
t.uf_rev_id,
t.fac_emplid,
m.rowcnt,
m.comp_id,
ROW_NUMBER() OVER (PARTITION BY m.appNumber order by newid()) AS rownum
FROM Match_NomineesWithReviewers m
JOIN #temp_reviewers_UF t ON t.short_plan != m.major
GROUP BY m.appNumber, m.Major, t.short_plan,
t.uf_rev_id, t.fac_emplid, m.rowcnt, m.comp_id
HAVING COUNT(t.uf_rev_id) <= #RevsPerRevieweruf
)
INSERT INTO #TempNomineeTable
SELECT uf_rev_id, fac_emplid, appNumber, Major, short_plan, null, 0, null, comp_id FROM rownum_matches rm
WHERE rownum <= rowcnt
group by uf_rev_id, fac_emplid, appNumber, Major, short_plan, comp_id
HAVING COUNT(uf_rev_id) <= #RevsPerRevieweruf

Identifying repeated fields in SQL query

I have an SQL query that returns a column like this:
foo
-----------
1200
1200
1201
1200
1200
1202
1202
1202
It has already been ordered in a specific way, and I would like to perform another query on this result set to ID the repeated data like this:
foo ID
---- ----
1200 1
1200 1
1201 2
1200 3
1200 3
1202 4
1202 4
1202 4
It's important that the second group of 1200 is identified as separate from the first. Every variation of OVER/PARTITION seems to want to lump both groups together. Is there a way to window the partition to only these repeated groups?
Edit:
This is for Microsoft SQL Server 2012
Not sure this will be the fastest results...
select main.num, main.id from
(select x.num,row_number()
over (order by (select 0)) as id
from (select distinct num from num) x) main
join
(select num, row_number() over(order by (select 0)) as ordering
from num) x2 on
x2.num=main.num
order by x2.ordering
Assuming the table "num" has a column "num" that contains your data, in the order-- of course num could be made into a view or a "with" for your original query.
Please see the following sqlfiddle
Here is one way to do this without a CURSOR
-- Create a temporay table
DECLARE #table TABLE
(
SeqID INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
foo INT,
id int null
)
DECLARE #i INT
DECLARE #j INT
DECLARE #k INT
declare #tFoo INT
declare #oldFoo INT
SET #k = 0
set #oldFoo = 0
-- Insert data into the temporary table
INSERT INTO #table(foo)
SELECT 1200
INSERT INTO #table(foo)
SELECT 1200
INSERT INTO #table(foo)
SELECT 1201
INSERT INTO #table(foo)
SELECT 1200
INSERT INTO #table(foo)
SELECT 1200
INSERT INTO #table(foo)
SELECT 1202
INSERT INTO #table(foo)
SELECT 1202
INSERT INTO #table(foo)
SELECT 1202
-- Get the max and min SeqIDs to loop through
SELECT #i = MIN(SeqID) FROM #table
SELECT #j = MAX(SeqID) FROM #table
-- Loop through the temp table using the SeqID indentity column
WHILE (#i <= #j)
BEGIN
SELECT #tFoo = foo FROM #table WHERE SeqID = #i
if #oldFoo <> #tFoo
set #k = #k + 1
update #table set id = #k where SeqID = #i
SET #oldFoo = #tFoo
-- Increment the counter
SET #i = #i + 1
END
SELECT * from #table
You can do it without using cursors but it does not look nice (at least what I came up with). So 1) I assume you have PK column which orders your main values.
Then 2) I assume you have an ID column which you want to set.
create table tbl(foo int, pk int, id int);
insert into tbl(foo, pk) values (1100, 5);
insert into tbl(foo, pk) values (1200, 10);
insert into tbl(foo, pk) values (1200, 20);
insert into tbl(foo, pk) values (1201, 30);
insert into tbl(foo, pk) values (1200, 40);
insert into tbl(foo, pk) values (1200, 50);
insert into tbl(foo, pk) values (1202, 60);
insert into tbl(foo, pk) values (1202, 70);
insert into tbl(foo, pk) values (1202, 80);
insert into tbl(foo, pk) values (1202, 90);
SQL Fiddle here: http://sqlfiddle.com/#!6/fdaaa/2
update tbl
set
ID = 1
update t
set
t.ID = m.RN2
from
tbl t
join
(
select
y1.RN as RN1, y1.PK as PK1,
y2.RN as RN2, y2.PK as PK2
FROM
(
SELECT
ROW_NUMBER() OVER(ORDER BY x.pk1 ASC) AS rn,
x.pk1 AS pk
FROM
(
SELECT t1.pk AS pk1, t2.pk AS pk2
FROM
tbl t1
LEFT JOIN tbl t2 ON
(
(t1.pk < t2.pk AND t1.foo = t2.foo)
AND
(
NOT EXISTS
(
SELECT tMid.pk FROM
tbl tMid WHERE
tMid.pk < t2.pk
AND
tMid.pk > t1.pk
)
)
)
) x WHERE x.pk2 IS NULL
) y1
left join
(
SELECT
ROW_NUMBER() OVER(ORDER BY x.pk1 ASC) AS rn,
x.pk1 AS pk
FROM
(
SELECT t1.pk AS pk1, t2.pk AS pk2
FROM
tbl t1
LEFT JOIN tbl t2 ON
(
(t1.pk < t2.pk AND t1.foo = t2.foo)
AND
(
NOT EXISTS
(
SELECT tMid.pk FROM
tbl tMid WHERE
tMid.pk < t2.pk
AND
tMid.pk > t1.pk
)
)
)
) x WHERE x.pk2 IS NULL
) y2 on y1.RN = y2.RN - 1
) m on
(
(t.pk > m.pk1 and ((m.pk2 is not null ) and (t.pk <= m.pk2)))
-- or
-- (t.pk<=m.pk1)
)
This is my solution using a cursor and a temporary table to hold the results.
DECLARE #foo INT
DECLARE #previousfoo INT = -1
DECLARE #id INT = 0
DECLARE #getid CURSOR
DECLARE #resultstable TABLE
(
primaryId INT IDENTITY(1, 1) NOT NULL PRIMARY KEY,
foo INT,
id int null
)
SET #getid = CURSOR FOR
SELECT originaltable.foo
FROM originaltable
OPEN #getid
FETCH NEXT
FROM #getid INTO #foo
WHILE ##FETCH_STATUS = 0
BEGIN
IF (#foo <> #previousfoo)
BEGIN
SET #id = #id + 1
END
INSERT INTO #resultstable VALUES (#foo, #id)
SET #previousfoo = #foo
FETCH NEXT
FROM #getid INTO #foo
END
CLOSE #getid
DEALLOCATE #getid

SQL Server 2008: Help in optimizing a query using a cursor

I need to rewrite this so it does not cause an overrun on my db. I need to rewrite it so it does not use a cursor.
Thank you.
UPDATED Here's some test data:
CREATE TABLE #TableB(SKU INT, BeginYear INT, EndYear INT, OptionA VARCHAR(10), OptionB VARCHAR(10))
INSERT INTO #TableB(SKU, BeginYear, EndYear, OptionA, OptionB)
VALUES (1, 1920, 1950, 'option1', 'option1'),
(1, 1980, 2001, 'option1', 'option2'),
(1, 1940, 1952, 'option1', 'option1'), --overlapping years
(2, 2001, 2005, 'option1', 'option1')
CREATE TABLE #TableA(SKU INT, OptionA VARCHAR(10), OptionB VARCHAR(10), Years INT)
Then you can try a recursive CTE:
;WITH CTE AS
(
SELECT DISTINCT SKU, BeginYear, EndYear, OptionA, OptionB
FROM #TableB
UNION ALL
SELECT SKU, BeginYear+1, EndYear, OptionA, OptionB
FROM CTE
WHERE BeginYear < EndYear
)
INSERT INTO #TableA(SKU, OptionA, OptionB, Years)
SELECT DISTINCT SKU, OptionA, OptionB, BeginYear
FROM CTE
OPTION(MAXRECURSION 0)
SELECT *
FROM #TableA
ORDER BY SKU, OptionA, OptionB, Years
Here there's no cartesian product.
You can easily substitute a while loop for a cursor and if you are blowing out your tempdb then do some intermediate transactions
declare #counter int
declare #temptable as tatble (RecID int Identity(1,1),
SKU Varchar(25),
OptionA Varchar(50),
OptionB Varchar(50),
CurrentYear Int,
EndYear Int)
insert into #temptable
Select SKU,OptionA, OptionB, BeginYear,EndYear
From TableB
set #counter = (select max(recid) from #temptable)
begin transaction
while #counter <> 0
Begin
(***whatever sql logic***)
if (#counter % 5000 = 0) and ##error = 0
commit Transaction
begin Transaction
set #counter = #counter -1
End
commit transaction
May be use less number of times the insert happen
Declare #temp table (
SKU Varchar(25),
OptionA Varchar(50),
OptionB Varchar(50),
[Year] Int
)
While ##FETCH_STATUS = 0
Begin
While #CurrentYear <= #EndYear
Begin
Insert Into #temp(SKU, OptionA, OptionB, Years)
Values(#SKU, #OptionA, #OptionB, #CurrentYear)
Set #CurrentYear = #CurrentYear + 1
End
Insert Into TableA
Select * From #temp
Delete From #temp
Fetch Next From #Row Into #SKU, #CurrentYear, #EndYear, #OptionA, #OptionB
End

SQL Group By Modulo of Row Count

I have the following sample data:
Id Name Quantity
1 Red 1
2 Red 3
3 Blue 1
4 Red 1
5 Yellow 3
So for this example, there are a total of 5 Red, 1 Blue, and 3 Yellow. I am looking for a way to group them by Color, but with a maximum of 2 items per group (sorting is not important). Like so:
Name QuantityInPackage
Red 2
Red 2
Red 1
Blue 1
Yellow 2
Yellow 1
Any suggestions on how to accomplish this using T-SQL on MS-SQL 2005?
I would define a table containing sequential numbers, say 1 to 1000 and join that table (unless your database supports generating these numbers in the query like Oracle using CONNECT BY):
Table num
n
1
2
3
...
I tried the following query using Oracle (should work with TSQL too):
With summed_colors As (
Select name, Sum(quantity) quantity
From colors
Group By name
)
Select
name,
Case When n*2-1 = quantity Then 1 Else 2 End quantityInPackage
From summed_colors
Join nums On ( n*2-1 <= quantity )
Order By name, quantityInPackage Desc
and it returns
Blue 1
Red 2
Red 2
Red 1
Yellow 2
Yellow 1
You need to use a numbers table to unpivot your data to make multiple rows:
DECLARE #PackageSize AS int
SET #PackageSize = 2
DECLARE #numbers AS TABLE (Number int)
INSERT INTO #numbers
VALUES (1)
INSERT INTO #numbers
VALUES (2)
INSERT INTO #numbers
VALUES (3)
INSERT INTO #numbers
VALUES (4)
INSERT INTO #numbers
VALUES (5)
INSERT INTO #numbers
VALUES (6)
INSERT INTO #numbers
VALUES (7)
INSERT INTO #numbers
VALUES (8)
INSERT INTO #numbers
VALUES (9)
INSERT INTO #numbers
VALUES (10)
DECLARE #t AS TABLE
(
Id int
,Nm varchar(6)
,Qty int
)
INSERT INTO #t
VALUES (1, 'Red', 1)
INSERT INTO #t
VALUES (2, 'Red', 3)
INSERT INTO #t
VALUES (3, 'Blue', 1)
INSERT INTO #t
VALUES (4, 'Red', 1)
INSERT INTO #t
VALUES (5, 'Yellow', 3) ;
WITH Totals
AS (
SELECT Nm
,SUM(Qty) AS TotalQty
,SUM(Qty) / #PackageSize AS NumCompletePackages
,SUM(Qty) % #PackageSize AS PartialPackage
FROM #t
GROUP BY Nm
)
SELECT Totals.Nm
,#PackageSize AS QuantityInPackage
FROM Totals
INNER JOIN #numbers AS numbers
ON numbers.Number <= Totals.NumCompletePackages
UNION ALL
SELECT Totals.Nm
,PartialPackage AS QuantityInPackage
FROM Totals
WHERE PartialPackage <> 0
It's not grouping or modulo/division that's the hard part here, it's the fact that you need to do an aggregate (sum) and then explode the data again. There aren't actually any "Red 2" rows, you have to create them somehow.
For SQL Server 2005+, I would probably use a function do the "exploding":
CREATE FUNCTION dbo.CreateBuckets
(
#Num int,
#MaxPerGroup int
)
RETURNS TABLE
AS RETURN
WITH First_CTE AS
(
SELECT CASE
WHEN #MaxPerGroup < #Num THEN #MaxPerGroup
ELSE #Num
END AS Seed
),
Sequence_CTE AS
(
SELECT Seed AS [Current], Seed AS Total
FROM First_CTE
UNION ALL
SELECT
CASE
WHEN (Total + #MaxPerGroup) > #Num THEN (#Num - Total)
ELSE #MaxPerGroup
END,
Total + #MaxPerGroup
FROM Sequence_CTE
WHERE Total < #Num
)
SELECT [Current] AS Num
FROM Sequence_CTE
Then, in the main query, group (sum) the data first and then use the bucket function:
WITH Totals AS
(
SELECT Name, SUM(Quantity) AS Total
FROM Table
GROUP BY Name
)
SELECT Name, b.Num AS QuantityInPackage
FROM Totals
CROSS APPLY dbo.CreateBuckets(Total, 2) b
This should work for any bucket size, doesn't have to be 2 (just change the parameter).
This is very crude, but it works.
CREATE TABLE #Colors
(
Id int,
Name varchar(50),
Quantity int
)
INSERT INTO #Colors VALUES (1, 'Red', 1)
INSERT INTO #Colors VALUES (2, 'Red', 3)
INSERT INTO #Colors VALUES (3, 'Blue', 1)
INSERT INTO #Colors VALUES (4, 'Red', 1)
INSERT INTO #Colors VALUES (5, 'Yellow', 3)
INSERT INTO #Colors VALUES (6, 'Green', 2)
SELECT
Name,
SUM(Quantity) AS TotalQuantity
INTO #Summed
FROM
#Colors
GROUP BY
Name
SELECT
Name,
TotalQuantity / 2 AS RecordsWithQuantity2,
TotalQuantity % 2 AS RecordsWithQuantity1
INTO #SortOfPivot
FROM
#Summed
ORDER BY
Name
DECLARE #RowCount int
SET #RowCount = (SELECT COUNT(*) FROM #SortOfPivot)
DECLARE #Name varchar(50)
DECLARE #TwosInsertCount int
DECLARE #OnesInsertCount int
CREATE TABLE #Result (Name varchar(50), Quantity int)
WHILE #RowCount > 0
BEGIN
SET #Name = (SELECT TOP 1 Name FROM #SortOfPivot)
SET #TwosInsertCount = (SELECT TOP 1 RecordsWithQuantity2 FROM #SortOfPivot)
SET #OnesInsertCount = (SELECT TOP 1 RecordsWithQuantity1 FROM #SortOfPivot)
WHILE #TwosInsertCount > 0
BEGIN
INSERT INTO #Result (Name, Quantity) VALUES (#Name, 2)
SET #TwosInsertCount = #TwosInsertCount - 1
END
WHILE #OnesInsertCount > 0
BEGIN
INSERT INTO #Result (Name, Quantity) VALUES (#Name, 1)
SET #OnesInsertCount = #OnesInsertCount - 1
END
DELETE FROM #SortOfPivot WHERE Name = #Name
SET #RowCount = (SELECT COUNT(*) FROM #SortOfPivot)
END
SELECT * FROM #Result
DROP TABLE #Colors
DROP TABLE #Result
DROP TABLE #Summed
DROP TABLE #SortOfPivot