Partition by to get the correct date - sql

I would like to retrieve the oldest date under the FirstScanned Column. My issue is that a product can be registered again at some point as a new product, when this happens, I'd like the FirstScanned to be retrieve the oldest scanned date based on the newest scan on Location 1
In the following example I'm trying to retrieve the correct FirstScanned Date without success:
CREATE TABLE [dbo].[Products123](
[ID] [int] NOT NULL,
[GTIN] [varchar](50) NULL,
[LocationID] [int] NULL,
[UserID] [int] NULL,
[Created] [datetime] )
insert into Products123(ID, GTIN, LocationID, UserID, Created)
Values(1, '12345678910', 1, 3, '2017-06-30 14:58:07.693'), -- Location "1" is when products was registered/scanned for this first time
(2, '12345678910', 5, 3, '2017-06-30 15:25:12.287'), -- The product is scanned into a new location
(3, '12345678910', 17, 3, '2017-06-30 14:58:07.693'), -- The product is now scanned into the "end" location and is considered to be closed
(4, '12345678910', 1, 7, '2017-08-01 11:34:16.347'), -- A month later the same productID has been registered,
(5, '12345678910', 4, 7, '2017-08-01 11:36:16.460') -- etc
DECLARE #Prev8workingdays date = CASE
WHEN datepart(dw, getdate()) IN (2,3,4) THEN dateadd(day,-14, getdate())
WHEN datepart(dw, getdate()) IN (1) THEN dateadd(day,-13, getdate())
ELSE dateadd(day,-12, getdate())
END
DECLARE #Pre6WorkingDay date = CASE
WHEN datepart(dw, getdate()) IN (2) THEN dateadd(day,-9, getdate())
WHEN datepart(dw, getdate()) IN (1) THEN dateadd(day,-8, getdate())
ELSE dateadd(day,-7, getdate())
END
select p.GTIN, p.FirstScanned as FirstScan, p.Created As lastScan
from
(
select p.GTIN
,p.LocationID
,p.Created
,min(p.Created) over (partition by p.GTIN) as FirstScanned
,max(p.Created) over (partition by p.GTIN) as LastScanned
from Products123 p
) p
where p.LastScanned = p.Created and LocationID not in (15,16,17) AND p.FirstScanned < #Prev8workingdays
Order by FirstScanned
My result looks like this:
GTIN | FirstScanned | LastScanned
12345678910 | 2017-06-30 14:58:07.693 |2017-08-01 11:36:16.460
But it Should be:
GTIN | FirstScanned | LastScanned
12345678910 | 2017-08-01 11:34:16.347 |2017-08-01 11:36:16.460

You can get there just using a simple case statement. Take the max date for location 1, and that will be the latest date
select p.GTIN, p.FirstScanned as FirstScan, p.Created As lastScan
from
(
select p.GTIN
,p.LocationID
,p.Created
,max(case when p.LocationID=1 then p.Created else null end) over (partition by p.GTIN) as FirstScanned
,max(p.Created) over (partition by p.GTIN) as LastScanned
from Products123 p
) p
where p.LastScanned = p.Created and LocationID not in (15,16,17) AND p.FirstScanned < #Prev8workingdays

Related

Reading 0.5 value in ROW_NUMBER() and PARTITION BY | SQL Server 2012

