SQL Server Spatial approach to merging sets of overlapping polygons - sql-server

We have a need to merge sets of overlapping geometry polygons. Usually easy enough in desktop GIS using Dissolve or similar functions. However we have >10 million polygons to test and desktop GIS is not coping - not a surprise.
So I'm attempting this in SQL Server which I've used for intersection analysis for intersection analysis of similar numbers of polygons and lines, but I'm stumped on this.
With the following test data of 9 squares I would expect a result of 5 merged squares, 2 merged squares and two single adjacent squares unmerged.
CREATE TABLE [dbo].[TestPolygons](
[OBJECTID] [int] IDENTITY(1,1) NOT NULL,
[GeomPoly] [geometry] NULL,
CONSTRAINT [PK_TestPolygons] PRIMARY KEY CLUSTERED
(
[OBJECTID] 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] TEXTIMAGE_ON [PRIMARY]
GO
-- Overlapping pair
insert into dbo.TestPolygons (GeomPoly) values (geometry::STGeomFromText('POLYGON ((0 0, 40 0, 40 40, 0 40, 0 0))',27700));
insert into dbo.TestPolygons (GeomPoly) values (geometry::STGeomFromText('POLYGON ((30 30, 70 30, 70 70, 30 70, 30 30))',27700));
-- no overlapping, just adjacent
insert into dbo.TestPolygons (GeomPoly) values (geometry::STGeomFromText('POLYGON ((120 0, 160 0, 160 40, 120 40, 120 0))',27700));
insert into dbo.TestPolygons (GeomPoly) values (geometry::STGeomFromText('POLYGON ((160 40, 200 40, 200 80, 160 80, 160 40))',27700));
-- additional overlaps
insert into dbo.TestPolygons (GeomPoly) values (geometry::STGeomFromText('POLYGON ((60 60, 100 60, 100 100, 60 100, 60 60))',27700));
insert into dbo.TestPolygons (GeomPoly) values (geometry::STGeomFromText('POLYGON ((90 90, 130 90, 130 130, 90 130, 90 90))',27700));
insert into dbo.TestPolygons (GeomPoly) values (geometry::STGeomFromText('POLYGON ((60 0, 100 0, 100 40, 60 40, 60 0))',27700));
-- overlapping pair
insert into dbo.TestPolygons (GeomPoly) values (geometry::STGeomFromText('POLYGON ((220 0, 260 0, 260 40, 220 40, 220 0))',27700));
insert into dbo.TestPolygons (GeomPoly) values (geometry::STGeomFromText('POLYGON ((250 30, 290 30, 290 70, 250 70, 250 30))',27700));
Test Data
I've got as far as merging everything into one record of two polygons, but this has dropped the unmerged records. This is the code;
select geometry::UnionAggregate(inter_geometry) as intersection_union
from
(
select P.objectid as id1, T.objectid as id2, P.GeomPoly.STUnion(T.GeomPoly) as inter_geometry
from dbo.TestPolygons P
inner join dbo.TestPolygons T on P.objectid < T.objectid
where P.GeomPoly.STOverlaps(T.GeomPoly) = 1
) N
This is what the result looks like.
Hopefully someone can help - I've found StackOverflow very useful in the past, but this is my first question.
Adrian

The result I'm after for the test data is four records; 1 record of a merged set of the five squares that serially overlap, 1 record of a merged set of the two overlapping squares, 2 records each of a single square.
Cool. UnionAggregate does all the hard work for you knitting the shapes together into a single multipolygon. Then you just have to enumerate the geometries that make it up, something like this:
with q as
(
select geometry::UnionAggregate(GeomPoly) g
from TestPolygons
), n as
(
select top 10000 row_number() over (order by (select null)) i
from (values (1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) v1(i)
)
select q.g.STGeometryN(n.i)
from q
cross apply (select i from n where i <= q.g.STNumGeometries()) n
which outputs four rows:

Related

Query to get ids of entries which appear in arbitrary amount different lists

id
category_id
product_id
status
13
93
2137
1
14
94
2137
1
15
93
2138
2
16
94
2138
2
17
87
2128
1
18
94
2128
1
19
87
2139
2
20
94
2139
2
21
88
2132
1
22
93
2132
1
23
88
2140
2
24
93
2140
2
25
87
2137
1
26
87
2141
2
27
93
2136
1
28
93
2137
1
29
88
2134
1
30
88
2143
2
I have this kind of data presented to me. For my query I'm given a list of category ids.
Let's say I'm given three lists with
1. {93, 94}
2. {88, 87, 86}
3. {93}
Now I would need a query, which would give me product ids, which appear at least once in ALL of those lists and for which the status is 1. So for the example query the result should be:
product_id
2137
The first step in any solution is to normalize the selection criteria data into a table of the form { category_group_id, category_id } with only one category_id for row. There are several ways to do this but I've used the relatively new STRING_SPLIT function here (same as Luis LL). This normalized criteria may be loaded into a temp table or included as a Common Table Expression (CTE) as is done below.
Once the criteria is normalized, the real problem can be solved by (1) filtering the input data by status, (2) joining it with the normalized selection criteria from above, (3) grouping by product ID, and then (4) counting the number of distinct category group IDs matched. If that count matches the total number of category group IDs (three for the sample data), we have a match.
;WITH NormalizedCategoryIds AS (
SELECT C.category_group_id, CAST(S.Value AS INT) AS category_id
FROM CategoryIds C
CROSS APPLY STRING_SPLIT(
REPLACE(REPLACE(category_id_list, '{', ''), '}', ''),
',') S
)
SELECT D.product_id
FROM SampleData D
JOIN NormalizedCategoryIds C on C.category_id = D.category_id
WHERE D.status = 1
GROUP BY D.product_id
HAVING COUNT(DISTINCT C.category_group_id) = (SELECT COUNT(*) FROM CategoryIds)
If we started with criteria that was already normalized, the HAVING clause could be changed to:
HAVING COUNT(DISTINCT C.category_group_id)
= (SELECT COUNT(DISTINCT C2.category_group_id) FROM NormalizedCategoryIds C2)
That value could also be calculated ahead of the query.
Sample results:
product_id
2132
2137
Even though not in the original posted results, 2132 is also included here, because it matches all three category groups. The 93 row matches category groups 1 and 3 and the 88 record matches category group 2.
See this db<>fiddle for a working demo including some extra test data.
This should work for SQL Server 2016 and above.
CREATE TABLE table1
(
id INT,
category_id INT,
product_id INT,
status INT
)
INSERT INTO table1
(id, category_id, product_id, status)
VALUES
( 13, 93, 2137, 1)
,( 14, 94, 2137, 1)
,( 15, 93, 2138, 2)
,( 16, 94, 2138, 2)
,( 17, 87, 2128, 1)
,( 18, 94, 2128, 1)
,( 19, 87, 2139, 2)
,( 20, 94, 2139, 2)
,( 21, 88, 2132, 1)
,( 22, 93, 2132, 1)
,( 23, 88, 2140, 2)
,( 24, 93, 2140, 2)
,( 25, 87, 2137, 1)
,( 26, 87, 2141, 2)
,( 27, 93, 2136, 1)
,( 28, 93, 2137, 1)
,( 29, 88, 2134, 1)
,( 30, 88, 2143, 2)
CREATE TABLE Input
(IdLst varchar(100))
INSERT INTO Input (IdLst)
VALUES
('{93, 94}')
,('{88, 87, 86}')
,('{93}')
;WITH Categories AS (
SELECT CONVERT(INT, Value ) category_id
FROM Input
CROSS APPLY STRING_SPLIT(REPLACE(REPLACE( IdLst, '{', ''), '}', ''), ',')
)
SELECT product_id
FROM Categories
INNER JOIN table1 ON table1.category_id = Categories.category_id
GROUP BY product_id
HAVING COUNT(1) = (SELECT COUNT(1) cntCategories FROM Categories )

Sql server for-each,or while row

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
;

Filling missing records with previous existing records

I have an existing database where some logic is made by the front end application.
Now I have to make reports from that database and I'm facing to a proble of missing records which are covered on a record basis in the frontend but have issues in the report
Given the following tables:
create table #T (id int, id1 int, label varchar(50))
create table #T1 (id int, T_id1 int, A int, B int, C int)
With the values:
insert into #T values (10, 1, 'label1'), (10, 2, 'label2'), (10, 3, 'label3'), (10, 15, 'label15'), (10, 16, 'label16'), (20, 100, 'label100'), (20, 101, 'label101')
insert into #T1 values (10, 1, 100, 200, 300), (10, 15, 150, 250, 350), (20, 100, 151, 251, 351), (20, 101, 151, 251, 351)
if I make a report we can see some missing records:
select #T.id, #T.id1, #T1.A, #T1.B, #T1.C
from #T left join #T1 on #T.id1 = #T1.T_id1
result:
id id1 A B C
10 1 100 200 300
10 2 NULL NULL NULL
10 3 NULL NULL NULL
10 15 150 250 350
10 16 NULL NULL NULL
20 100 151 251 351
20 101 151 251 351
Expected result would be:
id id1 A B B
10 1 100 200 300
10 2 100 200 300
10 3 100 200 300
10 15 150 250 350
10 16 150 250 350
20 100 151 251 351
20 101 151 251 351
As you can see here the missing data is filled out of the the first (in id, id1 order) previous existing record for a given id. For a given id there can be any number of "missing" records and for the given id there can be any number of existing records after a not existing ones.
I can do this with a cursor but I'm looking for a solution without cursor
You can use subquery (to find groups with same values) + window function
WITH Grouped AS (
SELECT #T.id, #T.id1, #T1.A, #T1.B, #T1.C,
GroupN = SUM(CASE WHEN #T1.A IS NULL THEN 0 ELSE 1 END) OVER(/* PARTITION BY id ? */ ORDER BY id1 ROWS UNBOUNDED PRECEDING)
FROM #T
LEFT JOIN #T1 ON #T.id1 = #T1.T_id1
)
SELECT Grouped.id, Grouped.id1,
A = MAX(A) OVER(PARTITION BY GroupN),
B = MAX(B) OVER(PARTITION BY GroupN),
C = MAX(C) OVER(PARTITION BY GroupN)
FROM Grouped
You can use below sql for thedesired output:
with cte (id, id1, A, B, C)
as
(
select #T.id, #T.id1, #T1.A, #T1.B, #T1.C
from #T left join #T1 on #T.id1 = #T1.T_id1
)
select cte.id, cte.id1,
coalesce(cte.A,TT.A) A,
coalesce(cte.B,TT.B) B,
coalesce(cte.C,TT.C) C
from cte
left join
(
select p.id1,max(q.id1) id1_max
from cte p
inner join cte q on p.id1 > q.id1 and p.a is null and q.a is not null
group by p.id1
) T on cte.id1 = T.id1
left join cte TT on T.id1_max = TT.id1

Pivot Tables - using more than one column

With Pivot Tables is it possible to base your columns on two values (in my example 'code' and 'val')? By the looks of it you can't, but that seems a bit of a limitation to me, so perhaps I've just misunderstood something. For example if my data table looks like this:
code val total
---- --- -----
SI 12 90
SI 12 30
SI 24 240
CI 12 210
and the output I desire is this:
SI12 SI24 CI12
---- ---- ----
120 240 210
I'd appreciate it if someone could show me how this could be achieved whether the solution is to use Pivot Tables or something else?
Try this,
DECLARE #mytable TABLE
(
code VARCHAR(2),
val INT,
total INT
)
INSERT INTO #mytable
VALUES ('SI',
12,
90),
('SI',
12,
30),
('SI',
24,
240),
('CI',
12,
210)
SELECT *
FROM #mytable
SELECT *
FROM (SELECT code + CONVERT(VARCHAR(2), VAL) VAL,
total
FROM #mytable) T
PIVOT(sum(total)
FOR VAL IN ([SI12],
[SI24],
[CI12])) AS PIVOTTABLE

Creating blocks within a CTE - SQL Server

I am trying to work out how I can tag unique (what i am calling) blocks (or segments if you will) which have a start and end based consecutive 'Trip' rows ordered by 'epoch' sharing the same 'code'. In this case group by 'trip', 'code' will not work as I need to measure the duration of the 'code' remains constant for the trip. I've tried to use a CTE but I have been unable to partition the data in such a way that it gives desired result shown below. The block number I've shown could be any value, just so long as it is unique so that it tags the consecutive occurrences of the same 'code' on the trip in order of 'epoch'.
Any ideas?
declare #data table (id int, trip int, code int NULL, epoch int, value1 int, value2 int);
insert into #data (id, trip, code, epoch, value1, value2)
values
(1, 1, null, 31631613, 0, 0),
(2, 2, 1, 31631614, 10, 40),
(3, 1, 1, 31631616, 10, 60),
(4, 1, 1, 31631617, 40, 60),
(5, 2, 1, 31631617, 23, 40),
(6, 2, 2, 31631620, 27, 40),
(7, 2, 2, 31631629, 23, 40),
(9, 1, 1, 31631618, 39, 60),
(10, 1, null, 31631621, 38, 60),
(12, 1, null, 31631625, 37, 60),
(15, 1, null, 31631627, 35, 60),
(19, 1, 1, 31631630, 39, 60),
(20, 1, 1, 31631632, 40, 60),
(21, 2, 1, 31631629, 23, 40);
block id trip code epoch value1 value2
1 1 1 NULL 31631613 0 0
2 2 2 1 31631614 10 40
2 5 2 1 31631617 23 40
3 3 1 1 31631616 10 60
3 4 1 1 31631617 40 60
3 9 1 1 31631618 39 60
4 6 2 2 31631620 27 40
4 7 2 2 31631629 23 40
5 10 1 NULL 31631621 38 60
5 12 1 NULL 31631625 37 60
5 15 1 NULL 31631627 35 60
6 19 1 1 31631630 39 60
6 20 1 1 31631632 40 60
7 21 2 1 31631629 23 40
You didn't update your expected output so I'm still not 100% sure this is what you want, but give it a try...
SELECT
DENSE_RANK() OVER (ORDER BY trip, code),
*
FROM
#data
ORDER BY
trip, code, epoch
Ok, it's far from perfect by any means but it is a starter that at least identifies the start and end of a contiguous block where the 'code' has remained the same for the trip. For the sake of at least contributing something I'll post what I jerried up. If I ever get time to do a proper job I'll post it.
declare #minint int; set #minint = -2147483648;
declare #maxint int; set #maxint = 2147483647;
declare #id_data table (pk int IDENTITY(1,1), id int, trip int, code int NULL, epoch int, value1 int, value2 int);
insert into #id_data VALUES(#minint, #minint, #minint, #minint, #minint, #minint);
insert into #id_data
SELECT id, trip, coalesce(code,0), epoch, value1, value2
FROM #data
order by trip, epoch, code;
insert into #id_data VALUES(#maxint, #maxint, #maxint, #maxint, #maxint, #maxint);
WITH CTE as
(
SELECT pk, id, trip, code, epoch, value1, value2, ROW_NUMBER() OVER (PARTITION BY trip ORDER BY epoch) as row_num
FROM #id_data
)
SELECT B.*, A.code, C.min_next_code
FROM CTE A
INNER JOIN CTE B ON (B.pk = A.pk + 1) AND (A.code != B.code) -- SELECTS THE RECORDS THAT START A NEW GROUP
OUTER APPLY (
SELECT min_next_code = MIN(pk) - 1 -- LOCATION OF NEXT GROUP
FROM CTE
WHERE pk > B.pk AND (trip = B.trip) AND (code != B.code)
) C
WHERE B.id < #maxint

Resources