How to turn potential loop query to set based? - sql

I have something like this currently -
declare #thisDate smalldatetime
create table #theseDates (thisDate smalldatetime)
insert into #theseDates(thisDate)
values ('11/1/2016', '5/1/2017', '9/30/2017)
create table #someData (productid int, productname varchar(32), productprice float, productsolddate smalldatetime)
declare date_cursor cursor for
select thisDate from #theseDates
open date_cursor
fetch next for date_cursor into #thisDate
while ##fetch_status = 0
begin
insert into #someData(soldid, productid, productname, productprice, productsolddate)
select soldid, productid, productname, productprice, productsolddate
from soldproducts
where productsolddate between #thisDate and getdate()
fetch next from date_cursor into #thisDate
end
close date_cursor
deallocate date_cursor
How do I change this from a cursor to a set-based solution?
NOTE: added fetch line

You don't need a cursor to solve this OP. Use CROSS Join instead
This way faster than Cursor.
See below.
FYI. the mydates table is just a random date example because I don't want type them
declare #myproducts table (prodid int, proddesc varchar(30), prodprice money, solddate date)
insert into #myproducts
values
(1,'prod 1',1,dateadd(day,-1,getdate())),
(2,'prod 2',10,dateadd(day,-10,getdate())),
(3,'prod 3',100,dateadd(day,-5,getdate())),
(1,'prod 1',1.5,dateadd(day,-6,getdate())),
(2,'prod 2',8.99,dateadd(day,-20,getdate())),
(3,'prod 3',95,dateadd(day,-15,getdate())),
(1,'prod 1',0.89,dateadd(day,-2,getdate()))
select * from #myproducts
declare #mydates table (mydate date)
/* reporting datefrom */
insert into #mydates
values (dateadd(day,-11,getdate())),(dateadd(day,-20,getdate())),(dateadd(day,-3,getdate()))
select * from #mydates
/* apply cross join to distribute the datefrom but should only be to data where solddate is between datefrom and today */
-- insert statement to dump all qualified data or not
select p.*,d.mydate [DateFrom] from #myproducts p
cross join #mydates d
where solddate between mydate and cast(getdate() as date)

Related

SQL query loops twice through each row?

When I trigger my stored procedure from a web app, it loops twice and creates two identical entries of the same row. I cannot work out why :/
The query is supposed to INSERT (re-schedule) all submitted rows. It uses a cursor to go through each row and SELECT, then INSERT, the correct data from/for each row.
Here is my SQL:
CREATE PROCEDURE [cil].[executeCIL_updateComplDate_And_ReSchedule]
#equipID INT,
#date DATE,
#ip VARCHAR(15)
AS
/* add completion date */
UPDATE cil.schedule
SET completionDate = CAST(GETUTCDATE() AS SMALLDATETIME),
complIP = #ip
WHERE schedule.id IN (SELECT schedule.id
FROM cil.schedule
LEFT JOIN cil.task ON cil.schedule.taskFK = cil.task.id
--WHERE CAST(scheduledDate AS DATE)<=CAST(GetDate() AS DATE)
WHERE CAST(scheduledDate as DATE) = #date
AND completionDate IS NULL
AND result IS NOT NULL
AND equipFK = #equipID);
/* reschedule tasks */
DECLARE #nextTaskID AS INT;
DECLARE #nextScheduledDate AS DATETIME2(6);
DECLARE #nextRotaCycle AS INT;
DECLARE db_cursor CURSOR FOR
SELECT taskFK, scheduledDate, rotaCycle
FROM cil.schedule
LEFT JOIN cil.task ON cil.schedule.taskFK = cil.task.id
WHERE completionDate = CAST(GETUTCDATE() AS SMALLDATETIME)
AND equipFK = #equipID;
OPEN db_cursor;
FETCH NEXT FROM db_cursor INTO #nextTaskID, #nextScheduledDate, #nextRotaCycle;
WHILE ##FETCH_STATUS = 0
BEGIN
--Do stuff with scalar values
INSERT INTO cil.schedule (taskFK, scheduledDate, rotaCycle)
VALUES (#nextTaskID,
DATEADD(dd, #nextRotaCycle, #nextScheduledDate),
#nextRotaCycle)
FETCH NEXT FROM db_cursor INTO #nextTaskID, #nextScheduledDate,
#nextRotaCycle;
END;
CLOSE db_cursor;
DEALLOCATE db_cursor;
GO
It seems that you have duplicates caused by a join of the query of the cursor
If you run this query externally, in SSMS, will it produce only the one row?
SELECT taskFK, scheduledDate, rotaCycle
FROM cil.schedule
LEFT JOIN cil.task ON cil.schedule.taskFK=cil.task.id
WHERE completionDate=CAST(GETUTCDATE() AS SMALLDATETIME)
AND equipFK=#equipID ;
and doubles comes from a table task.
If this table is not in use, consider to remove it

Looping through a table SQL

Okay so I have this temp table. It has all the orders which a company needs to ship out. I need to somehow loop through the table and insert the information into 3+ tables.
#TempTable Table
(
OrderID Int
)
Declare #value int = (select count(orderID) from #temptable)
Declare #i int = 1
WHILE #i < #value BEGIN
Declare #orderid= (select first(orderid) from #temptable)
INSERT INTO shipment (orderid, Price, Date, DateDue)
VALUES (#orderid, #Price, #Date, #DateDue);
Set #i += 1
Delete top(1) from #temptable
END
Is there a better way of doing this?
Adding a little more to my issue
I'm taking in 3 values from VB.Net that as an example is #Price, #Date, and #DateDue.Because of this I wasn't able just to do a select statement cause the values are mixed with this passed values.
Do it in a single query
INSERT INTO (orderid, -some other value-)
SELECT orderid, -some other value-
FROM #temptable
Looping is not efficient. Always try to avoid it. You will need two queries: one for selection and inserting and one for deletion
INSERT INTO shipment (orderid, Price, Date, DateDue)
SELECT orderid, #Price, #Date, #DateDue FROM #temptable;
DELETE FROM #temptable;
Note also that the #temptable has a limited lifetime. Therefore - depending on the situation - deleting it might not be necessary at all.

SQL Query List of months between dates

I've been searching without success for a way to list the months where my tables entries are in use.
Let's say we have a table with items in use between two dates :
ID StartDate EndDate as ItemsInUse
A 01.01.2013 31.03.2013
B 01.02.2013 30.04.2013
C 01.05.2013 31.05.2013
I need a way to query that table and return something like :
ID Month
A 01
A 02
A 03
B 02
B 03
B 04
C 05
I'm really stuck with this. Does anyone have any clues on doing this ?
PS : European dates formats ;-)
Create a calendar table then
SELECT DISTINCT i.Id, c.Month
FROM Calendar c
JOIN ItemsInUse i
ON c.ShortDate BETWEEN i.StartDate AND i.EndDate
Here should be the answer:
select ID,
ROUND(MONTHS_BETWEEN('31.03.2013','01.01.2013')) "MONTHS"
from TABLE_NAME;
I would use MONTH() http://dev.mysql.com/doc/refman/5.6/en/date-and-time-functions.html#function_month
SELECT ID, MONTH(StartDate)
If your dates are padded in the way you suggest - i.e. where one digit days or months have an initial 0 - you can just use the SUBSTRING function as in:
SELECT SUBSTRING(StartDate, 3, 2) ...
Alternatively, if you do not have the padding you can use:
SELECT SUBSTRING_INDEX( SUBSTRING_INDEX(StartDate, '.',2), '.', -1) ...
(This latter command is in two parts. The inner SUBSTRING_INDEX generates a string containing your day and month, and the outer SUBSTRING_INDEX takes the month.)
Hmm... Dare say this can be improved on, but as a rough 'n' ready (assuming ms-sql)
create table #results (ID varchar(10), Month int, Year int);
declare #ID varchar(10);
declare #StartDate datetime;
declare #EndDate datetime;
declare myCursor cursor for select [ID], [StartDate],[EndDate] from ItemsInUse;
open myCursor;
delete from #results;
fetch next from myCursor into #ID, #StartDate, #EndDate;
declare #tempDate datetime;
while ##FETCH_STATUS = 0
begin
set #tempdate = CAST( CAST(year(#StartDate) AS varchar) + '-' + CAST(MONTH(#StartDate) AS varchar) + '-' + CAST(1 AS varchar) AS DATETIME);
while #tempDate < #EndDate
begin
insert into #results values (#ID, MONTH(#tempDate), YEAR(#tempDate));
set #tempDate = DATEADD(month,1,#tempdate);
end
fetch next from myCursor into #ID, #StartDate, #EndDate;
end
close myCursor;
deallocate myCursor;
select * from #results;
drop table #results;

Loop through a recordset and use the result to do another SQL select and return the results

I am completely new to stored procedure. This time, I need to create a stored procedure in MS SQL.
Let's say I have the following table.
Table name: ListOfProducts
--------------------------
SomeID, ProductID
34, 4
35, 8
35, 11
How do I pass in a SomeID. Use this SomeID to select a recordset from table, ListOfProducts. Then loop through this record set.
Let's say I pass in SomeID = 35.
So, the record set will return 2 records with SomeID 35. In the loop, I will get ProductID 8 and 11, which will be used to do another select from another table.
The stored procedure should return the results from the 2nd select.
How can I do this in MS SQL stored procedure?
Sorry, for this newbie question. Thanks for any help.
If you want looping through the records. You can do like:
--Container to Insert Id which are to be iterated
Declare #temp1 Table
(
tempId int
)
--Container to Insert records in the inner select for final output
Declare #FinalTable Table
(
Id int,
ProductId int
)
Insert into #temp1
Select Distinct SomeId From YourTable
-- Keep track of #temp1 record processing
Declare #Id int
While((Select Count(*) From #temp1)>0)
Begin
Set #Id=(Select Top 1 tempId From #temp1)
Insert Into #FinalTable
Select SomeId,ProductId From ListOfProducts Where Id=#Id
Delete #temp1 Where tempId=#Id
End
Select * From #FinalTable
There is probably no point in writing an explicit loop if you don't need to preform some action on the products that can't be done on the whole set. SQL Server can handle stuff like this much better on its own. I don't know what your tables look like, but you should try something that looks more like this.
CREATE PROC dbo.yourProcName
#SomeID int
AS
BEGIN
SELECT
P.ProductId,
P.ProductName
FROM
Product P
JOIN
ListOfProducts LOP
ON LOP.ProductId = P.ProductId
WHERE
LOP.SomeId = #SomeID
END
I had to do something similar in order to extract hours from a select resultset start/end times and then create a new table iterating each hour.
DECLARE #tCalendar TABLE
(
RequestedFor VARCHAR(50),
MeetingType VARCHAR(50),
RoomName VARCHAR(MAX),
StartTime DATETIME,
EndTime DATETIME
)
INSERT INTO #tCalendar(RequestedFor,MeetingType,RoomName,StartTime,EndTime)
SELECT req as requestedfor
,meet as meetingtype
,room as rooms
,start as starttime
,end as endtime
--,u.datetime2 as endtime
FROM mytable
DECLARE #tCalendarHours TABLE
(
RequestedFor VARCHAR(50),
MeetingType VARCHAR(50),
RoomName VARCHAR(50),
Hour INT
)
DECLARE #StartHour INT,#EndHour INT, #StartTime DATETIME, #EndTime DATETIME
WHILE ((SELECT COUNT(*) FROM #tCalendar) > 0)
BEGIN
SET #StartTime = (SELECT TOP 1 StartTime FROM #tCalendar)
SET #EndTime = (SELECT TOP 1 EndTime FROM #tCalendar)
SET #StartHour = (SELECT TOP 1 DATEPART(HOUR,DATEADD(HOUR,0,StartTime)) FROM #tCalendar)
SET #EndHour = (SELECT TOP 1 DATEPART(HOUR,DATEADD(HOUR,0,EndTime)) FROM #tCalendar)
WHILE #StartHour <= #EndHour
BEGIN
INSERT INTO #tCalendarHours
SELECT RequestedFor,MeetingType,RoomName,#StartHour FROM #tCalendar WHERE StartTime = #StartTime AND EndTime = #EndTime
SET #StartHour = #StartHour + 1
END
DELETE #tCalendar WHERE StartTime = #StartTime AND EndTime = #EndTime
END
Do something like this:
Declare #ID int
SET #ID = 35
SELECT
p.SomeID
,p.ProductID
FROM ListOfProducts p
WHERE p.SomeID = #ID
-----------------------
--Or if you have to join to get it
Declare #ID int
SET #ID = 35
SELECT
c.SomeID
,p.ProductID
,p.ProductName
FROM ListOfProducts p
INNER JOIN categories c on p.ProductID = c.SomeID
WHERE p.SomeID = #ID
You can use option with WHILE loop and BREAK/CONTINUE keywords
CREATE PROC dbo.yourProcName
#SomeID int
AS
BEGIN
IF OBJECT_ID('tempdb.dbo.#resultTable') IS NOT NULL DROP TABLE dbo.#resultTable
CREATE TABLE dbo.#resultTable
(Col1 int, Col2 int)
DECLARE #ProductID int = 0
WHILE(1=1)
BEGIN
SELECT #ProductID = MIN(ProductID)
FROM ListOfProducts
WHERE SomeID = #SomeID AND ProductID > #ProductID
IF #ProductID IS NULL
BREAK
ELSE
INSERT dbo.#resultTable
SELECT Col1, Col2
FROM dbo.yourSearchTable
WHERE ProductID = #ProductID
CONTINUE
END
SELECT *
FROM dbo.#resultTable
END
Demo on SQLFiddle

Consecutive streak of dates

Hopefully this isn't a dupe of another question, but I couldn't see it anywhere else - also this is a simplified version of another question I asked, hopefully to get me started on working out how to approach it.
I am looking to work out consecutive ranges of payments where there has been at least one payment in each month.
I have the following sample data
CREATE TABLE #data
(
Contact_reference NVARCHAR(55)
,Date_payment DATETIME
,Payment_value MONEY
)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2003-06-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2004-06-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2004-12-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-04-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-05-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-06-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-07-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-08-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-09-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-10-10',12.8205)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-11-10',12.8205)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-12-10',12.8205)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2006-01-10',12.8205)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2006-02-10',12.8205)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2006-02-28',12.8205)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2006-04-12',12.8205)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2006-05-10',19.2308)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2007-06-11',19.2308)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2007-07-10',19.2308)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2007-08-09',19.2308)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2007-09-10',19.2308)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2007-10-09',19.2308)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2007-11-09',19.2308)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2007-12-10',19.2308)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2008-01-10',19.2308)
And what I would like to be able to do is to work out for each contact the ranges over which they gave consecutively (defined as giving at least once in every calendar month), the number of consecutive payments, the total value per range (and ideally if possible the gap between the current range and the end of the most recent one).
For the test data above my output would look like this:
CREATE TABLE #results
(
contact_reference NVARCHAR(55)
,Range_start DATETIME
,Range_end DATETIME
,Payments INT
,Value MONEY
,months_until_next_payment INT --works out the gap between the range_end date for a group and the range_start date for the next group
)
INSERT INTO #results VALUES('18EC3CD2-3065-4FF4-BE40-000004228590','2003-06-08','2003-06-08',1,12.82,12)
INSERT INTO #results VALUES('18EC3CD2-3065-4FF4-BE40-000004228590','2004-06-08','2004-06-08',1,12.82,6)
INSERT INTO #results VALUES('18EC3CD2-3065-4FF4-BE40-000004228590','2004-12-08','2004-12-08',1,12.82,4)
INSERT INTO #results VALUES('18EC3CD2-3065-4FF4-BE40-000004228590','2005-04-08','2006-02-28',12,153.843,2)
INSERT INTO #results VALUES('18EC3CD2-3065-4FF4-BE40-000004228590','2006-04-12','2008-06-06',27,416.6673,NULL)
I've looked for answers using islands, or iterations but I quite frankly don't even know where to begin applying them to my question, so any help massively appreciated :)
Edit: I've added in the months_until_next_payment column. This would be more efficiently done in the application rather than with a self join however as SQL Server does not have any particularly satisfactory way of referencing next and previous rows.
;WITH base AS (
SELECT Contact_reference ,
Payment_value,
DATEPART(YEAR, Date_payment)*12 + DATEPART(MONTH, Date_payment) -
DENSE_RANK() OVER
(PARTITION BY Contact_reference
ORDER BY DATEPART(YEAR, Date_payment)*12 + DATEPART(MONTH, Date_payment)) AS G,
Date_payment
FROM #data
),
cte AS
(
SELECT
Contact_reference,
ROW_NUMBER() over (partition by Contact_reference
order by MIN(Date_payment)) RN,
MIN(Date_payment) Range_start,
MAX(Date_payment) Range_end,
COUNT(Payment_value) Payments,
SUM(Payment_value) Value
FROM base
GROUP BY Contact_reference, G
)
SELECT
c1.Contact_reference,
c1.Payments,
c1.Range_end,
c1.Range_start,
c1.Value,
DATEDIFF(month, c1.Range_end,c2.Range_start) months_until_next_payment
FROM cte c1
LEFT join cte c2 ON c1.Contact_reference=c2.Contact_reference and c2.RN = c1.RN+1
You can do it using cursor. Language like c#/java are better choice for this problem.
DECLARE #date DATETIME
DECLARE #nextDate DATETIME
DECLARE #rangeStart DATETIME
DECLARE #rangeEnd DATETIME
DECLARE #value decimal(18,2)
DECLARE #valueSum decimal(18,2)
DECLARE #count int
DECLARE #PaymentCursor CURSOR
SET #PaymentCursor = CURSOR FOR
SELECT Date_payment, Payment_value FROM #data
ORDER BY Date_payment
OPEN #PaymentCursor
FETCH NEXT FROM #PaymentCursor INTO #nextDate, #value
SET #date = #nextDate
SET #rangeStart = #nextDate
SET #valueSum = 0
SET #count = 0
WHILE (##FETCH_STATUS = 0)
BEGIN
FETCH NEXT FROM #PaymentCursor INTO #nextDate, #value
SET #count = #count + 1
SET #valueSum = #valueSum + #value
IF (DATEDIFF(mm, #date, #nextDate) > 1)
BEGIN
SELECT #rangeStart AS RangeStart, #date AS RangeEnd, #count AS Coount, #valueSum AS VALUE, DATEDIFF(mm, #date, #nextDate) AS months_until_next_payment
SET #valueSum = 0
SET #count = 0
SET #rangeStart = #nextDate
END
SET #date = #nextDate
END
SELECT #rangeStart AS RangeStart, #date AS RangeEnd, #count AS Coount, #valueSum AS VALUE, null AS months_until_next_payment
CLOSE #PaymentCursor
DEALLOCATE #PaymentCursor