SQL Server: is this a bug or do I have a misunderstanding? - sql

Today I'm found a very sticky problem on SQL Server 2014.
Scenario: I want to pay awards to my customer (some pin code for cell phone operator)
In last cycle of loop T.Used = 0 condition is bypassed and is not working. I know in other conditions in that query (T.Cash < (#myAwards - #paid)) is there a mistake and I must to use T.Cash <= (#myAwards - #paid) instead of this but please focus on main question.
Why it's happened when I update Used flag to 1 (True) then in next loop it's selected while it doesn't have a valid condition (T.Used = 0)?
DECLARE #myAwards INT = 90000,
#paid INT = 0;
DECLARE #Temp TABLE
(
Id INT NOT NULL,
Pin VARCHAR(100) NOT NULL,
Cash INT NOT NULL,
[Weight] INT NULL,
Used BIT NOT NULL
)
INSERT INTO #Temp
SELECT
UPFI.Id, UPFI.PinCode,
PT.Cash, NULL, 0
FROM
dbo.UploadedPinFactorItem UPFI WITH (NOLOCK)
INNER JOIN
dbo.PinType PT WITH (NOLOCK) ON PT.ID = UPFI.PinTypeID
WHERE
PT.Cash <= #myAwards
UPDATE T
SET [Weight] = ISNULL((SELECT COUNT(TT.Id)
FROM #Temp TT
WHERE TT.Cash = T.Cash), 0) * T.Cash
FROM #Temp T
--For debug (first picture)
SELECT * FROM #Temp
DECLARE #i int = 1
DECLARE #count int = 0
SELECT #count = COUNT([Id]) FROM #Temp C WHERE C.Used = 0
WHILE (#i <= #count AND #paid < #myAwards)
BEGIN
DECLARE #nextId INT,
#nextCash INT,
#nextFlag BIT;
-- 'T.Used = 0' condition is by passed
SELECT TOP (1)
#nextId = T.Id, #nextCash = T.Cash, #nextFlag = T.Used
FROM
#Temp T
WHERE
T.Used = 0
AND T.Cash < (#myAwards - #paid)
ORDER BY
T.[Weight] DESC, T.Cash DESC, T.Id DESC
UPDATE #Temp
SET Used = 1
WHERE Id = #nextId
SET #i = #i + 1
SET #paid = #paid + #nextCash
--Show result in second picture
SELECT
#i AS 'i', #paid AS 'paid', #nextFlag AS 'flag', #nextId AS 'marked Id',*
FROM
#temp T
ORDER BY
T.[Weight] DESC, T.Cash DESC, T.Id DESC
END
SELECT 'final', #paid, *
FROM #temp T
ORDER BY T.[Weight] DESC, T.Cash DESC, T.Id DESC
Please let me to understand this is a bug or I have misunderstanding
First screenshot:
Second screenshot (result of loop):
Third screenshot (final result):

As per my comments:
This isn't a problem with the condition, the problem is with the implemented logic. After i = 4, there are no more rows where T.Used = 0 AND T.Cash < (#myAwards - #paid), that makes it so your reassigning variables gets zero rows, so they mantain the previous values.
You can test this behavior by doing:
DECLARE #A INT = 10;
SELECT #A = object_id
FROM sys.all_objects
WHERE name = 'an object that doesn''t exist'
SELECT #A;

Related

SQL Server generate script for views and how to decide order?

I am generating the script for views using SQL Server built-in feature (Task -> Generate script). I am creating separate file for each object (of view). I have say around 400 files (containing SQL script of all views) to be executed on another database and to do that automatically I have created BAT file which takes care of that.
There are views which are dependent on other views and due to that many views failed to execute. Is there any way by which we can set order of execution and get rid off the failure ?
Any pointers would be a great help.
Please let me know if you need more details.
Thanks
Jony
Could you try this query? You can execute the create scripts in order to "gen" (generation).
DECLARE #cnt int = 0, #index int;
DECLARE #viewNames table (number int, name varchar(max))
DECLARE #viewGen table (id uniqueidentifier, gen int, name varchar(max), parentId uniqueidentifier)
INSERT INTO #viewNames
SELECT ROW_NUMBER() OVER(ORDER BY object_Id), name FROM sys.views
SELECT #cnt = COUNT(*) FROM #viewNames
SET #index = #cnt;
WHILE ((SELECT COUNT(*) FROM #viewGen) < #cnt)
BEGIN
DECLARE #viewName varchar(200)
SELECT #viewName = name FROM #viewNames WHERE number = #index;
DECLARE #depCnt int = 0;
SELECT #depCnt = COUNT(*) FROM sys.dm_sql_referencing_entities ('dbo.' + #viewName, 'OBJECT')
IF (#depCnt = 0)
BEGIN
INSERT INTO #viewGen SELECT NEWID(), 0, name, null FROM #viewNames WHERE number = #index;
END
ELSE
BEGIN
IF EXISTS(SELECT * FROM sys.dm_sql_referencing_entities ('dbo.' + #viewName, 'OBJECT') AS r INNER JOIN #viewGen AS v ON r.referencing_entity_name = v.name)
BEGIN
DECLARE #parentId uniqueidentifier = NEWID();
INSERT INTO #viewGen SELECT #parentId, 0, name, null FROM #viewNames WHERE number = #index;
UPDATE v
SET v.gen = (v.gen + 1), parentId = #parentId
FROM #viewGen AS v
INNER JOIN sys.dm_sql_referencing_entities('dbo.' + #viewName, 'OBJECT') AS r ON r.referencing_entity_name = v.name
UPDATE #viewGen
SET gen = gen + 1
WHERE Id = parentId OR parentId IN (SELECT Id FROM #viewGen WHERE parentId = parentId)
END
END
SET #index = #index - 1
IF (#index < 0) BEGIN SET #index = #cnt; END
END
SELECT gen as [order], name FROM #viewGen ORDER BY gen
Expecting result:
order name
0 vw_Ancient
1 vw_Child1
1 vw_Child2
2 vw_GrandChild

Using TOP(variable) with a subset of data

I have abstracted the problem to the following situation:
I have a table (A) that contains the number (Quantity) of items I want to update.
Next I have a table (SL) that contains the references to table (A) that I need to select from.
And finally the table that needs to get updated (B)
CREATE TABLE A
(
Id int,
Quantity int
)
CREATE TABLE SL
(
Id int,
A_Id int,
S_Id int
)
CREATE TABLE B
(
Id int,
StatusValue int,
A_Id int,
S_Id int NULL,
)
So let's insert some data for testing purposes:
INSERT INTO A Values (1, 4), (2, 2), (3, 3), (4, 4), (5, 2)
delete from B
declare #i int = 1;
declare #j int = 0;
declare #maxA int = 5;
declare #rows_to_insert int = 10;
while #i < #maxA
begin
while #j < #rows_to_insert * #i
begin
INSERT INTO B VALUES (10+#j, 0, #i, null)
set #j = #j + 1
end
set #i = #i + 1
end
select * from B
INSERT INTO SL Values (1, 1, 1), (2, 2, 1), (3 ,2, 1)
And now on to the problem. I want to update TOP(Quantity) of records in B relating to the records in SL. Basically this is what I want to do, but it is unsupported in SQL:
DECLARE #Sale_Id int = 1;
WITH AB (AId, AQuantity, SaleId)
AS
(
SELECT A.Id, A.Quantity, SL.S_Id FROM A
INNER JOIN SL on A.Id = SL.A_Id
WHERE SL.S_Id = #Sale_Id
)
UPDATE TOP(AB.Quantity) B
SET StatusValue = 1,
S_Id = AB.SaleId
FROM AB
WHERE StatusValue = 0 -- Edited
AND B.A_Id = AB.AId
The error message is
Msg 4104, Level 16, State 1, Line 55
The multi-part identifier "AB.Quantity" could not be bound.
what are my options of getting this done?
(There is always the Cursor but is that a good option?)
Note: The data has a funny side to it that in SL there is 2 times a record referencing A_Id = 2. This implies that the result needs to have 4 B records with A_Id = 2 updated.
This should work
DECLARE #Sale_Id int = 1;
with tmp as (
SELECT A.Id, A.Quantity, SL.S_Id SaleID, B.S_ID, b.id b_id, B.StatusValue,
rn=dense_rank() over (partition by a.id order by b.id)
FROM A
JOIN SL on A.Id = SL.A_Id
JOIN B ON B.A_Id = A.Id
WHERE SL.S_Id = #Sale_Id
)
update tmp
set S_ID = SaleID,
StatusValue = 1
where rn <= quantity;
However, your data looks funny with A.id=2 being sold twice on the same SL.id=1.
SQL Fiddle
So it seems the Cursor is the way I'll need to go:
DECLARE #A_Id int, #Quantity int;
DECLARE ABCursor CURSOR LOCAL READ_ONLY FOR
SELECT A.Id, A.Quantity FROM A
INNER JOIN SL on A.Id = SL.A_Id
WHERE SL.S_Id = #Sale_Id
OPEN ABCursor
FETCH NEXT FROM ABCursor
INTO #A_Id, #Quantity
WHILE ##FETCH_STATUS = 0
BEGIN
UPDATE TOP(#Quantity) B
SET StatusValue = 1,
S_Id = #Sale_Id
WHERE StatusValue = 0
AND B.A_Id = #A_Id
AND S_Id is null
FETCH NEXT FROM ABCursor
INTO #A_Id, #Quantity
END
CLOSE ABCursor;
DEALLOCATE ABCursor;
Now must read up on what is the best definition for the Cursor i.e.: LOCAL READ_ONLY

within a sql while loop, get id of last inserted row then insert it into a different table, using OUTPUT?

I am writing some sql, using sql server 2008, to loop around some records. Within this loop I need to insert a row and the ID of that row will be put into a field in an existing record in a different table. I thought I would be able to achieve this with OUTPUT but I couldn't find an example of exactly what I wanted to do.
Here's what I have so far:
DECLARE #courseTempID INT
DECLARE #courseID INT
DECLARE #academicYearID INT
DECLARE #courseCode VARCHAR(10)
DECLARE #uio_id INT
DECLARE #creatorIntranetUserID INT
WHILE (SELECT count(*) FROM CoursesTemp WHERE dmprocessed = 0) > 0
BEGIN
SELECT TOP 1 #id = courseTempID FROM CoursesTemp WHERE dmprocessed = 0
SELECT TOP 1 #academicYearID = academicYearID FROM CoursesTemp WHERE dmprocessed = 0
SELECT TOP 1 #courseCode = courseCode FROM CoursesTemp WHERE dmprocessed = 0
SELECT TOP 1 #uio_id = uio_id FROM CoursesTemp WHERE dmprocessed = 0
SELECT TOP 1 #creatorIntranetUserID = creatorIntranetUserID FROM CoursesTemp WHERE dmprocessed = 0
INSERT INTO dbo.Courses VALUES(3,#academicYearID,1,#courseCode,#uio_id,GETDATE(),#creatorIntranetUserID)
OUTPUT INSERTED.courseID INTO #courseID
UPDATE CoursesTemp SET dmprocessed = 1, courseID = #courseID WHERE courseTempID = #courseTempID
END
The error I am getting is "Incorrect syntax near 'INSERTED'".
Can anyone help me work out how to use OUTPUT in this way please?
A few performance improvements applied. It is safer to use SCOPE_IDENTITY()
DECLARE #courseTempID INT
DECLARE #courseID INT
DECLARE #academicYearID INT
DECLARE #courseCode VARCHAR(10)
DECLARE #uio_id INT
DECLARE #creatorIntranetUserID INT
DECLARE #id INT
WHILE EXISTS(SELECT 1 FROM CoursesTemp WHERE dmprocessed = 0)
BEGIN
SELECT TOP 1 #id = courseTempID
,#academicYearID = academicYearID
,#courseCode = courseCode
,#uio_id = uio_id
,#creatorIntranetUserID = creatorIntranetUserID
FROM CoursesTemp WHERE dmprocessed = 0
INSERT INTO dbo.Courses VALUES(3,#academicYearID,1,#courseCode,#uio_id,GETDATE(),#creatorIntranetUserID)
UPDATE CoursesTemp SET dmprocessed = 1, courseID = SCOPE_IDENTITY() WHERE courseTempID = #courseTempID
END
OUTPUT needs to go between INSERT INTO dbo.Courses and VALUES(...)
You also have OUTPUT INSERTED.courseID INTO #CourseId but #CourseId is an int variable, it needs to be the table you want to insert into.
EDIT I've reread what you're tying to do. Instead of using OUTPUT, you can change your last line to
UPDATE CoursesTemp SET dmprocessed = 1, courseId = SCOPE_IDENTITY()
WHERE courseTempID = #courseTempID
You may be able to use ##IDENTITY to retrieve the last-inserted ID value. http://msdn.microsoft.com/en-us/library/ms187342.aspx

SQL Server 2000 : generating and incrementing data from column conditionally without using CURSOR

:)
Is there any way to create an index, and incrementing with a given condition, but without CURSOR handling/usage
For example:
The condition in my case is that: "if the current color (this is the item to be checked) is the same as the last one: not increment, otherwise increment in one unit"
This must be in a SQL query with no CURSOR USAGE and of course a good time (work with ... 10000 rows at least)
Thanks in advance.
EDIT: I forgot to mention that NEW_INDEX Column doesn't exist. It must be generated with the with the query.
EDIT2: Is there a way that only make use of SELECT/INSERT/UPDATE statements? (not set, declare...)
Assume a table called Colors with fields ID, Color, and ColorIndex of types int, varchar, and int respectively. I also assume the OP means prev / after based on an ordering of the ID field in asc order.
You could do this without a cursor, and use a while loop...but it definately isn't set based:
DECLARE #MyID int
DECLARE #CurrentIndex int
DECLARE #CurrentColor varchar(50)
DECLARE #PreviousColor varchar(50)
SET #CurrentIndex = (SELECT 0)
SET #MyID = (SELECT TOP 1 ID FROM Colors ORDER BY ID ASC)
SET #CurrentColor = (SELECT '')
SET #PreviousColor = (SELECT Color FROM Colors WHERE ID = #MyID)
WHILE (#MyID IS NOT NULL)
BEGIN
IF (#CurrentColor <> #PreviousColor)
BEGIN
SET #PreviousColor = (SELECT Color FROM Colors WHERE ID = #MyID)
SET #CurrentIndex = (SELECT #CurrentIndex + 1)
UPDATE Colors SET ColorIndex = #CurrentIndex WHERE ID = #MyID
END
ELSE
BEGIN
UPDATE Colors SET ColorIndex = #CurrentIndex WHERE ID = #MyID
SET #PreviousColor = (SELECT Color FROM Colors WHERE ID = #MyID)
END
SET #MyID = (SELECT TOP 1 ID FROM Colors WHERE ID > #MyID ORDER BY ID ASC)
SET #CurrentColor = (SELECT Color FROM Colors WHERE ID = #MyID)
END
The result after execution:
Performance wasn't too shabby as long as ID and color are indexed. The plus side is it is a bit faster then using a regular old CURSOR and it's not as evil. Solution supports SQL 2000, 2005, and 2008 (being that you are using SQL 2000 which did not support CTEs).
declare #ID int,
#MaxID int,
#NewIndex int,
#PrevCol varchar(50)
select #ID = min(ID),
#MaxID = max(ID),
#PrevCol = '',
#NewIndex = 0
from YourTable
while #ID <= #MaxID
begin
select #NewIndex = case when Colour = #PrevCol
then #NewIndex
else #NewIndex + 1
end,
#PrevCol = Colour
from YourTable
where ID = #ID
update YourTable
set NewIndex = #NewIndex
where ID = #ID
set #ID = #ID + 1
end
https://data.stackexchange.com/stackoverflow/q/122958/
select
IDENTITY(int,1,1) as COUNTER
,c1.ID
into
#temp
from
CUSTOMERS c1
left outer join (
select
c1.ID, max(p.ID) as PRV_ID
from
CUSTOMERS c1,
(
select
ID
from
CUSTOMERS
) p
where
c1.ID > p.ID
group by
c1.ID
) k on k.ID = c1.ID
left outer join CUSTOMERS p on p.ID = k.PRV_ID
where
((c1.FAVOURITE_COLOUR < p.FAVOURITE_COLOUR)
or
(c1.FAVOURITE_COLOUR > p.FAVOURITE_COLOUR)
or
p.FAVOURITE_COLOUR is null)
update
CUSTOMERS
set
NEW_INDEX = i.COUNTER
--select *
from
CUSTOMERS
inner join (
select
c1.ID, max(t.COUNTER) as COUNTER
from
CUSTOMERS c1,
(
select
ID
,COUNTER
from
#temp
) t
where
c1.ID >= t.ID
group by
c1.ID
) i on i.ID = CUSTOMERS.ID
drop table #temp
select * from CUSTOMERS

Stored Procedure and output parameter from paging script (SQL Server 2008)

I have the below stored procedure and would like to only have one SQL statement. At the moment you can see there are two statements, one for the actual paging and one for a count of the total records which needs to be return to my app for paging.
However, the below is inefficient as I am getting the total rows from the first query:
COUNT(*) OVER(PARTITION BY 1) as TotalRows
How can I set TotalRows as my output parameter?
ALTER PROCEDURE [dbo].[Nop_LoadAllOptimized]
(
#PageSize int = null,
#PageNumber int = null,
#WarehouseCombinationID int = null,
#CategoryId int = null,
#OrderBy int = null,
#TotalRecords int = null OUTPUT
)
AS
BEGIN
WITH Paging AS (
SELECT rn = (ROW_NUMBER() OVER (
ORDER BY
CASE WHEN #OrderBy = 0 AND #CategoryID IS NOT NULL AND #CategoryID > 0
THEN pcm.DisplayOrder END ASC,
CASE WHEN #OrderBy = 0
THEN p.[Name] END ASC,
CASE WHEN #OrderBy = 5
THEN p.[Name] END ASC,
CASE WHEN #OrderBy = 10
THEN wpv.Price END ASC,
CASE WHEN #OrderBy = 15
THEN wpv.Price END DESC,
CASE WHEN #OrderBy = 20
THEN wpv.Price END DESC,
CASE WHEN #OrderBy = 25
THEN wpv.UnitPrice END ASC
)),COUNT(*) OVER(PARTITION BY 1) as TotalRows, p.*, pcm.DisplayOrder, wpv.Price, wpv.UnitPrice FROM Nop_Product p
INNER JOIN Nop_Product_Category_Mapping pcm ON p.ProductID=pcm.ProductID
INNER JOIN Nop_ProductVariant pv ON p.ProductID = pv.ProductID
INNER JOIN Nop_ProductVariant_Warehouse_Mapping wpv ON pv.ProductVariantID = wpv.ProductVariantID
WHERE pcm.CategoryID = #CategoryId
AND (wpv.Published = 1 AND pv.Published = 1 AND p.Published = 1 AND p.Deleted = 0 AND pv.Deleted = 0 and wpv.Deleted = 0)
AND wpv.WarehouseID IN (select WarehouseID from Nop_WarehouseCombination where UserWarehouseCombinationID = #WarehouseCombinationID)
)
SELECT TOP (#PageSize) * FROM Paging PG
WHERE PG.rn > (#PageNumber * #PageSize) - #PageSize
SELECT #TotalRecords = COUNT(p.ProductId) FROM Nop_Product p
INNER JOIN Nop_Product_Category_Mapping pcm ON p.ProductID=pcm.ProductID
INNER JOIN Nop_ProductVariant pv ON p.ProductID = pv.ProductID
INNER JOIN Nop_ProductVariant_Warehouse_Mapping wpv ON pv.ProductVariantID = wpv.ProductVariantID
WHERE pcm.CategoryID = #CategoryId
AND (wpv.Published = 1 AND pv.Published = 1 AND p.Published = 1 AND p.Deleted = 0 AND pv.Deleted = 0 and wpv.Deleted = 0)
AND wpv.WarehouseID IN (select WarehouseID from Nop_WarehouseCombination where UserWarehouseCombinationID = #WarehouseCombinationID)
END
I think I understand your issue here. Have you considered that the Count could be done BEFORE the CTE
and then passed in as value to the CTE as a variable.
i.e, set the value for #TotalRecords up front, pass it in, and so the CTE will use this count rather than executing the count a second time?
Does this make sense, or have I missed your point here.
no problem friend, highly possible i missed a trick here. However without the schema and data its tricky to test what I am suggesting. In the absence of someone giving a better answer, I've put this test script with data together to demo what I am talking about. If this isn't what you want then no problem. If it is just plain missing the point again, then I'll take that on the chin.
Declare #pagesize as int
Declare #PageNumber as int
Declare #TotalRowsOutputParm as int
SET #pagesize = 3
SET #PageNumber = 2;
--create some test data
DECLARE #SomeData table
(
[ID] [int] IDENTITY(1,1) NOT NULL,
[SomeValue] [nchar](10) NULL
)
INSERT INTO #SomeData VALUES ('TEST1')
INSERT INTO #SomeData VALUES ('TEST2')
INSERT INTO #SomeData VALUES ('TEST3')
INSERT INTO #SomeData VALUES ('TEST4')
INSERT INTO #SomeData VALUES ('TEST5')
INSERT INTO #SomeData VALUES ('TEST6')
INSERT INTO #SomeData VALUES ('TEST7')
INSERT INTO #SomeData VALUES ('TEST8')
INSERT INTO #SomeData VALUES ('TEST9')
INSERT INTO #SomeData VALUES ('TEST10');
--Get total count of all rows
Set #TotalRowsOutputParm = (SELECT COUNT(SomeValue) FROM #SomeData p) ;
WITH Paging AS
(
SELECT rn = (ROW_NUMBER() OVER (ORDER BY SomeValue ASC)),
#TotalRowsOutputParm as TotalRows, p.*
FROM [SomeData] p
)
SELECT TOP (#PageSize) * FROM Paging PG
WHERE PG.rn > (#PageNumber * #PageSize) - #PageSize
PRINT #TotalRowsOutputParm
I don't think you can do it without running the query twice if you want to assign it to a variable
however, can't you just add another column and do something like this instead?
;WITH Paging AS (select *,ROW_NUMBER() OVER(ORDER BY name) AS rn FROM sysobjects)
SELECT (SELECT MAX(rn) FROM Paging) AS TotalRecords,* FROM Paging
WHERE rn < 10
Or in your case
SELECT TOP (#PageSize) *,(SELECT MAX(PG.rn) FROM Paging) AS TotalRecords
FROM Paging PG
WHERE PG.rn > (#PageNumber * #PageSize) - #PageSize
Then from the front end grab that column
In the end I decided just to use two different SQL statements, one for count, one for select.
The "COUNT(*) OVER(PARTITION BY 1) as TotalRows" actually was pretty expensive and it turned out much quicker to just use two different statements.
Thank you everyone who helped with this question.