Sql server for-each,or while row - sql-server

First I Used AdvantureWork2019 DB you can use it to see the same result that I have, if you want to test it and I'll add a script to Create The Discount Tables at the end and to create the view I used to create the ##tempTables, My question is:
I have 2 Temp tables the first one is This:
DiscID Desc DiscLimitID Limit DiscPercID Perc Qty DiscQtyID Allowed Allocated
1 NA 1 0.0000000 1 0.0200000 1.0000000 1 0.0000000 0.0000000
2 Cheap 2 50.0000000 2 0.0100000 4.0000000 2 1000.0000000 0.0000000
3 Moderate 3 200.0000000 3 0.0250000 3.0000000 3 5000.0000000 0.0000000
4 Expensive 4 1000.0000000 4 0.0500000 2.0000000 4 20000.0000000 5000.0000000
The second one :
SalesOrderID SalesOrderDetailID LineTotal OrderQty ODiscID
43659 1 2024.994000 1 0
43659 2 6074.982000 3 4
43659 3 2024.994000 1 0
43659 4 2039.994000 1 0
43659 5 2039.994000 1 0
43659 6 4079.988000 2 4
43659 7 2039.994000 1 0
43659 8 86.521200 3 0
43659 9 28.840400 1 1
43659 10 34.200000 6 1
I made a query to give me this result (sample) :
SOdID LineTotal DiscID Discount FinalTotal FinalAllocated
9 28.840400 1 0.576808 28.263592 0.576808
10 34.200000 1 0.684000 33.516000 1.260808
11 10.373000 1 0.207460 10.165540 1.468268
18 20.746000 1 0.414920 20.331080 1.883188
12 80.746000 2 0.807460 79.938540 0.807460
19 115.361600 2 1.153616 114.207984 1.961076
29 100.932500 2 1.009325 99.923175 2.970401
85 173.042400 2 1.730424 171.311976 4.700825
105 115.361600 2 1.153616 114.207984 5.854441
139 80.746000 2 0.807460 79.938540 6.661901
34 551.814600 3 13.795365 538.019235 13.795365
44 535.742400 3 13.393560 522.348840 27.188925
95 551.814600 3 13.795365 538.019235 40.984290
104 535.742400 3 13.393560 522.348840 54.377850
2 6074.982000 4 303.749100 5771.232900 303.749100
6 4079.988000 4 203.999400 3875.988600 507.748500
17 1429.408600 4 71.470430 1357.938170 579.218930
20 1445.189800 4 72.259490 1372.930310 651.478420
21 6074.982000 4 303.749100 5771.232900 955.227520
22 4049.988000 4 202.499400 3847.488600 1157.726920
24 1637.400000 4 81.870000 1555.530000 1239.596920
what I want to do now is to make a Cursor or While loop to stop the query from making Discount and adding to the FinalAllocated and replace it to 0
for Example if DiscID = 3 reached 5000(FinalAllocated) then stop the query from continue for DiscID 3 and Add 0 Discount to the rest of the Rows which contain DiscID = 3 then go to the next DiscID which is 4 and start again
Ex:
while (FinalAllocated < Allowed for that ID)
Contnue
Else 0
Here you'll find everything I used to test it if you like
From AdventureWork2019 DB:
Create View vSalesOrderAll As
select SOH.SalesOrderID,SOD.SalesOrderDetailID,SOD.LineTotal,SOH.OrderDate,SOD.OrderQty from sales.SalesOrderDetail as SOD
Inner Join [Sales].[SalesOrderHeader] AS SOH ON
SOD.SalesOrderID = SOH.SalesOrderID
Script for Discount Tables :
USE [AdventureWorks2019]
GO
/****** Object: Table [dbo].[DiscountClass] Script Date: 6/29/2020 12:29:43 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[DiscountClass](
[DiscountClassID] [int] IDENTITY(1,1) NOT NULL,
[Description] [nvarchar](50) NULL,
CONSTRAINT [PK_DiscountClass] PRIMARY KEY CLUSTERED
(
[DiscountClassID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object: Table [dbo].[DiscountLimit] Script Date: 6/29/2020 12:29:43 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[DiscountLimit](
[DiscountLimitID] [int] IDENTITY(1,1) NOT NULL,
[DiscountClassID] [int] NULL,
[Limit] [numeric](24, 7) NULL,
CONSTRAINT [PK_DiscountLimit] PRIMARY KEY CLUSTERED
(
[DiscountLimitID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object: Table [dbo].[DiscountPercentage] Script Date: 6/29/2020 12:29:43 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[DiscountPercentage](
[DiscountPercentageID] [int] IDENTITY(1,1) NOT NULL,
[DiscountLimitID] [int] NULL,
[Quantity] [numeric](24, 7) NULL,
[Percentage] [numeric](24, 7) NULL,
CONSTRAINT [PK_DiscountPercentage] PRIMARY KEY CLUSTERED
(
[DiscountPercentageID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object: Table [dbo].[DiscountQuota] Script Date: 6/29/2020 12:29:43 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[DiscountQuota](
[DiscountQuotaId] [int] IDENTITY(1,1) NOT NULL,
[DiscountClassId] [int] NULL,
[Allowed] [numeric](24, 7) NULL,
[Allocated] [numeric](24, 7) NULL,
CONSTRAINT [PK_DiscountQuota] PRIMARY KEY CLUSTERED
(
[DiscountQuotaId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
SET IDENTITY_INSERT [dbo].[DiscountClass] ON
INSERT [dbo].[DiscountClass] ([DiscountClassID], [Description]) VALUES (1, N'NA')
INSERT [dbo].[DiscountClass] ([DiscountClassID], [Description]) VALUES (2, N'Cheap')
INSERT [dbo].[DiscountClass] ([DiscountClassID], [Description]) VALUES (3, N'Moderate')
INSERT [dbo].[DiscountClass] ([DiscountClassID], [Description]) VALUES (4, N'Expensive')
SET IDENTITY_INSERT [dbo].[DiscountClass] OFF
GO
SET IDENTITY_INSERT [dbo].[DiscountLimit] ON
INSERT [dbo].[DiscountLimit] ([DiscountLimitID], [DiscountClassID], [Limit]) VALUES (1, 1, CAST(0.0000000 AS Numeric(24, 7)))
INSERT [dbo].[DiscountLimit] ([DiscountLimitID], [DiscountClassID], [Limit]) VALUES (2, 2, CAST(50.0000000 AS Numeric(24, 7)))
INSERT [dbo].[DiscountLimit] ([DiscountLimitID], [DiscountClassID], [Limit]) VALUES (3, 3, CAST(200.0000000 AS Numeric(24, 7)))
INSERT [dbo].[DiscountLimit] ([DiscountLimitID], [DiscountClassID], [Limit]) VALUES (4, 4, CAST(1000.0000000 AS Numeric(24, 7)))
SET IDENTITY_INSERT [dbo].[DiscountLimit] OFF
GO
SET IDENTITY_INSERT [dbo].[DiscountPercentage] ON
INSERT [dbo].[DiscountPercentage] ([DiscountPercentageID], [DiscountLimitID], [Quantity], [Percentage]) VALUES (1, 1, CAST(1.0000000 AS Numeric(24, 7)), CAST(0.0200000 AS Numeric(24, 7)))
INSERT [dbo].[DiscountPercentage] ([DiscountPercentageID], [DiscountLimitID], [Quantity], [Percentage]) VALUES (2, 2, CAST(4.0000000 AS Numeric(24, 7)), CAST(0.0100000 AS Numeric(24, 7)))
INSERT [dbo].[DiscountPercentage] ([DiscountPercentageID], [DiscountLimitID], [Quantity], [Percentage]) VALUES (3, 3, CAST(3.0000000 AS Numeric(24, 7)), CAST(0.0250000 AS Numeric(24, 7)))
INSERT [dbo].[DiscountPercentage] ([DiscountPercentageID], [DiscountLimitID], [Quantity], [Percentage]) VALUES (4, 4, CAST(2.0000000 AS Numeric(24, 7)), CAST(0.0500000 AS Numeric(24, 7)))
SET IDENTITY_INSERT [dbo].[DiscountPercentage] OFF
GO
SET IDENTITY_INSERT [dbo].[DiscountQuota] ON
INSERT [dbo].[DiscountQuota] ([DiscountQuotaId], [DiscountClassId], [Allowed], [Allocated]) VALUES (1, 1, CAST(0.0000000 AS Numeric(24, 7)), CAST(0.0000000 AS Numeric(24, 7)))
INSERT [dbo].[DiscountQuota] ([DiscountQuotaId], [DiscountClassId], [Allowed], [Allocated]) VALUES (2, 2, CAST(10000.0000000 AS Numeric(24, 7)), CAST(0.0000000 AS Numeric(24, 7)))
INSERT [dbo].[DiscountQuota] ([DiscountQuotaId], [DiscountClassId], [Allowed], [Allocated]) VALUES (3, 3, CAST(50000.0000000 AS Numeric(24, 7)), CAST(0.0000000 AS Numeric(24, 7)))
INSERT [dbo].[DiscountQuota] ([DiscountQuotaId], [DiscountClassId], [Allowed], [Allocated]) VALUES (4, 4, CAST(200000.0000000 AS Numeric(24, 7)), CAST(5000.0000000 AS Numeric(24, 7)))
SET IDENTITY_INSERT [dbo].[DiscountQuota] OFF
GO
Second View :
Create View vAllDiscounts As
select DS.DiscountClassID,DS.Description
,DL.DiscountLimitID,DL.Limit
,DP.DiscountPercentageID,DP.Percentage,DP.Quantity
,DQ.DiscountQuotaId,DQ.Allowed,DQ.Allocated
from DiscountClass DS Inner join
DiscountLimit DL ON DS.DiscountClassID = DL.DiscountClassID
INNER JOIN DiscountPercentage DP ON DL.DiscountLimitID = DP.DiscountLimitID
INNER JOIN DiscountQuota DQ ON DQ.DiscountClassId = DL.DiscountClassID
Temp Tables :
select *
INTO ##TempDiscount
from vAllDiscounts
select *
INTO ##TempOrederData
from vSalesOrderAll
The ODiscId
update TOD
SET TOD.ODiscID = (CASE WHEN TOD.LineTotal <= 49 AND TOD.OrderQty >= 1 THEN 1
WHEN TOD.LineTotal >= 50 AND TOD.LineTotal <= 199 AND TOD.OrderQty >= 4 THEN 2
WHEN TOD.LineTotal >= 200 AND TOD.LineTotal <= 999 AND TOD.OrderQty >= 3 THEN 3
WHEN TOD.LineTotal >= 1000 AND TOD.OrderQty >= 2 THEN 4
ELSE 0 END)
from ##TempOrederData AS TOD
This is the query that I've used to get my output:
WITH TotalDisc AS
(
SELECT TOD.SalesOrderID,TOD.SalesOrderDetailID AS SOdID
,TOD.LineTotal,TOD.OrderQty,TOD.OrderDate,AD.DiscountClassID AS DiscID
,Percentage ,(LineTotal*Percentage) AS Discount, AD.Allocated,AD.Allowed AS Allo
FROM ##TempOrederData TOD
INNER JOIN ##TempDiscount AD ON TOD.ODiscID = AD.DiscountClassID
), TotalAndDiscount AS
(
SELECT SalesOrderID,SOdID,LineTotal,OrderQty,OrderDate,DiscID,Discount,Allocated,LineTotal-Discount AS FinalTotal
FROM TotalDisc
)
SELECT SOdID,LineTotal,DiscID,Discount,FinalTotal,
sum(Discount) over ( Partition by DiscID order by SOdID) AS FinalAllocated
FROM TotalAndDiscount
EDIT :
I Added the Case Statement to stop it when it reach a limit
WITH TotalDisc AS
(
SELECT TOD.SalesOrderID,TOD.SalesOrderDetailID AS SOdID
,TOD.LineTotal,TOD.OrderQty,TOD.OrderDate,AD.DiscountClassID AS DiscID
,Percentage ,(LineTotal*Percentage) AS Discount, AD.Allocated AS Allc,AD.Allowed AS Allo
FROM ##TempOrederData TOD
INNER JOIN ##TempDiscount AD ON TOD.ODiscID = AD.DiscountClassID
), TotalAndDiscount AS
(
SELECT SalesOrderID,SOdID,LineTotal,OrderQty,OrderDate,DiscID,Discount,Allc,LineTotal-Discount AS FinalTotal
FROM TotalDisc
), FinalCalc AS
(
SELECT SOdID,LineTotal,DiscID,Discount,FinalTotal,
sum(Discount) over ( Partition by DiscID order by SOdID) AS FinalAllocated
FROM TotalAndDiscount
),TestAllo AS
(
Select *
,Allocated = SUM(CASE WHEN DiscID = 1 AND FinalAllocated < 5000 THEN FinalAllocated
WHEN DiscID = 2 AND FinalAllocated < 10000 THEN FinalAllocated
WHEN DiscID = 3 AND FinalAllocated < 20000 THEN FinalAllocated
WHEN DiscID = 4 AND FinalAllocated < 50000 THEN FinalAllocated
Else '0' END )
from FinalCalc
group by FinalCalc.SodID,FinalCalc.LineTotal,FinalCalc.DiscID,FinalCalc.Discount,FinalCalc.FinalTotal,FinalCalc.FinalAllocated
)
select * from TestAllo
where Allocated != 0
order by DiscID
Edit 2 :
Desired Output would be something like that
SOdID LineTotal DiscID Discount FinalTotal FinalAllocated Allocated
915 2144.112900 4 107.205645 2036.907255 48226.798765 48226.798765
916 27055.760424 4 1352.788021 25702.972403 49579.586786 49579.586786
918 8159.976000 4 407.998800 7751.977200 49987.585586 49987.585586
924 1749.588000 4 0 1749.588000 49987.585586 0.000000
928 1749.588000 4 0 1749.588000 49987.585586 0.000000
932 1749.588000 4 0 1749.588000 49987.585586 0.000000
934 2097.294500 4 0 2097.294500 49987.585586 0.000000
942 1258.376700 4 0 1258.376700 49987.585586 0.000000
is when Allocated reach the limit I want it to stop the discount and replace the next Discounts with 0 and to take The allowed from the discount table which would be something like that :
CASE WHEN DiscID = 1 AND FinalAllocated < (Select allowed from DiscountQuota where DiscId=1) THEN FinalAllocated
but I keep getting this error
Cannot perform an aggregate function on an expression containing an aggregate or a subquery.
Again what i'm trying to achieve is to make a Cursor or While statement to stop the sum when the (FinalAllocated) > Allowed for that ID from table ##TempDiscount
Thank you in Advance

Create the output fields in the temp tables to help with the calculations.
First calculate the FinalAllocated without any restrictions:
WITH FinalAllocatedCTE AS (
SELECT
SalesOrderID
, FinalAllocated
, SUM(Discount) OVER (PARTITION BY DiscID ORDER BY LineTotal ASC) AS Calc_FinalAllocated
FROM
##TempOrederData
)
UPDATE
FinalAllocatedCTE
SET
FinalAllocated = Calc_FinalAllocated
Then reset the records which are over the limits:
UPDATE
DST
SET
FinalAllocated= 0
FROM
##TempOrederData AS DST
INNER JOIN ##TempDiscount AS DISC
ON DST.DiscID = DISC.DiscID
AND DST.FinalAllocated > DISC.Limit
;
Now you can do whatever you want with the records having 0 in FinalAllocated:
DECLARE #MaxAllocated TABLE (DiscID INT, MaxAllocated DECIMAL...);
INSERT INTO MaxAllocated (DiscID, MaxAllocated)
SELECT DiscID, MAX(FinalAllocated) AS MaxAllocated FROM ##TempOrederData
;
UPDATE
DST
SET
DST.FinalAllocated= MA.MaxAllocated
, DST.Discount = 0
, DST.Allocated = 0
FROM
##TempOrederData AS DST
INNER JOIN #MaxAllocated AS MA
ON DST.DiscID = MA.DiscID
WHERE
TotalAllocated = 0
;

Related

Selecting records for deletions based on relationship with previous and next records

I have a SQL Server 2014 table with millions of gps coordinates, each at a particular time. However the interval between the registrations is not fixed and varies from 1 second to a couple of hours. I only want to keep one measurement every 4 minutes, so the other records have to be deleted.
I tried a WHILE loop in T-SQL that traverses every record, with inside the loop a select statement with a double CROSS APPLY to only return a record if it sits in beween 2 other records which are not more than 4 minutes apart. However this strategy turns out to be too slow.
Can this be done with a set-based solution ? Or is there a way to speed-up this query ? (the test query below is just printing, not yet deleting)
SELECT * INTO #myTemp FROM gps ORDER BY TimePoint asc
declare #Id Uniqueidentifier
declare #d1 varchar(19)
declare #d2 varchar(19)
declare #d3 varchar(19)
While EXISTS (select * from #myTemp )
BEGIN
select top 1 #Id = ID FROM #myTemp order by TimePoint asc
SELECT
#d1 = convert(varchar(19), a.justbefore, 121),
#d2 = convert(varchar(19), b.tijdstip, 121),
#d3 = convert(varchar(19), c.justafter, 121)
FROM Gps B CROSS APPLY
(
SELECT top 1 TimePoint as justbefore
FROM Gps
WHERE (B.TimePoint > TimePoint ) AND (B.Id = #Id )
ORDER by TimePoint desc
) A
CROSS APPLY (
SELECT top 1 TimePoint as justafter
FROM Gps
WHERE (Datediff(n,A.justbefore,TimePoint ) between -4 AND 0)
AND (B.TimePoint < TimePoint )
ORDER by TimePoint asc
) C
print 'ID=' + Cast(#id as varchar(50))
+ ' / d1=' + #d1 + ' / d2=' + #d2 + ' / d3=' + #d3
DELETE #myTemp where Id = #id
END
--
Sample data:
Id TimePoint Lat Lon
1 20170725 13:05:27 12,256 24,123
2 20170725 13:10:27 12,254 24,120
3 20170725 13:10:29 12,253 24,125
4 20170725 13:11:55 12,259 24,127
5 20170725 13:11:59 12,255 24,123
6 20170725 13:14:28 12,254 24,126
7 20170725 13:16:52 12,259 24,121
8 20170725 13:20:53 12,257 24,125
In this case records 3,4,5 should be deleted.
Record 7 should stay as the gap between 7 and 8 is longer than 4 minutes.
Looking at the numbers... It looks like 1 & 2 stay (5 mins apart)...3, 4, & 5 should go... 6 stays (4 mins from 2)... 7 should go (only 2 mins from 6) and 8 stays (6 mins from 6)...
If this is correct, the following will do what you're looking for...
IF OBJECT_ID('tempdb..#TestData', 'U') IS NOT NULL
DROP TABLE #TestData;
CREATE TABLE #TestData (
Id INT NOT NULL PRIMARY KEY CLUSTERED,
TimePoint DATETIME2(0) NOT NULL,
Lat DECIMAL(9,3),
Lon DECIMAL(9,3)
);
INSERT #TestData (Id, TimePoint, Lat, Lon) VALUES
(1, '20170725 13:05:27', 12.256, 24.123),
(2, '20170725 13:10:27', 12.254, 24.120),
(3, '20170725 13:10:29', 12.253, 24.125),
(4, '20170725 13:11:55', 12.259, 24.127),
(5, '20170725 13:11:59', 12.255, 24.123),
(6, '20170725 13:14:28', 12.254, 24.126),
(7, '20170725 13:16:52', 12.259, 24.121),
(8, '20170725 13:20:53', 12.257, 24.125);
-- SELECT * FROM #TestData td;
--================================================================================
WITH
cte_AddLag AS (
SELECT
td.Id, td.TimePoint, td.Lat, td.Lon,
MinFromPrev = DATEDIFF(mi, LAG(td.TimePoint, 1) OVER (ORDER BY td.TimePoint), td.TimePoint)
FROM
#TestData td
),
cte_TimeGroup AS (
SELECT
*,
TimeGroup = ISNULL(SUM(al.MinFromPrev) OVER (ORDER BY al.TimePoint ROWS UNBOUNDED PRECEDING) / 4, 0)
FROM
cte_AddLag al
)
SELECT TOP 1 WITH TIES
tg.Id,
tg.TimePoint,
tg.Lat,
tg.Lon
FROM
cte_TimeGroup tg
ORDER BY
ROW_NUMBER() OVER (PARTITION BY tg.TimeGroup ORDER BY tg.TimePoint);
Results...
Id TimePoint Lat Lon
----------- --------------------------- --------------------------------------- ---------------------------------------
1 2017-07-25 13:05:27 12.256 24.123
2 2017-07-25 13:10:27 12.254 24.120
6 2017-07-25 13:14:28 12.254 24.126
8 2017-07-25 13:20:53 12.257 24.125
HTH, Jason

Link Age to AgeGroup Table

I have a client table that has a calculated column for Age.
CREATE TABLE [dbo].[Client]
(
[ClientID] [int] IDENTITY(1,1) NOT NULL,
[ClientName] varchar(50) NOT NULL,
[DOB] [date] NOT NULL,
[Age] AS (datediff(year,[DOB],getdate())-case when dateadd(year,datediff(year,[DOB],getdate()),[DOB])>getdate() then (1) else (0) end),
CONSTRAINT [PK_Client] PRIMARY KEY CLUSTERED
(
[ClientID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
)
I then having an AgeGroup table that has the following rows.
AgeGroupID AgeGroup StartRange EndRange
1 65 & over 65 255
2 55-64 55 64
3 45-54 45 54
4 35-44 35 44
5 25-34 25 34
6 19-24 19 24
7 13-18 13 18
8 Below 12 0 12
How could I link Client to AgeGroup to show a clients AgeGroup in a select query ?
With the output being returned such as the following
ClientID ClientName Age AgeGroup
1 Peter Smith 25 25-34
Maybe, something like this:
SELECT c.ClientID
,c.ClientName
,c.Age
,ag.AgeGroup
FROM client c
LEFT JOIN AgeGroup ag
ON c.age BETWEEN ag.StartRange AND ag.EndRange

SQL Server locking on non clustered index

I'm in a big trouble with one database instance of a MS SQL Server 2008 R2! I have the structure bellow that I've created it to simulate my real problem.
USE [sor]
GO
ALTER DATABASE [sor] SET ALLOW_SNAPSHOT_ISOLATION on;
ALTER DATABASE [sor] SET READ_COMMITTED_SNAPSHOT on;
CREATE TABLE [dbo].[test] (
[name] [nvarchar](50) NOT NULL,
[id] [int] NOT NULL,
CONSTRAINT [PK_test] PRIMARY KEY CLUSTERED ([id] 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
CREATE NONCLUSTERED INDEX [IX_test] ON [dbo].[test] (
[name] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
ALTER TABLE [dbo].[test] ADD CONSTRAINT [PK_test] PRIMARY KEY CLUSTERED (
[id] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
CREATE UNIQUE NONCLUSTERED INDEX [pk_key] ON [dbo].[test] (
[id] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
My problem is that when I execute two or more sessions (transactions) concurrently I'm being locked by some database indexes. To simulate it I start one sqlcmd:
-- sqlcmd 1
begin tran s1
insert into dbo.test (id, name) values(1, 'bob');
go
and start another sqlcmd to be the next concurrent transaction:
-- sqlcmd 2
begin tran s2
insert into dbo.test (id, name) values(2, 'john');
go -- locking here!!!!
when I run the go inside the transaction named s2 I'm being locked.
In order to discover what is going on I run
SELECT * FROM sys.dm_exec_requests where DB_NAME(database_id)='sor' and blocking_session_id <>0
session_id request_id start_time status command sql_handle statement_start_offset statement_end_offset plan_handle database_id user_id connection_id blocking_session_id wait_type wait_time last_wait_type wait_resource open_transaction_count open_resultset_count transaction_id context_info percent_complete estimated_completion_time cpu_time total_elapsed_time scheduler_id task_address reads writes logical_reads text_size date_first quoted_identifier arithabort ansi_null_dflt_on ansi_defaults ansi_warnings ansi_padding ansi_nulls concat_null_yields_null transaction_isolation_level lock_timeout deadlock_priority row_count prev_error nest_level granted_query_memory executing_managed_code group_id query_hash query_plan_hash
---------- ----------- ----------------------- -------------- ---------------- --------------------------------------------------------------- ---------------------- -------------------- --------------------------------------------------- ----------- ----------- ------------------------------------ ------------------- ----------- ----------- ---------------- ------------------------------------------ ---------------------- -------------------- -------------------- -------------- ---------------- ------------------------- ----------- ------------------ ------------ ------------------ -------------------- -------- -------------- ----------- ---------- ----------------- ---------- ----------------- ------------- ------------- ------------ ---------- ----------------------- --------------------------- ------------ ----------------- -------------------- ----------- ----------- -------------------- ---------------------- ----------- ------------------ ------------------
74 0 2014-10-01 10:56:54.143 suspended INSERT 0x0200000098A4BD0B49707895A7295A343EF775E7CF23BCF2 50 -1 0x0600170098A4BD0B4001C69D000000000000000000000000 23 1 E24BB9F3-D12A-44EC-944E-FDA206590705 73 LCK_M_X 1762594 LCK_M_X KEY: 23:72057594038910976 (020068e8b274) 3 1 243137590 0x 0 0 0 1762596 1 0x000000018281E2C8 0 0 4 4096 7 0 0 1 0 1 1 1 1 2 -1 0 0 0 0 0 0 2 0xF22D2FE93F4C59F0 0x354B4D3AA833139F
and running
SELECT t1.resource_type, t1.resource_database_id, t1.resource_associated_entity_id, t1.request_mode, t1.request_session_id, t2.blocking_session_id, o1.name 'object name', o1.type_desc 'object descr', p1.partition_id 'partition id', p1.rows 'partition/page rows', a1.type_desc 'index descr', a1.container_id 'index/page container_id' FROM sys.dm_tran_locks as t1 INNER JOIN sys.dm_os_waiting_tasks as t2 ON t1.lock_owner_address = t2.resource_address LEFT OUTER JOIN sys.objects o1 on o1.object_id = t1.resource_associated_entity_id LEFT OUTER JOIN sys.partitions p1 on p1.hobt_id = t1.resource_associated_entity_id LEFT OUTER JOIN sys.allocation_units a1 on a1.allocation_unit_id = t1.resource_associated_entity_id
I have the following
resource_type resource_database_id resource_associated_entity_id request_mode request_session_id blocking_session_id object name object descr partition id partition/page rows index descr index/page container_id
---------------- -------------------- ----------------------------- -------------- ------------------ ------------------- ------------- ------------- ------------- -------------------- -------------- -----------------------
KEY 23 72057594038910976 X 74 73 NULL NULL NULL NULL IN_ROW_DATA 72057594038255616
Once I use READ COMMITED SNAPSHOT I ask you why the sql server is locking the index? what can I do? I cannot delete the indexes! I need them.
There is quite a lot of information out there, but the answer by Martin Smith to this question might shed some light:
Why insert TSQL statement block when transaction isolation level for another transaction is serializable with non-conflicting filter?
It looks like with locking enabled and with potentially only 0 or 1 record in your table then effectively the whole table is locked.
For the real life problem are you how many entry's are in the table you're inserting into? Also is there additional code that that causes the lock to be held for a lot longer than it could be?

CTE - recursively update quantity until total consumed

I've been researching CTEs trying to determine if it's possible to recursively update inventory quantity records with an order quantity until the order quantity is consumed.
Here are the tables and records:
CREATE TABLE [dbo].[myOrder](
[Account] [float] NOT NULL,
[Item] [float] NOT NULL,
[Quantity] [float] NOT NULL
) ON [PRIMARY]
insert into dbo.myOrder values (12345, 1, 50)
CREATE TABLE [dbo].[myInventory](
[ID] [int] IDENTITY(1,1) NOT NULL,
[Account] [float] NOT NULL,
[InvDate] [numeric](18, 0) NOT NULL,
[Item] [float] NOT NULL,
[Quantity] [float] NOT NULL,
[QuantitySold] [float] NOT NULL
) ON [PRIMARY]
insert into dbo.myInventory values (12345, 111287, 1, 45, 40)
insert into dbo.myInventory values (12345, 111290, 1, 40, 0)
insert into dbo.myInventory values (12345, 111290, 1, 12, 0)
insert into dbo.myInventory values (12345, 111291, 1, 25, 0)
The record in the myOrder table indicates that an order is to be created for account 12345 for item #1, quantity 50:
Account Item Quantity
------- ---- --------
12345 1 50
The inventory table shows that we have plenty of item #1 on hand for account 12345:
ID Account InvDate Item Quantity QuantitySold
-- ------- ------- ---- -------- ------------
1 12345 111287 1 45 40
2 12345 111290 1 40 0
3 12345 111290 1 12 0
4 12345 111291 1 25 0
The goal is to start plugging in the order quantity of 50 into the inventory records until all 50 are consumed. Inventory records are ordered by the value in the InvDate column. Record 1 has 5 remaining quantity (45 - 40 = 5), which would leave us with 45 more to consume for the order. Record 2 can consume 40. Record 3 can consume the last 5. When the query completes the inventory records would look like this:
ID Account InvDate Item Quantity QuantitySold
-- ------- ------- ---- -------- ------------
1 12345 111287 1 45 45
2 12345 111290 1 40 40
3 12345 111290 1 12 5
4 12345 111291 1 25 0
Note: The inventory table stores QuantitySold, not QuantityRemaining, so you have to do the math (Quantity - QuantitySold) to determine how much quantity remains per inventory record.
I've gotten almost nowhere with the CTE. I've found plenty of examples for doing selects where you have 2 parts to your CTE - an initialization part and the recursive part UNIONed together. I could write this with a cursor, but I think it's possible to do with a CTE and I'd like to learn how.
If anyone can confirm this is possible with a CTE or explain how to set up the CTE, I'd appreciate it. Thanks!
--#inserted table mimics inserted virtual table from AFTER INSERT triggers on [dbo].[myOrder] table
DECLARE #inserted TABLE
(
[Account] [float] NOT NULL,
[Item] [float] NOT NULL,
[Quantity] [float] NOT NULL
);
INSERT #inserted
VALUES (12345, 1, 50);
WITH CteRowNumber
AS
(
SELECT inv.ID
,inv.Account
,inv.Item
,inv.Quantity
,inv.QuantitySold
,i.Quantity QuantityOrdered
,ROW_NUMBER() OVER(PARTITION BY inv.Account,inv.Item ORDER BY inv.ID ASC) RowNumber
FROM myInventory inv
INNER JOIN #inserted i ON inv.Account = i.Account
AND inv.Item = i.Item
WHERE inv.Quantity > inv.QuantitySold
), CteRecursive
AS
(
SELECT a.ID
,a.Account
,a.Item
,a.RowNumber
,CASE
WHEN a.Quantity - a.QuantitySold < a.QuantityOrdered THEN a.Quantity - a.QuantitySold
ELSE a.QuantityOrdered
END QuantitySoldNew
,CASE
WHEN a.Quantity - a.QuantitySold < a.QuantityOrdered THEN a.Quantity - a.QuantitySold
ELSE a.QuantityOrdered
END RunningTotal
FROM CteRowNumber a
WHERE a.RowNumber = 1
UNION ALL
SELECT crt.ID
,crt.Account
,crt.Item
,crt.RowNumber
,CASE
WHEN prev.RunningTotal + (crt.Quantity - crt.QuantitySold) < crt.QuantityOrdered THEN crt.Quantity - crt.QuantitySold
ELSE crt.QuantityOrdered - prev.RunningTotal
END QuantitySoldNew
,CASE
WHEN prev.RunningTotal + (crt.Quantity - crt.QuantitySold) < crt.QuantityOrdered THEN prev.RunningTotal + (crt.Quantity - crt.QuantitySold)
ELSE crt.QuantityOrdered
END RunningTotal
FROM CteRecursive prev
INNER JOIN CteRowNumber crt ON prev.Account = crt.Account
AND prev.Item = crt.Item
AND prev.RowNumber + 1 = crt.RowNumber
WHERE prev.RunningTotal < crt.QuantityOrdered
)
SELECT cte.ID
,cte.Account
,cte.Item
,cte.QuantitySoldNew
FROM CteRecursive cte;
--or CteRecursive can be used to update QuantitySold column from [dbo].[myInventory] table
--UPDATE myInventory
--SET QuantitySold = inv.QuantitySold + cte.QuantitySoldNew
--FROM myInventory inv
--INNER JOIN CteRecursive cte ON inv.ID = cte.ID;

Identify data inconsistency in a Tuple-like table

I have a table that holds availability status for workers. Here is the structure:
CREATE TABLE [dbo].[Availability]
(
[OID] BIGINT IDENTITY (1, 1) NOT NULL,
[LocumID] BIGINT NOT NULL,
[AvailableDate] SMALLDATETIME NOT NULL,
[AvailabilityStatusID] INT NOT NULL,
[LastModifiedAt] TIMESTAMP NOT NULL,
CONSTRAINT [PK_Availability] PRIMARY KEY CLUSTERED ([OID] ASC) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY];
And here is a result:
OID LocumID AvailableDate AvailabilityStatusID LastModifiedAt
-------------------- -------------------- ----------------------- -------------------- ------------------
1 1 2009-03-02 00:00:00 1 0x0000000000201A8C
2 2 2009-03-04 00:00:00 1 0x0000000000201A8D
3 1 2009-03-05 00:00:00 1 0x0000000000201A8E
4 1 2009-03-06 00:00:00 1 0x0000000000201A8F
5 2 2009-03-07 00:00:00 1 0x0000000000201A90
6 7 2009-03-09 00:00:00 1 0x0000000000201A91
7 1 2009-03-11 00:00:00 1 0x0000000000201A92
8 1 2009-03-12 00:00:00 2 0x0000000000201A93
9 1 2009-03-14 00:00:00 1 0x0000000000201A94
10 1 2009-03-16 00:00:00 1 0x0000000000201A95
Now, the table has over 3mil record and I noticed that there are inconsistencies in my data. I need to somehow find rows where for any [AvailableDate], the [LocumID] (regardless of how many,) must be unique. So, basically, a worker can have one of these [AvailabilityStatusID] = 1, 2, 3, or 4 on one date. However, in this table, there are errors where a worker is entered twice or more against a [AvailableDate] with same [AvailabilityStatusID] or different [AvailabilityStatusID]
How can I detect these records?
Regards.
WITH x AS
(
SELECT LocumID, dt = AvailableDate
FROM dbo.Availability
GROUP BY LocumID, AvailableDate
HAVING COUNT(*) > 1
)
SELECT a.OID, a.LocumID, a.AvailableDate,
a.AvailabilityStatusID, a.LastModifiedAt
FROM x
INNER JOIN dbo.Availability AS a
ON x.LocumID = a.LocumID
AND x.dt = a.AvailableDate
ORDER BY a.LocumID, a.AvailableDate;
Once you clean this data up (not sure what your rule will be regarding which rows to keep), you should consider a unique constraint on (LocumID, AvailableDate). Here is how you would create the constraint (though you will not be able to create it until you have removed the duplicates):
ALTER TABLE dbo.Availability
ADD CONSTRAINT uq_l_ad
UNIQUE (LocumID, AvailableDate);
Of course now you will have new errors returned to your application (Msg 2627), since your current code clearly doesn't already check if a LocumID/AvailabilityDate combination already exists before adding a new one.

Resources