I have this question before Looping between 2 case parameters (Date Range and Row Count) | SQL Server 2012 , now I'm thinking about a scenario, what if the value has a 0.5? or .5? Will this work using ROW_NUMBER()?
I'm trying to make this work using only CASE method.
This is my old script:
DECLARE #dbApple TABLE
(
FromDate varchar(30) NOT NULL,
ToDate varchar(30) NOT NULL,
Name varchar(30) NOT NULL,
Count float(30) NOT NULL
)
INSERT INTO #dbApple (FromDate, ToDate, Name, Count)
VALUES ('2019-10-05', '2019-10-09', 'APPLE', '2.5');
(SELECT
CONVERT(date, CONVERT(date, DATEADD(D, VAL.NUMBER, FromDate))) AS Date,
DB.Name,
CASE
WHEN CONVERT(date, CONVERT(date, DATEADD(D, VAL.NUMBER, FromDate))) BETWEEN CONVERT(date, CONVERT(date, DATEADD(D, VAL.NUMBER, FromDate))) AND CONVERT(date, CONVERT(date, DATEADD(D, VAL.NUMBER, ToDate)))
THEN
CASE
WHEN ROW_NUMBER() OVER (PARTITION BY Count, FromDate, ToDate ORDER BY Count) <= Count
THEN (COUNT / COUNT)
END
END AS Count
FROM
#dbApple DB
JOIN
MASTER..SPT_VALUES VAL ON VAL.TYPE = 'P'
AND VAL.NUMBER BETWEEN 0 AND DATEDIFF(D, FromDate, ToDate))
This is the output:
This is my expected output:
Is there a way for this to work? Thank you.
You can greatly simplify your query by noting that VAL.NUMBER is already your row number (just starting at 0 instead of 1). You can then compare your Count value to VAL.NUMBER and if Count - VAL.NUMBER is greater than 1, output 1; if it's greater than 0 output the difference, otherwise output NULL. For this demo query I've simulated your numbers table with a table value constructor:
declare #dbApple TABLE(
FromDate varchar(30) NOT NULL,
ToDate varchar(30) NOT NULL,
Name varchar(30) NOT NULL,
Count float(30) NOT NULL
)
INSERT INTO #dbApple
(FromDate,ToDate,Name,Count) VALUES ('2019-10-05','2019-10-09','APPLE',2.5);
SELECT
CONVERT(date,CONVERT(date,DATEADD(D,VAL.NUMBER,FromDate))) AS Date,
Name,
CASE WHEN Count - VAL.NUMBER > 1 THEN 1
WHEN Count - VAL.NUMBER > 0 THEN Count - VAL.NUMBER
END AS Count
FROM
#dbApple D
JOIN (VALUES (0), (1), (2), (3), (4), (5)) VAL(NUMBER)
ON VAL.NUMBER BETWEEN 0 AND DATEDIFF(D, FromDate, ToDate)
Output:
Date Name Count
2019-10-05 APPLE 1
2019-10-06 APPLE 1
2019-10-07 APPLE 0.5
2019-10-08 APPLE (null)
2019-10-09 APPLE (null)
Demo on dbfiddle

Looping between 2 case parameters (Date Range and Row Count) | SQL Server 2012

