T-SQL: process consequtive periods and count by group - sql-server

I deal with big job to find max period by FULL months of enrollment within the year (12 months period), which I did OK if we have 2 periods. Just got stuck at the end while testing if I have 3+ periods. Data below and picture hope will provide all information and easy start. Thanks to all. This is final work table I got at the end of my process, thanks all. Code below produces partially correct results. My global task find MAX period for each member, so some fields are just for easy working.
/*
DROP TABLE IF EXISTS #t;
CREATE TABLE #t ( Cust VARCHAR(10), mm INT, mm_prev INT, rn INT)
INSERT #t values
(123456, 1, NULL, 1), (123456, 2, 1, 2),
(123456, 4, 2, 3), (123456, 5, 4, 4), (123456, 6, 5, 5),
(123456, 8, 6, 6), (123456, 9, 8, 7), (123456, 10, 9, 8), (123456, 11, 10, 9), (123456, 12, 11, 10),
(777 , 1, NULL, 1),(777 , 2, 1, 2)
SELECT * from #t
*/
select
Cust, MIN(mm) mmStart, MAX(mm) mmEnd,
CASE WHEN mm = rn THEN 'Grp A' ELSE 'Grp B' END Grp
,COUNT(*) mm_count
FROM #t
WHERE 1=1
--mm - ISNULL(mm_prev,0) = 1 --check for conseq but we drop mm=6--> start of new period
-- AND mm = rn -- this brings only first group by mm
GROUP BY Cust, CASE WHEN mm = rn THEN 'Grp A' ELSE 'Grp B' END
ORDER BY 1,4
just for the case if somebody prefer to deal with initial raw data I posting it here too with some gap and islands:
CREATE TABLE #tr ( Cust varchar(10), ENR_START date, enr_END date, rn INT); -- SELECT * FROM #t
INSERT #tr VALUES
('123456' , '2018-12-01', '2019-3-1' , 1),
('123456' , '2019-3-28', '2019-6-30' , 2), -- 6 month with 2 periods, island
('123456' , '2019-7-26', '2019-8-20' , 3),
('123456' , '2019-8-15', '2019-12-31' , 4),
('777' , '2018-11-4', '2019-3-3' , 1)
select * from #tr
Screenshot is here:

looks to me, you wanted this. Not really sure what is the purpose of the case statement in your query
with cte as
(
SELECT *,
grp = mm - rn
from #t
)
SELECT Cust, MIN(mm) as mmStart, MAX(mm) as mmEnd, grp,
count(*) as mm_count
FROM cte
GROUP BY Cust, grp
order by Cust, mmStart

Related

TSQL: Continuous period for whole year / each month

