Looping through a table SQL - 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.

Related

Best way to lock SQL Server database

I'm writing a C++ application that is connecting to a SQL Server database via ODBC.
I need an Archive function so I'm going to write a stored procedure that takes a date. It will total up all the transactions and payments prior to that date for each customer, update the customer's starting balance accordingly, and then delete all transactions and payments prior to that date.
It occurs to me that it could be very bad if someone else is adding or deleting transactions or payments at the same time this stored procedure runs. Therefore, I'm thinking I should lock the entire database during execution, which would not happen that often.
I'm curious if my logic is good and what would be the best way to lock the entire database for such a purpose.
UPDATE:
Based on user12069178's answer, here's what I've come up with so far. Would appreciate any feedback on it.
ALTER PROCEDURE [dbo].[ArchiveData] #ArchiveDateTime DATETIME
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #TempTable TABLE
(
CustomerId INT,
Amount BIGINT
);
BEGIN TRANSACTION;
-- Archive transactions
DELETE Transactions WITH (TABLOCK)
OUTPUT deleted.CustomerId, deleted.TotalAmount INTO #TempTable
WHERE [TimeStamp] < #ArchiveDateTime;
IF EXISTS (SELECT 1 FROM #TempTable)
BEGIN
UPDATE Customers SET StartingBalance = StartingBalance +
(SELECT SUM(Amount) FROM #TempTable temp WHERE Id = temp.CustomerId)
END;
DELETE FROM #TempTable
-- Archive payments
DELETE Payments WITH (TABLOCK)
OUTPUT deleted.CustomerId, deleted.Amount INTO #TempTable
WHERE [Date] < #ArchiveDateTime;
IF EXISTS (SELECT 1 FROM #TempTable)
BEGIN
UPDATE Customers SET StartingBalance = StartingBalance -
(SELECT SUM(Amount) FROM #TempTable temp WHERE Id = temp.CustomerId)
END;
COMMIT TRANSACTION;
END
Generally the way to make sure that the rows you are deleting are the ones that you are totalling and inserting is to use the OUTPUT clause while deleting. It can output the rows that were selected for deletion.
Here's a setup that will give us some transactions:
USE tempdb;
GO
DROP TABLE IF EXISTS dbo.Transactions;
GO
CREATE TABLE dbo.Transactions
(
TransactionID int NOT NULL IDENTITY(1,1)
CONSTRAINT PK_dbo_Transactions
PRIMARY KEY,
TransactionAmount decimal(18,2) NOT NULL,
TransactionDate date NOT NULL
);
GO
SET NOCOUNT ON;
DECLARE #Counter int = 1;
WHILE #Counter <= 50
BEGIN
INSERT dbo.Transactions
(
TransactionAmount, TransactionDate
)
VALUES (ABS(CHECKSUM(NewId())) % 10 + 1, DATEADD(day, 0 - #Counter * 3, GETDATE()));
SET #Counter += 1;
END;
SELECT * FROM dbo.Transactions;
GO
Now the following code deletes the rows past a cutoff, and concurrently outputs the amounts into a table variable, and then inserts the total row into the transactions table.
DECLARE #CutoffDate date = DATEADD(day, 1, EOMONTH(DATEADD(month, -2, GETDATE())));
DECLARE #TransactionAmounts TABLE
(
TransactionAmount decimal(18,2)
);
BEGIN TRAN;
DELETE dbo.Transactions WITH (TABLOCK)
OUTPUT deleted.TransactionAmount INTO #TransactionAmounts
WHERE TransactionDate < #CutoffDate;
IF EXISTS (SELECT 1 FROM #TransactionAmounts)
BEGIN
INSERT dbo.Transactions (TransactionAmount, TransactionDate)
SELECT SUM(TransactionAmount), DATEADD(day, 1, #CutoffDate)
FROM #TransactionAmounts;
END;
COMMIT;
I usually try to avoid specifying locks whenever possible but based on your suggestion, I've added it. If you didn't have the table lock, it'd still be ok but would mean that even if someone adds in a new "old" row while you're doing this, it won't be in the total or deleted either. Making the transaction serializable would also achieve the outcome and would lock less than the table lock if the number of rows being deleted was less than the lock escalation threshold (defaults to 5000).
Hope that helps.

Conditional SQL Insert

I have to write a. insert statement that looks at a table and inserts a record if the conditions are met. This is a one time thing so not overly concerned about it being efficient.
the table contains a work breakdown structure for a project ( each project having, a project level(wbs1), a phase level(wbs2) and a task level (wbs3)
that table looks like this
Wbs1 wbs2 wbs3 name
262 ProjectA
262 01 Data Analsys
262 01 01 Data cleansing
262 01 02 Data Transforming
I need to insert a phase(WBS2) to each project(WBS1) with an insert statement, for example adding a wbs2 "02" to each project(wbs1).
writing the insert statment is no problem and I select the data from the project level since most of it is redundant so no issue there, im just not sure how to have it loop through and add the phase to each project, since there are multiple rows with the same project(wbs1) number
insert statement sample
Insert into dbo.pr ([WBS1],[WBS2],[WBS3],[Name])
(Select [WBS1],'999',[WBS3],'In-House Expenses'
from dbo.pr where wbs1 = #ProjectID
and wbs2 ='')
How do i run this statement to inserta row every project?(wbs1)
hopefully this makes sense.
You can use a temporary table with an added RowNumber field and then a WHILE loop to handle the looping over each row. You can then run an IF EXISTS as a criteria check before running the stored procedure. See below for example
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
DECLARE #ProjectId NVARCHAR(50) = '262'
CREATE TABLE #Temp (RowNumber INT, wbs1 NVARCHAR(255), wbs2 NVARCHAR(255), wbs3 NVARCHAR(255), name NVARCHAR(255))
INSERT INTO #Temp
SELECT ROW_NUMBER() OVER (ORDER BY wbs1, wbs2, wbs3, name)
,pr.*
FROM pr
select *
from #temp
-- Create loop variables to handle incremeting
DECLARE #Counter INT = 1;
DECLARE #MaxLoop INT = (SELECT COUNT(wbs1) FROM #temp)
WHILE #Counter <= #MaxLoop
BEGIN
-- Use if Exists to check the current looped meets whatever critiera you have
IF EXISTS (SELECT 'true'
FROM #Temp
WHERE RowNumber = #Counter
AND wbs1 = #ProjectId
AND wbs2 = ''
)
BEGIN
Insert into pr (wbs1,wbs2,wbs3,name)
(Select [WBS1],'999',[WBS3],'In-House Expenses'
from #temp where RowNumber = #Counter)
END
-- Remember to increment the counter
SET #Counter = #Counter + 1;
END
SELECT *
FROM pr
drop table #temp

SQL server update query to add totals

I have a table as below:
The first record Amount and TotalAmount are same
In the second record Amount is added from first row and current and TotalAmount is added
And so on....
Now if I update the second row from 1.25 to 2, then the TotalAmount for all subsequent records should be changed.
I need an update query for this.
I have seq_no and row no as reference and field Type is the reference
Ideally you should create a view or stored procedure that performs a running total, this is an example of one method using a subquery:
SELECT
Type,
Amount ,
Total =
(
SELECT SUM(Amount)
FROM SomeTable B
WHERE B.Type=A.Type AND B.RowNum <= A.RowNum
)
FROM SomeTable A
This is just one method (not necessarily the best). I would suggest you google 'Running totals in SQL' and familiarise yourself with the explanation of this and other methods their pros, cons and performance implications of each.
One question, what version of SQL server are you using?
If you can use row number as reference than you can try following query but storing totals in table is bad idea as mentioned in comment:
DECLARE #Temp TABLE
(
Amount float,
TotalAmount float,
Rownum int
)
INSERT INTO #Temp VALUES (1.25,1.25,1),(1.25,2.50,2),(10,12.50,3)
DECLARE #PreviousAmount AS FLOAT
SELECT #PreviousAmount = Amount FROM #Temp WHERE Rownum=1
DECLARE #NewAmount AS FLOAT = 2
UPDATE #Temp SET TotalAmount = TotalAmount - #PreviousAmount WHERE Rownum>=1
UPDATE #Temp SET Amount=#NewAmount, TotalAmount = TotalAmount + #NewAmount WHERE Rownum=1
UPDATE #Temp SET TotalAmount = TotalAmount + #NewAmount WHERE Rownum>1
SELECT * FROM #Temp
If you want to use triggers(which is not recommended).you can use this:
create trigger trigger_name
for update
as
declare #count int= (select count(*) from table)
declare #a int =1
while(#a<#count)
begin
update table
set total_amount=(select amount from table where row_number=#a) + (select amount from table where row_number=#a-1 )
where row_number!=1
set #a=#a+1
end
Go

SQL Insert from 2 tables with multiple rows on subquery

I am trying to update some budgeting data in an SQL database. I have 1 table which is the set of data for this year, and another table which contains last years (and I need to insert this years data into).
During the insert I need to create a unique row for each sitenumber which is in a temporary table, against each site number needs to be this years information (week number, startdate, enddate etc).
I have tried using a subquery (but obviously this fails as the subquery for the getting the site number will return multiple records. So I am trying a cursor, however although it doesn't error, it doesn't insert any date. if anyone has any ideas that'd be great.
This is my cursor code
create table #tempSiteNoTable (SiteNo int)
insert into #tempSiteNoTable
Select distinct(SiteNumber)
from Lynx_Period_Lookup
begin tran xxx
Declare #SiteNNo int
Declare SiteNumberCursor Cursor FOR
Select
SiteNo from #tempSiteNoTable where SiteNo = #SiteNNo
Open SiteNumberCursor
Fetch next from SiteNumberCursor
Into #SiteNNo while ##fetch_status = 0
begin
insert into Lynx_Period_Lookup
(SiteNumber,SubPeriod,StartDate,EndDate,[Year],Period,[Week],BusinessCalendarNumber,BusinessCalendarName)
Select
#SiteNNo,
SubPeriod,
StartDate,
EndDate,
2014 as year,
Period,
WeekNo,
BusinessCalendarNumber,
BusinessCalendarName
from accountingperiods
Fetch next from SiteNumberCursor
into #SiteNNo
End
Close SiteNumberCursor
Deallocate SiteNumberCursor
You should be able to do this without a CURSOR - and quite easily, too!
Try this:
CREATE TABLE #tempSiteNoTable (SiteNo int)
INSERT INTO #tempSiteNoTable(SiteNo)
SELECT DISTINCT (SiteNumber)
FROM dbo.Lynx_Period_Lookup
INSERT INTO dbo.Lynx_Period_Lookup(SiteNumber, SubPeriod, StartDate, EndDate, [Year], Period, [Week], BusinessCalendarNumber, BusinessCalendarName)
SELECT
t.SiteNo,
ap.SubPeriod,
ap.StartDate,
ap.EndDate,
2014 as year,
ap.Period,
ap.WeekNo,
ap.BusinessCalendarNumber,
ap.BusinessCalendarName
FROM
#tempSiteNoTable t
INNER JOIN
dbo.AccountingPeriods ap ON ... (how are those two sets of data related?)...
The only point I don't know - how are AccountingPeriods and #tempSiteNoTable related - what common column do they share?
That's all there is to it - no cursor, no messing around with row-by-agonizing-row (RBAR) processing - just one, nice, clean set-based statement and you're done
I think you're looking for a CROSS JOIN; try the following snippet of code.
DECLARE #Table1 TABLE (Week int, StartDate datetime)
DECLARE #Table2 TABLE (sitenumber int)
INSERT INTO #Table1 (Week, StartDate)
VALUES (1, '2014-01-01'), (2, '2014-01-08')
INSERT INTO #Table2 (sitenumber)
VALUES (1), (2)
SELECT *
FROM #Table1 CROSS JOIN #Table2

Select random rows and stop when a specific sum/total is reached

I'm using SQL Server 2012 and I'm trying to do something like this:
SELECT SUM(MILES) from tblName WHERE
mDate > = '03/01/2012' and
mDate <= '03/31/2012'
-- and...
/*
now I want to add here do until the SUM of Miles
is equal to or greater then '3250' and get the
results rows randomly
*/
So in other words, I want to select random rows from a table that have a specified from and to date and stop when the sum of miles is at or over the number: 3250
Since you're using SQL Server 2012, here is a much easier approach that doesn't require looping.
DECLARE #tbl TABLE(mDate DATE, Miles INT)
INSERT #tbl VALUES
('20120201', 520), ('20120312', 620),
('20120313', 720), ('20120314', 560),
('20120315', 380), ('20120316', 990),
('20120317', 1020), ('20120412', 520);
;WITH x AS
(
SELECT
mDate,
Miles,
s = SUM(Miles) OVER
(
ORDER BY NEWID() ROWS UNBOUNDED PRECEDING
)
FROM #tbl
WHERE mDate >= '20120301'
AND mDate < '20120401'
)
SELECT
mDate,
Miles,
s
FROM x
WHERE s <= 3250
ORDER BY s;
SQLfiddle demo - hit "Run SQL" multiple times to see random results.
You can do SELECT TOP x ... ORDER BY newid() to get a sum of random rows. The problem lies in determining 'x'. You can't even be sure that the largest value of 'x' (number of rows that match the query) will have a total large enough to meet your requirements without testing that first:
DECLARE #stopAt int
DECLARE #x int
DECLARE #result int
SET #stopAt = 3250
SET #x = 1
SELECT #result = SUM(MILES) from tblName
WHERE
mDate >= '03/01/2012' and
mDate <= '03/31/2012'
IF (#result < #stopAt)
SELECT NULL -- this can't be done
ELSE
BEGIN
WHILE (1=1)
BEGIN
SELECT TOP (#x) #result = SUM(MILES) FROM tblName
WHERE
mDate >= '03/01/2012' and
mDate <= '03/31/2012'
ORDER BY newid()
IF #result >= #stopAt
BREAK
SET #x = #x + 1
END
SELECT #result
END
Just a note about this - the algorithm starts and 1 and increments up until a suitable match is found. A more efficient approach (for larger sets of data) might include a binary type search that caches the lowest suitable result and returns when the deepest node (or an exact match) is found.
I can't think of a way without a TSQL While... loop. This in combination with the TSQL paging with ROW_NUMBER() should get you there.
http://www.mssqltips.com/sqlservertip/1175/page-through-sql-server-results-with-the-rownumber-function/
In the ROW_NUMBER query, sum the Miles into another MileSum column, then in the while loop select the set all the rows that correspond with the ROW_NUMBER query while accumulating these MileSum values into a variable. Terminate when the variable exceeds 3250.
Try
SELECT MILES
, RANK() OVER (ORDER BY NEWID()) yourRank
FROM #tblName
WHERE miles>3250
AND mDate >= '03/01/2012'
AND mDate <= '03/31/2012'
ORDER BY yourRank
and then you can add a TOP 5 or whatever you want.
You get those in random order for sure.
Just a sample code for you to understand the concept.
create table temp(intt int)
insert into temp values(1)
insert into temp values(2)
insert into temp values(3)
insert into temp values(4)
insert into temp values(5)
insert into temp values(6)
insert into temp values(7)
insert into temp values(8)
insert into temp values(9)
insert into temp values(10)
insert into temp values(11)
insert into temp values(12)
insert into temp values(13)
insert into temp values(14)
insert into temp values(15)
insert into temp values(16)
insert into temp values(17)
insert into temp values(18)
insert into temp values(19)
insert into temp values(20)
insert into temp values(21)
declare #sum int = 0;
declare #prevInt int = 0;
while(#sum<50)
begin
set #prevInt = (select top(1) intt from temp order by newid());
set #sum = #sum + #prevInt;
end
set #sum = #sum-#prevInt;
select #sum
drop table temp
The reason for this approach is that paging would not return wide spread result unless and until you have thousands of records because in it the data is grouped into pages and with less records, the same page is hit multiple number of times giving the same result.
Also, there might be cases when a blank page is hit giving 0 as the result.(i don't know why sometimes a blank page is hit.)