I'm trying to understand on how to properly execute this query into this table.
The output is supposed to only loop based on count table, but will display all the dates within the parameter of the table row.
I separated the database for APPLE, ORANGE, and MANGO because I need to mimic my database structure. The data should be called from different database, and then process the looping of Date Range and Row Count.
I got the idea from this question, and used some of the codes to try to replicate it :
SQL how to convert row with date range to many rows with each date
This is the expected output.
This is the query that I'm trying to fix
DECLARE #dbApple TABLE
(
FromDate VARCHAR(30) NOT NULL,
ToDate VARCHAR(30) NOT NULL,
Name VARCHAR(30) NOT NULL,
Count VARCHAR(30) NOT NULL
)
INSERT INTO #dbApple (FromDate, ToDate, Name, Count)
VALUES ('2019-10-05', '2019-10-09', 'APPLE', '3');
DECLARE #dbOrange TABLE
(
FromDate VARCHAR(30) NOT NULL,
ToDate VARCHAR(30) NOT NULL,
Name VARCHAR(30) NOT NULL,
Count VARCHAR(30) NOT NULL
)
INSERT INTO #dbOrange (FromDate, ToDate, Name, Count)
VALUES ('2019-10-10', '2019-10-14', 'ORANGE', '2');
DECLARE #dbMango TABLE
(
FromDate VARCHAR(30) NOT NULL,
ToDate VARCHAR(30) NOT NULL,
Name VARCHAR(30) NOT NULL,
Count VARCHAR(30) NOT NULL
)
INSERT INTO #dbMango (FromDate, ToDate, Name, Count)
VALUES ('2019-10-15', '2019-10-19', 'MANGO', '4');
(SELECT
CONVERT(DATE, CONVERT(DATE, DATEADD(D, v.number, FromDate))) AS Date,
DB.Name,
CASE
WHEN ROW_NUMBER() OVER(PARTITION BY Count, FromDate, ToDate ORDER BY Count) = Count
THEN Count
ELSE NULL
END AS Count
FROM
#dbApple DB
JOIN
MASTER..spt_values v ON v.TYPE = 'P'
AND v.number BETWEEN 0 AND DATEDIFF(D, FromDate, ToDate))
UNION
(SELECT
CONVERT(DATE, DATEADD(D, v.number, FromDate)) AS Date,
DB.Name,
CASE
WHEN ROW_NUMBER() OVER(PARTITION BY Count, FromDate, ToDate ORDER BY Count) = Count
THEN Count
ELSE NULL
END AS Count
FROM
#dbOrange DB
JOIN
MASTER..spt_values v ON v.TYPE = 'P'
AND v.number BETWEEN 0 AND DATEDIFF(D, FromDate, ToDate))
UNION
(SELECT
CONVERT(DATE, DATEADD(D, v.number, FromDate)) AS Date,
DB.Name,
CASE
WHEN ROW_NUMBER() OVER(PARTITION BY Count, FromDate, ToDate ORDER BY Count) = Count
THEN Count
ELSE NULL
END AS Count
FROM
#dbMango DB
JOIN
MASTER..spt_values v ON v.TYPE = 'P'
AND v.number BETWEEN 0 AND DATEDIFF(D, FromDate, ToDate))
This is the output:
Tried using CASE WITHIN CASE but no luck.
declare #dbApple TABLE(
FromDate varchar(30) NOT NULL,
ToDate varchar(30) NOT NULL,
Name varchar(30) NOT NULL,
Count varchar(30) NOT NULL
)
INSERT INTO #dbApple
(FromDate,ToDate,Name,Count) VALUES ('2019-10-05','2019-10-09','APPLE','3');
(SELECT
CONVERT(date,CONVERT(date,DATEADD(D,VAL.NUMBER,FromDate))) AS Date,
DB.Name,
CASE WHEN CONVERT(date,CONVERT(date,DATEADD(D,VAL.NUMBER,FromDate))) BETWEEN
CONVERT(date,CONVERT(date,DATEADD(D,VAL.NUMBER,FromDate))) AND
CONVERT(date,CONVERT(date,DATEADD(D,VAL.NUMBER,ToDate)))
THEN CASE WHEN ROW_NUMBER()
OVER(PARTITION BY Count,FromDate,ToDate
ORDER BY Count) = Count
THEN Count
ELSE '1' END
ELSE NULL END AS Count
FROM
#dbApple DB
JOIN MASTER..SPT_VALUES VAL on VAL.TYPE='P'
AND VAL.NUMBER BETWEEN 0 AND DATEDIFF(D, FromDate, ToDate))
Output using CASE WITHIN CASE.
How about solving this recursively?
First put the fruit in a basket, then peel them.
WITH BASKET AS
(
SELECT FromDate, ToDate, Name, Count
FROM #dbApple
UNION ALL
SELECT FromDate, ToDate, Name, Count
FROM #dbOrange
UNION ALL
SELECT FromDate, ToDate, Name, Count
FROM #dbMango
),
PEELED AS
(
SELECT
FromDate as [Date], 1 as Lvl,
FromDate, ToDate, Name, Count
FROM BASKET
UNION ALL
SELECT
DATEADD(day,1,[Date]), Lvl +1,
FromDate, ToDate, Name, Count
FROM PEELED p
WHERE [Date] < ToDate
)
SELECT [Date], [Name],
CASE WHEN Lvl <= Count THEN 1 END AS [Count]
FROM PEELED
ORDER BY [Date];
A test on rextester here
Using a tally, you can achieve this very easily:
CREATE TABLE dbo.YourTable (Fruit varchar(20),
Quantity int);
INSERT INTO dbo.YourTable
VALUES ('Apple',3),
('Orange',2),
('Mango',4);
GO
DECLARE #StartDate date = '20190101',
#EndDate date = '20190110';
--Going to us a tally, incase there can be large date ranges
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP(DATEDIFF(DAY, #StartDate, #EndDate)+1) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2, N N3) --1000 rows
SELECT DATEADD(DAY, T.I-1, #StartDate) aS [Date],
YT.Fruit,
CASE WHEN T.I <= YT.Quantity THEN 1 END AS [Count]
FROM dbo.YourTable YT
CROSS JOIN Tally T
ORDER BY Fruit,
[Date];
GO
DROP TABLE dbo.YourTable

SQL order by needs to check if DATETIME2 is not null then return them first and after order by id

I have two tables and I have trouble figuring out how to do the order by statement to fit my needs.
Basically if the FeaturedUntil column if greater than now then these should be returned first ordered by the PurchasedAt column. Most recent purchases should be first. After these everything should be ordered by the item Id column descending.
Create Table Script
create table Items(
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] nvarchar(200) null,
)
create table Feature(
[Id] [int] IDENTITY(1,1) NOT NULL,
[PurchasedAt] [datetime2](7) NOT NULL,
[FeaturedUntil] [datetime2](7) NOT NULL,
[ItemId] [int] NOT NULL,
)
Insert Script
insert into Items(Name) values ('test1')
insert into Feature(PurchasedAt, FeaturedUntil, ItemId) values (dateadd(day, -3, getdate()), dateadd(month, 1, getdate()), ##IDENTITY)
insert into Items(Name) values ('test2')
insert into Feature(PurchasedAt, FeaturedUntil, ItemId) values (dateadd(day, -2, getdate()), dateadd(month, 1, getdate()), ##IDENTITY)
insert into Items(Name) values ('test3')
insert into Feature(PurchasedAt, FeaturedUntil, ItemId) values (dateadd(day, -1, getdate()), dateadd(month, -1, getdate()), ##IDENTITY)
insert into Items(Name) values ('test4')
Select Script
select *
from Items i
left join Feature f on i.Id = f.ItemId
order by
case when f.FeaturedUntil is not null THEN f.PurchasedAt
else i.Id
end
The select should return test2 first as it's FeaturedUntil is greater than now and it is the most recently purchased, second row should be test1 as it is bought before test2. After these should be test4 and last one is test3, because these have no joining Feature table data or the FeatureUntil is not greater than now and these are order by their Item.Id descending.
SELECT *
FROM items i
LEFT JOIN feature f
ON i.id = f.itemid
ORDER BY CASE
WHEN f.featureduntil > getdate THEN purchasedat
ELSE '19000101'
END DESC,
id DESC
You need to order this in descending in order to get the most recent purchase first; the ID sort will still occur, so if you have two PurchasedAt's that are the same, it would sort those 2 by ID.
Based on what you've told us, I think this might be what you're after:
ORDER BY CASE WHEN FeaturedUntil > GETDATE THEN PurchasedAt ELSE '99991231' END ASC, --Future features first, and in date order
--(past have a silly in future date, so always last
Id; --Then ID
Try the following.
select *, case when f.FeaturedUntil is not null THEN f.PurchasedAt else NULL end AS PurchasedAtNew
from Items i
left join Feature f on i.Id = f.ItemId
order by PurchasedAtNew desc, i.Id

Table with inconsistent starttime and stoptime

I have a table that contains a date, a starttime, and a stoptime. I have several problems that I don't know how to solve for. Each row contains either a starttime OR a stoptime (not both). While this itself is not a problem, I need to calculate the runtime by date. Also, there are instances of multiple starttimes before there is a stoptime registered. Assume the following:
Date, Starttime, Stoptime
4/1/2016, 23:00:00, NULL
4/2/2016, NULL, 03:00:00
4/2/2016, 05:00:00, NULL
4/2/2016, 07:00:00, NULL
4/2/2016, NULL, 08:00:00
4/2/2016, 10:00:00, NULL
4/2/2016, NULL, 10:15:00
I need the output to be:
4/1/2016, 01:00:00
4/2/2016, 06:15:00
I have tried a few things, with very poor results. Can any experts out there solve this problem?
Here's one way how you could handle this:
Setup:
CREATE TABLE #Table1
([Date] date, [Starttime] time, [Stoptime] time)
;
INSERT INTO #Table1
([Date], [Starttime], [Stoptime])
VALUES
('2016-04-01', '23:00:00', NULL),
('2016-04-02', NULL, '00:00:00'),
('2016-04-02', '00:00:00', NULL),
('2016-04-02', NULL, '03:00:00'),
('2016-04-02', '05:00:00', NULL),
('2016-04-02', '07:00:00', NULL),
('2016-04-02', NULL, '08:00:00'),
('2016-04-02', '10:00:00', NULL),
('2016-04-02', NULL, '10:15:00')
;
SQL:
select
convert(date, StartTime),
convert(time, dateadd(second, sum(datediff(second, StartTime, EndTime)),0))
from (
select
min(Time) as StartTime,
min(case when Type = 0 then Time end) as EndTime
from
(
select
sum(Type) over (order by Time asc, Type Asc) as GRP, *
from (
select
isnull(lag(Type) over (order by Time asc, Type Asc), -1) as LagType, *
from (
select
case when Starttime is NULL then 0 else 1 end as Type,
case when Starttime is NULL
then convert(datetime, [Date]) + convert(datetime, Stoptime)
else convert(datetime, [Date]) + convert(datetime, Starttime) end as Time
from
#Table1
) A
) B
where Type != 1 or LagType != 1
) C
group by GRP
) D
group by convert(date, StartTime)
Result:
2016-04-01 01:00:00
2016-04-02 06:15:00
This requires that you have start and stop record in your data for each of the days, so you'll need to use union or something like that to add that to your initial data.
The innermost select generates typing (0/1) for the rows and calculates the date + start / end time into a single column. The next one adds TypeLag to the data and it's used to remove the duplicate start records. The next select has a running total over Type, so that start and end records belonging together will have a unique group number. The rest will just pick the start time + earliest end time from each group and calculate the durations.

How to select max of sum of columns per another column over datetime range?

CREATE TABLE [T]
(
CreatedOn DATETIME NOT NULL
,Name NVARCHAR(20) NOT NULL
,Code NVARCHAR(20) NOT NULL
,R FLOAT NULL
,C1 FLOAT NULL
,C2 FLOAT NULL
,C3 FLOAT NULL
);
INSERT INTO [T] VALUES
('2013-01-01', N'A', N'', 13, NULL, NULL, NULL)
,('2013-01-07', N'A', N'1', NULL, 5, NULL, NULL)
,('2013-01-31', N'A', N'2', NULL, 4, NULL, NULL)
,('2013-02-01', N'A', N'1', NULL, NULL, 6, NULL)
,('2013-02-15', N'A', N'2', NULL, NULL, NULL, 3)
,('2013-03-01', N'A', N'1', NULL, 1, NULL, NULL)
,('2013-03-05', N'A', N'', 8, NULL, NULL, NULL)
,('2013-03-22', N'A', N'2', NULL, NULL, NULL, 1)
,('2013-05-01', N'A', N'1', NULL, 2, NULL, NULL);
In [T]
1. One and only one non-null value per row for [R], [C1], [C2] and [C3]
2. [Code] contains a non-empty value if [C1], [C2] or [C3] contains a non-null value
3. There is an index on [Name]
4. Contains millions of rows
5. Few unique values of [Code], typically less than 100
6. Few unique values of [Name], typically less than 10000
7. Is actually a complex view containing several inner joins
How does one select from [T] ([DateMonth], [P]) where [CreatedOn] >= #Start AND [CreatedOn] <= #End AND [Name] = #Name AND [P] = Sum([R]) - (Sum(MaxOf(Sum([C1]), Sum([C2]), Sum([C3]), per unique [Code])))? (See the expected output below for a more accurate 'explanation'.) There should be a row in the resultset for each month #Start - #End regardless of whether there are rows for that month in [T]. Temporary table use is acceptable.
Expected Output
#Name = N'A'
#Start = '2012-12-01'
#End = '2013-07-01'
DateMonth P
'2012-12-01' 0
'2013-01-01' 4 -- 4 = SUM([R])=13 - (MaxForCode'1'(SUM(C1)=5, SUM(C2)=0, SUM(C3)=0)=5 + MaxForCode'2'(SUM(C1)=4, SUM(C2)=0, SUM(C3)=0)=4)
'2013-02-01' 3 -- 3 = SUM([R])=13 - (MaxForCode'1'(SUM(C1)=5, SUM(C2)=6, SUM(C3)=0)=6 + MaxForCode'2'(SUM(C1)=4, SUM(C2)=0, SUM(C3)=3)=4)
'2013-03-01' 11 -- 11 = SUM([R])=13+8=21 - (MaxForCode'1'(SUM(C1)=5+1=6, SUM(C2)=6, SUM(C3)=0)=6 + MaxForCode'2'(SUM(C1)=4, SUM(C2)=0, SUM(C3)=3+1=4)=4)
'2013-04-01' 11
'2013-05-01' 9 -- 9 = SUM([R])=13+8=21 - (MaxForCode'1'(SUM(C1)=5+1=6, SUM(C2)=6+2=8, SUM(C3)=0)=8 + MaxForCode'2'(SUM(C1)=4, SUM(C2)=0, SUM(C3)=3+1=4)=4)
'2013-06-01' 9
'2013-07-01' 9
Here is one solution. There are certainly some performance improvements that could be made, but I'll leave that up to you and your specific situation. Note that the CTE usage is certainly not necessary and adding CreatedOn to the index would be very helpul. A temp table may also be better than a table variable, but you'll need to evaluate that.
Since I think what you're looking for are running totals, this article may be helpful in improving the performance of my suggested solution.
Personally, I would first consider not using the view, as working directly with the sql that creates the view may be more performant.
Here is the SQL and a SQLFiddle link.
DECLARE #Name NVARCHAR(1) = N'A',
#Start DATETIME = '2012-12-01',
#End DATETIME = '2013-07-01'
--get the date for the first of the start and end months
DECLARE #StartMonth DATETIME = DATEADD(month, DATEDIFF(month, 0, #Start), 0)
DECLARE #EndMonth DATETIME = DATEADD(month, DATEDIFF(month, 0, #End), 0)
DECLARE #tt TABLE
(
DateMonth DATETIME,
sum_r FLOAT,
code NVARCHAR(20),
max_c FLOAT
)
--CTE to create a simple table with an entry for each month (and nxt month)
;WITH Months
as
(
SELECT #StartMonth as 'dt', DATEADD(month, 1, #StartMonth) as 'nxt'
UNION ALL
SELECT DATEADD(month, 1, dt) as 'dt', DATEADD(month, 2, dt) as 'nxt'
FROM Months
WHERE dt < #EndMonth
)
--SELECT * FROM Months OPTION(MAXRECURSION 9965) --for the CTE, you could also select dates into a temp table/table var first
INSERT INTO #tt (DateMonth, sum_r, code, max_c)
SELECT M.dt DateMonth,
ISNULL(t.sum_r,0) sum_r,
ISNULL(t.code,'') code,
ISNULL(t.max_c,0) max_c
--sum_c1, sum_c2, sum_c3, cnt
FROM Months M
OUTER APPLY (
SELECT sum_r,
code,
CASE WHEN sum_c1 >= sum_c2 AND sum_c1 >= sum_c3 THEN sum_c1
WHEN sum_c2 >= sum_c1 AND sum_c2 >= sum_c3 THEN sum_c2
ELSE sum_c3
END max_c
--sum_c1, sum_c2, sum_c3, cnt
FROM ( --use a sub select here to improve case statement performance getting max_c
SELECT SUM(ISNULL(r,0)) sum_r,
code,
sum(ISNULL(c1,0)) sum_c1,
sum(ISNULL(c2,0)) sum_c2,
SUM(ISNULL(c3,0)) sum_c3
FROM T
WHERE CreatedOn >= #Start AND CreatedOn < M.nxt
AND CreatedOn <= #End
AND Name = #Name
GROUP BY code
) subselect
) t
OPTION (MAXRECURSION 999)
SELECT DateMonth, SUM(sum_r) - SUM(max_c) p
FROM #tt
GROUP BY DateMonth