I try to find all Cust who have membership for at least for one day in each month during 2018.
I came up with solution checking their membership at the beginning / middle / end end of each month like in snippet below, but trying to find more intelligent solution.
I know that I can use tally table for each of 365 days to check this but probably there is more elegant solution ? I'm bit new to SQL, I think I'm missing something in GROUPing area.
In the code snippet shown below, both Cust have at least one day membership.
Desired output:
CustID
------
1
22
Code:
with data as
(
select *
from (values (1, 1, '2017-12-11', '2018-01-16'), (1, 22, '2018-01-28', '2018-03-9' ), (1, 333, '2018-03-1', '2018-12-31') , -- island
(22, 1, '2017-12-31', '2018-01-11'), (22, 2, '2017-2-11', '2019-12-31')) as t (CustID, ContractID, StartDD, EndDD) ---
)
select
isdate(startDD), isdate(EndDD)
from
data
), gaps as
(
select
*,
datediff(day, lag(EndDD, 1, StartDD) over (partition by CustID order by StartDD), StartDD) as BreakDD -- negative is island
from
data
)
select
*,
datepart(month,StartDD) mmS , datepart(month,EndDD) mmE
from
gaps
-- and was active any 1+ day during each of the 12 months in 2018 ????
where
1 = 1
/* and (cast('1/1/2018' as date) between StartDD and EndDD
or cast('1/15/2018' as date) between StartDD and EndDD
or cast('1/31/2018' as date) between StartDD and EndDD)
---- etc.. for each month
and ( cast('12/1/2018' as date) between StartDD and EndDD
or cast('12/15/2018' as date) between StartDD and EndDD
or cast('12/31/2018' as date) between StartDD and EndDD
)
*/
--select CustID, max(BreakDD) Max_Days
--from gaps
--group by CustID
Try this answer.
First create a function to return all the month and year between the given dates.
Function:
--SELECT * FROM dbo.Fn_GetMonthYear('2017-12-11','2018-01-16')
ALTER FUNCTION dbo.Fn_GetMonthYear(#StartDate DATETIME,#EndDate DATETIME)
RETURNS TABLE
AS
RETURN(
SELECT DATEPART(MONTH, DATEADD(MONTH, x.number, #StartDate)) AS [Month]
,DATEPART(YEAR, DATEADD(MONTH, x.number, #StartDate)) AS [Year]
FROM master.dbo.spt_values x
WHERE x.type = 'P'
AND x.number <= DATEDIFF(MONTH, #StartDate, #EndDate)
)
Table Schema:
CREATE TABLE #t(CustID INT, ContractID INT, StartDD date, EndDD date)
INSERT INTO #t values (1, 1, '2017-12-11', '2018-01-16'), (1, 22, '2018-01-28', '2018-03-9' ), (1, 333, '2018-03-1', '2018-12-31') , -- island
(22, 1, '2017-12-31', '2018-01-11'), (22, 2, '2017-2-11', '2019-12-31')
Here is the T-SQL Query for your requirement.
SELECT CustID
,COUNT(DISTINCT [Month]) NoOfMonths
FROM(
SELECT *
FROM #t t
CROSS APPLY dbo.Fn_GetMonthYear(StartDD,EndDD)
)D
WHERE [Year] = 2018
GROUP BY CustID
HAVING COUNT(DISTINCT [Month])=12
Result:
CustID NoOfMonths
1 12
22 12
find all Cust who have membership for at least for one day in each
month during 2018
I think this mean that data must be present between '2018-01-01' and '2018-12-31' for each custid.
CREATE TABLE #t(CustID INT, ContractID INT, StartDD date, EndDD date)
INSERT INTO #t values (1, 1, '2017-12-11', '2018-01-16'), (1, 22, '2018-01-28', '2018-03-9' ), (1, 333, '2018-03-1', '2018-12-31') , -- island
(22, 1, '2017-12-31', '2018-01-11'), (22, 2, '2017-2-11', '2019-12-31')
declare #From Datetime='2018-01-01'
declare #To datetime='2018-12-31'
;with CTE as
(
select CustID,min(StartDD)StartDD
,max(EndDD)EndDD
from #t
group by CustID
)
select CustID,StartDD
,EndDD
from CTE
where StartDD<=#From and EndDD>=#To
This script is not tested across all sample data.
But logic is clear.So it can be corrected accordingly.
So tell for what sample data it is not working.

How to get the incremented value in a column based on the other column in sql

I have the below table records. For Every TranNo I have to increment one in the expected result column. Is it possible?
How to get this result in sqlserver?
I am unable to get the appropriate result
<Expected Result>
1
1
2
3
4
5
6
7
8
I tried:
SELECT BatchNo,
TranNo,
WorkSource
FROM table1 WITH (NOLOCK)
WHERE BatchNo IN ('0000000420', '0000000421', '0000000422')
AND PROCESSDATE = '20190206'
GROUP BY WorkSource,
BATCHNO,
TranNo;
You can try this
create table #temp (WorkSource char(3), BatchNo char(3), TranNo int)
insert into #temp values
('012', '001', 1),
('012', '001', 1),
('012', '001', 2),
('012', '001', 3),
('012', '002', 1),
('012', '002', 2),
('012', '003', 3),
('013', '005', 1),
('013', '005', 2)
SELECT *, Dense_Rank() OVER(ORDER BY BatchNo, TranNo) AS Row_Number
FROM #temp
Try with this:
;WITH LagTranNo AS
(
SELECT
*,
IsDiffTranNo = CASE WHEN TranNo = LAG(TranNo) OVER (ORDER BY WorkSource, BatchNo, TranNo) THEN 0 ELSE 1 END
FROM
YourTable
)
SELECT
L.*,
SUM(L.IsDiffTranNo) OVER (ORDER BY WorkSource, BatchNo, TranNo)
FROM
LagTranNo AS L
I believe you want to add 1 only when the TranNo changes. Need to be careful with the ordering.
Note: LAG with only work with SQL Server 2012+.

How to get the Previous & next row based on condition

I am trying to get the statement on fetching the previous and next rows of a selected row.
Declare #OderDetail table
(
Id int primary key,
OrderId int,
ItemId int,
OrderDate DateTime2,
Lookup varchar(15)
)
INSERT INTO #OderDetail
VALUES
(1, 10, 1, '2018-06-11', 'A'),
(2, 10, 2, '2018-06-11', 'BE'), --this
(3, 2, 1, '2018-06-04', 'DR'),
(4, 2, 2, '2018-06-04', 'D'), --this
(5, 3, 2, '2018-06-14', 'DD'), --this
(6, 4, 2, '2018-06-14', 'R');
DECLARE
#ItemId int = 2,
#orderid int = 10
Required output:
Input for the procedure is order id =10 and item id =2 and i need to check item-2 is in the any other order i.e only previous and next item of matched record/order as per order date
Is this what your after? (Updated to reflect edit [OrderDate] to question)
Declare #OderDetail table
(
Id int primary key,
OrderId int,
ItemId int,
OrderDate DateTime2,
Lookup varchar(15)
)
INSERT INTO #OderDetail
VALUES
(1, 10, 1, '2018-06-11', 'A'),
(2, 10, 2, '2018-06-11', 'BE'), --this
(3, 2, 1, '2018-06-04', 'DR'),
(4, 2, 2, '2018-06-04', 'D'), --this
(5, 3, 2, '2018-06-14', 'DD'), --this
(6, 4, 2, '2018-06-14', 'R');
declare #ItemId int=2 , #orderid int = 10;
Query
With cte As
(
Select ROW_NUMBER() OVER(ORDER BY OrderDate) AS RecN,
*
From #OderDetail Where ItemId=#ItemId
)
Select Id, OrderId, ItemId, [Lookup] From cte Where
RecN Between ((Select Top 1 RecN From cte Where OrderId = #orderid) -1) And
((Select Top 1 RecN From cte Where OrderId = #orderid) +1)
Order by id
Result:
Id OrderId ItemId Lookup
2 10 2 BE
4 2 2 D
5 3 2 DD
Another possible approach is to use LAG() and LEAD() functions, that return data from a previous and subsequent row form the same resul tset.
-- Table
DECLARE #OrderDetail TABLE (
Id int primary key,
OrderId int,
ItemId int,
OrderDate DateTime2,
Lookup varchar(15)
)
INSERT INTO #OrderDetail
VALUES
(1, 10, 1, '2018-06-11', 'A'),
(2, 10, 2, '2018-06-11', 'BE'), --this
(3, 2, 1, '2018-06-04', 'DR'),
(4, 2, 2, '2018-06-04', 'D'), --this
(5, 3, 2, '2018-06-14', 'DD'), --this
(6, 4, 2, '2018-06-14', 'R');
-- Item and order
DECLARE
#ItemId int = 2,
#orderid int = 10
-- Statement
-- Get previois and next ID for every order, grouped by ItemId, ordered by OrderDate
;WITH cte AS (
SELECT
Id,
LAG(Id, 1) OVER (PARTITION BY ItemId ORDER BY OrderDate) previousId,
LEAD(Id, 1) OVER (PARTITION BY ItemId ORDER BY OrderDate) nextId,
ItemId,
OrderId,
Lookup
FROM #OrderDetail
)
-- Select current, previous and next order
SELECT od.*
FROM cte
CROSS APPLY (SELECT * FROM #OrderDetail WHERE Id = cte.Id) od
WHERE (cte.OrderId = #orderId) AND (cte.ItemId = #ItemId)
UNION ALL
SELECT od.*
FROM cte
CROSS APPLY (SELECT * FROM #OrderDetail WHERE Id = cte.previousId) od
WHERE (cte.OrderId = #orderId) AND (cte.ItemId = #ItemId)
UNION ALL
SELECT od.*
FROM cte
CROSS APPLY (SELECT * FROM #OrderDetail WHERE Id = cte.nextId) od
WHERE (cte.OrderId = #orderId) AND (cte.ItemId = #ItemId)
Output:
Id OrderId ItemId OrderDate Lookup
2 10 2 11/06/2018 00:00:00 BE
4 2 2 04/06/2018 00:00:00 D
5 3 2 14/06/2018 00:00:00 DD
Update to given this data set: I see where you are going with this. Note that in SOME cases there IS no row before the given one - so it only returns 2 not 3. Here I updated the CTE version. Un-comment the OTHER row to see 3 not 2 as there is then one before the selected row with that Itemid.
Added a variable to demonstrate how this is better allowing you to get 1 before and after or 2 before/after if you change that number (i.e. pass a parameter) - and if less rows, or none are before or after it gets as many as it can within that constraint.
Data setup for all versions:
Declare #OderDetail table
(
Id int primary key,
OrderId int,
ItemId int,
OrderDate DateTime2,
Lookup varchar(15)
)
INSERT INTO #OderDetail
VALUES
(1, 10, 1, '2018-06-11', 'A'),
(2, 10, 2, '2018-06-11', 'BE'), --this
(3, 2, 1, '2018-06-04', 'DR'),
(4, 2, 2, '2018-06-04', 'D'), --this
(5, 3, 2, '2018-06-14', 'DD'), --this
(9, 4, 2, '2018-06-14', 'DD'),
(6, 4, 2, '2018-06-14', 'R'),
--(10, 10, 2, '2018-06-02', 'BE'), -- un-comment to see one before
(23, 4, 2, '2018-06-14', 'R');
DECLARE
#ItemId int = 2,
#orderid int = 2;
CTE updated version:
DECLARE #rowsBeforeAndAfter INT = 1;
;WITH cte AS (
SELECT
Id,
OrderId,
ItemId,
OrderDate,
[Lookup],
ROW_NUMBER() OVER (ORDER BY OrderDate,Id) AS RowNumber
FROM #OderDetail
WHERE
ItemId = #itemId -- all matches of this
),
myrow AS (
SELECT TOP 1
Id,
OrderId,
ItemId,
OrderDate,
[Lookup],
RowNumber
FROM cte
WHERE
ItemId = #itemId
AND OrderId = #orderid
)
SELECT
cte.Id,
cte.OrderId,
cte.ItemId,
cte.OrderDate,
cte.[Lookup],
cte.RowNumber
FROM ctE
INNER JOIN myrow
ON ABS(cte.RowNumber - myrow.RowNumber) <= #rowsBeforeAndAfter
ORDER BY OrderDate, OrderId;
You probably want the CTE method (See an original at the end of this) however:
Just to point out, this gets the proper results but is probably not what you are striving for since it is dependent on the row order and the item id not the actual row with those two values:
SELECT TOP 3
a.Id,
a.OrderId,
a.ItemId,
a.Lookup
FROM #OderDetail AS a
WHERE
a.ItemId = #ItemId
To fix that, you can use an ORDER BY and TOP 1 with a UNION, kind of ugly. (UPDATED with date sort and != on the id.)
SELECT
u.Id,
u.OrderId,
u.OrderDate,
u.ItemId,
u.Lookup
FROM (
SELECT
a.Id,
a.OrderId,
a.OrderDate,
a.ItemId,
a.Lookup
FROM #OderDetail AS a
WHERE
a.ItemId = #ItemId
AND a.OrderId = #orderid
UNION
SELECT top 1
b.Id,
b.OrderId,
b.OrderDate,
b.ItemId,
b.Lookup
FROM #OderDetail AS b
WHERE
b.ItemId = #ItemId
AND b.OrderId != #orderid
ORDER BY b.OrderDate desc, b.OrderId
UNION
SELECT top 1
b.Id,
b.OrderId,
b.OrderDate,
b.ItemId,
b.Lookup
FROM #OderDetail AS b
WHERE
b.ItemId = #ItemId
AND b.OrderId != #orderid
ORDER BY b.OrderDate asc, b.OrderId
) AS u
ORDER BY u.OrderDate asc, u.OrderId
I think its simple, you can check with min(Id) and Max(id) with left outer join or outer apply
like
Declare #ItemID int = 2
Select * From #OderDetail A
Outer Apply (
Select MIN(A2.Id) minID, MAX(A2.Id) maxID From #OderDetail A2
Where A2.ItemId =#ItemID
) I05
Outer Apply(
Select * From #OderDetail Where Id=minID-1
Union All
Select * From #OderDetail Where Id=maxID+1
) I052
Where A.ItemId =#ItemID Order By A.Id
Let me know if this helps you or you face any problem with it...
Regards,

Query to get date rows older than a start date (not a simple WHERE)

I have a feeling this is quite simple, but I can't put my finger on the query. I'm trying to find all of the activities of an employee which corresponds to their start date in a specific location.
create table Locations (EmployeeID int, LocationID int, StartDate date);
create table Activities (EmployeeID int, ActivityID int, [Date] date);
insert into Locations values
(1, 10, '01-01-2010')
, (1, 11, '01-01-2012')
, (1, 11, '01-01-2013');
insert into Activities values
(1, 1, '02-01-2010')
, (1, 2, '04-01-2010')
, (1, 3, '06-06-2014');
Expected result:
EmployeeID LocationID StartDate EmployeeID ActivityID Date
1 10 '01-01-2010' 1 1 '02-01-2010'
1 10 '01-01-2010' 1 2 '04-01-2010'
1 11 '01-01-2013' 1 3 '06-06-2014'
So far, I have this, but it's not quite giving me the result I was hoping for. I somehow have to reference only the information from the most recent Location, which the la.StartDate <= a.Date does not filter out and includes information from older locations as well.
select *
from Locations la
inner join Activities a on la.EmployeeID = a.EmployeeID
and la.StartDate <= a.Date
Give this one a try:
with Locations as (
select
*
from (values
(1, 10, '01-01-2010')
, (1, 11, '01-01-2012')
, (1, 11, '01-01-2013')
) la (EmployeeID, LocationID, StartDate)
),
Activities as (
select
*
from (
values
(1, 1, '02-01-2010')
, (1, 2, '04-01-2010')
, (1, 3, '06-06-2014')
) a (EmployeeID, ActivityID, [Date])
)
select
la.*,
a.*
from Activities a
cross apply (
select
*
from (
select
la.*,
ROW_NUMBER() OVER (
PARTITION BY
EMPLOYEEID
ORDER BY
DATE DESC
) seqnum
from Locations la
where
la.EmployeeID = a.EmployeeID and
la.StartDate <= a.Date
) la
where
la.seqnum = 1
) la
Thank you all, but I managed to find the answer:
select *
from LocationAssociations la
inner join Activities a on la.EmployeeID = a.EmployeeID
and la.StartDate = (select max(StartDate) from LocationAssociations where StartDate >= la.StartDate and StartDate <= a.Date)

Report Server aggregation over groups by line

Maybe this is a newbie question, but
Imagine I have a report that shows the sales-order list pr sales-rep, itemized
to sku level, and want to show how many percent of the total sale of 1 sku, the sales-rep has sold.
I.e.
Sales-person
List of orders
- List of items no sold: 5 out of this months total 942
Example:
John Doe
- Order #12312
- SKU SP1231 Sold 5 . Month total 445
- SKU SP4141 Sold 63 . Month total 300
Emma Doe
- Order #123324
- SKU SP1231 Sold 65 . Month total 445
- SKU SP4141 Sold 2 . Month total 300
etc
The Month total figure is the number of items sold of that particular sku in the total reporting period.
How do I go about adding this number? If I use Fields!TotalAmount.Value it gives the total as the group'ed total. i.e. how many of sku Y was sold on order X by sales-rep Z.
I need the global total of sales of that particular SKU.
If i say SUM(Fields!Amount,Nothing) to set global scope, it gives the sum of ALL sku's, not just the sku in question.
How do I do this?
EDIT
The Report Server is SSRS, the report uses a shared Datasource that is a Report Model already hosted on the reporting server, which points to a SQL Server database with
the contents.
You didn't say what DBMS you are using (not Oracle clearly from the Fields!Amount syntax). Does this work for your DBMS?:
with sku_sales as
( select sku, sum(value) as sku_value
from sales
group by sku
)
select sales.salesperson, sum(sales.value), sku_sales.sku_value
from sales
join sku_sales on sku_sales.sku = sales.sku
group by sales.salesperson, sku_sales.sku_value
What would I do is select Sku total sum in your report dataset using comma separated lists:
-- testing data
DECLARE #Order TABLE (ID INT, SalesRepID INT, Date DATETIME)
INSERT INTO #Order
SELECT 1, 1, GETDATE() UNION
SELECT 2, 2, GETDATE() UNION
SELECT 3, 1, GETDATE() UNION
SELECT 4, 1, GETDATE() UNION
SELECT 5, 2, GETDATE()
DECLARE #OrderDetail TABLE (ID INT, OrderID INT, SkuID INT, SkuCount INT)
INSERT INTO #OrderDetail
SELECT 1, 1, 1, 10 UNION
SELECT 2, 1, 2, 5 UNION
SELECT 3, 1, 3, 20 UNION
SELECT 4, 1, 4, 10 UNION
SELECT 5, 2, 1, 15 UNION
SELECT 6, 2, 2, 25 UNION
SELECT 7, 2, 3, 15 UNION
SELECT 8, 3, 1, 15 UNION
SELECT 9, 3, 1, 10 UNION
SELECT 10, 3, 3, 10 UNION
SELECT 11, 3, 4, 15 UNION
SELECT 12, 4, 1, 5
DECLARE #Sku TABLE (ID INT, SkuCode VARCHAR(10))
INSERT INTO #Sku
SELECT 1, 'SP1233' UNION
SELECT 2, 'SP2262' UNION
SELECT 3, 'SP1531' UNION
SELECT 4, 'SP4235'
DECLARE #SalesRep TABLE (ID INT, SalesRepName VARCHAR(20))
INSERT INTO #SalesRep
SELECT 1, 'John Doe' UNION
SELECT 2, 'Emma Doe'
-- filters for testing
DECLARE #StartDate DATETIME, #EndDate DATETIME
SELECT #StartDate = GETDATE(), #EndDate = GETDATE()
DECLARE #SkuIDList VARCHAR(8000), #SkuSumList VARCHAR(8000)
SELECT #SkuIDList = '', #SkuSumList = ''
--gether all sku IDs and Sum in two comma separated list
SELECT #SkuIDList = #SkuIDList + CONVERT(VARCHAR, OD.SkuID) + ',',
#SkuSumList = #SkuSumList + CONVERT(VARCHAR, SUM(OD.SkuCount)) + ','
FROM #Order O
INNER JOIN #OrderDetail OD ON O.ID = OD.OrderID
WHERE O.Date BETWEEN #StartDate AND #EndDate
GROUP BY OD.SkuID
-- remove last ','
SELECT #SkuIDList = SUBSTRING(#SkuIDList, 0, LEN(#SkuIDList)),
#SkuSumList = SUBSTRING(#SkuSumList, 0, LEN(#SkuSumList))
-- include thouse lists in the main select for your report dataset
SELECT O.ID, OD.SkuID, O.SalesRepID, SR.SalesRepName, S.SkuCode,
OD.SkuCount, #SkuIDList AS SkuIDs, #SkuSumList AS SkuSums
FROM #Order O
INNER JOIN #OrderDetail OD ON O.ID = OD.OrderID
INNER JOIN #Sku S ON OD.SkuID = S.ID
INNER JOIN #SalesRep SR ON O.SalesRepID = SR.ID
WHERE O.Date BETWEEN #StartDate AND #EndDate
Then you can use some custome code to retrieve sum value by sku ID (I have to write in C# currently, you easely convert it to VB):
public int GetSkuSum(string skuSumCSV, string skuIDCSV, int searchSkuID)
{
string[] strSkuSum = skuSumCSV.Split(',');
string[] strSkuID = skuIDCSV.Split(',');
for (int i = 0; i < strSkuID.Length; i++)
{
if (Convert.ToInt32(strSkuID[i].Trim()) == searchSkuID)
{
return Convert.ToInt32(strSkuSum[i]);
}
}
return 0;
}
Then use it in your textbox Value expression:
=Code.GetSkuSum(Fields!SkuIDs.Value,Fields!SkuSums.Value,Fields!SkuID.Value)

Resources