T-SQL to efficiently select the most recent previous order item - sql-server

I am trying to figure out an efficient way to select the previous order item for each order item in a current order.
For example each order has a set of order items, i.e. the amount of an article/product that was ordered. For a particular customer, this would result in an order history as illustrated below:
I would like to find the "previous" orderitem for each orderitem in a particular order, i.e. the most recent orderitem for the same customer and article product preceding the order in question. These are highlighted above. Note that the previous orderitem may not be from the previous order of that customer.
Selecting it for a particular orderitem in an order with :datetime for :customerID and :articleID could be done like this:
select top(1) *
from orderitem
join order on order.orderid = orderitem.orderid
where order.customerID = :customerID
and order.datetime < :datetime
and orderitem.articleID = :articleID
order by order.datetime desc
However, is there an efficient way rather than looping or using a sub-select to select all previous orderitems for a given order with a single select or some type of join?

Your description of your desired outcome isn't quite clear. This sounds like you want a pivot table for the customer with the most recent order on the left. Here's an example of using PIVOT to accomplish the desired output. It only has order dates, if you want order numbers, that can be added in the column names.
--Create tables for test data.
CREATE TABLE order_hdr (
order_number int
, order_date date
, customer_id int
);
CREATE TABLE order_lines (
order_number int
, line_number int
, quantity int
, item_id nvarchar(50)
);
--Populate test data.
INSERT INTO order_hdr (order_number, order_date, customer_id)
VALUES (111, '1/1/2022', 12345)
, (112, '1/2/2022', 45678)
, (113, '1/2/2022', 87964)
, (114, '1/3/2022', 12345)
, (115, '1/3/2022', 45678)
, (116, '1/3/2022', 87964)
, (117, '1/9/2022', 12345)
, (118, '1/9/2022', 45678)
, (119, '1/9/2022', 87964)
;
INSERT INTO order_lines (order_number, line_number, quantity, item_id)
VALUES (111, 1, 4, 'Article A')
, (111, 2, 2, 'Article B')
, (111, 3, 3, 'Article C')
, (111, 4, 1, 'Article D')
, (112, 1, 1, 'Article B')
, (112, 2, 1, 'Article C')
, (112, 3, 1, 'Article D')
, (113, 1, 11, 'Article B')
, (113, 2, 9, 'Article C')
, (113, 3, 8, 'Article D')
, (114, 1, 4, 'Article C')
, (114, 2, 6, 'Article D')
, (115, 1, 77, 'Article A')
, (115, 2, 22, 'Article B')
;
--Variables for filtering data.
DECLARE #CustID int = 12345;
DECLARE #ReportDate date = '2/1/2022';
DECLARE #ItemID nvarchar(50) = 'Article A'; --Not used.
--Create a variable to hold the string of dates.
DECLARE #dates nvarchar(max);
--Get last 5 distinct order dates.
SET #dates = STUFF((
SELECT ',"' + CAST(order_date as nvarchar) + '"'
FROM (
SELECT DISTINCT TOP (5) order_date
FROM order_hdr
WHERE customer_id = #CustID
ORDER BY order_date DESC
) as prelim
FOR XML PATH('')
),1,1,'');
--For debugging, show the string of dates.
PRINT 'Date string contents: ' + #dates;
--For debugging/testing, here is a static query using PIVOT
PRINT 'Test Query using Static Order Dates:';
SELECT item_id, "1/3/2022", "1/2/2022"
FROM (
SELECT
oh.order_date
, ol.item_id
, ol.quantity
FROM order_hdr as oh
INNER JOIN order_lines as ol
ON ol.order_number = oh.order_number
WHERE oh.customer_id = #CustID
) as st
PIVOT
(
SUM(quantity)
FOR order_date IN ("1/3/2022", "1/2/2022")
) as pt
;
--Now create a text variable to hold the dynamic SQL. We will substitue the place holders for the order dates or order numbers.
DECLARE #sqlText nvarchar(max) = N'
SELECT item_id, {{0}}
FROM (
SELECT
oh.order_date
, ol.item_id
, ol.quantity
FROM order_hdr as oh
INNER JOIN order_lines as ol
ON ol.order_number = oh.order_number
WHERE oh.customer_id = {{1}}
) as st
PIVOT
(
SUM(quantity)
FOR order_date IN ({{0}})
) as pt
;
';
--Replace the placeholders with the date string;
SET #sqlText = REPLACE(#sqlText, '{{0}}', #dates);
--Replace the placeholders with the date string;
SET #sqlText = REPLACE(#sqlText, '{{1}}', CAST(#CustID as nvarchar));
--Show query for debugging.
PRINT 'The dynamic query: ' + CHAR(13) + #sqlText;
--Execute the dynamic sql.
PRINT 'Final Query using Dynamic Order Dates:';
EXEC (#sqlText);
Test Query using Static Order Dates:
item_id
1/3/2022
1/2/2022
Article A
null
null
Article B
null
null
Article C
4
null
Article D
6
null
Date string contents: "2022-01-09","2022-01-03","2022-01-01"
The dynamic query:
SELECT item_id, "2022-01-09","2022-01-03","2022-01-01"
FROM (
SELECT
oh.order_date
, ol.item_id
, ol.quantity
FROM order_hdr as oh
INNER JOIN order_lines as ol
ON ol.order_number = oh.order_number
WHERE oh.customer_id = 12345
) as st
PIVOT
(
SUM(quantity)
FOR order_date IN ("2022-01-09","2022-01-03","2022-01-01")
) as pt
;
Final Query using Dynamic Order Dates:
item_id
2022-01-09
2022-01-03
2022-01-01
Article A
null
null
4
Article B
null
null
2
Article C
null
4
3
Article D
null
6
1
fiddle

Related

How to convert two rows into one column using pivot in sql

I want to convert two columns into one row using PIVOT FUNCTION in SQL database.
But I'm facing problems. I can convert 1 column into 1 row but not 2 columns into 1 row.
You can try this
Create Table Article (ArticleId Int, [Description] Varchar(10))
Insert Into Article Values (1, 'Test')
Create Table OrderForecast(ArticleId Int, [Week] Int, [Order] Int, Amount Int)
Insert Into OrderForecast Values (1, 51, 1, 0),(1, 52, 2, 150), (1, 1, 3, 0),(1, 2, 4, 200), (1, 3, 5,0)
Select ArticleId, [Description], Week51, Week52, Week1, Week2, Week3
from
(
select ArticleId, [Description], Amount, [Week]
from
(
SELECT OrderForecast.ArticleId, 'Week' + Convert(Varchar(10), OrderForecast.[Week]) as [Week], [Order], Amount,
Article.[Description] as [Description] FROM OrderForecast
Inner Join Article On OrderForecast.ArticleId = Article.ArticleId
)a
) d
pivot
(
max(Amount)
for [Week] in (Week51, Week52, Week1, Week2, Week3)
) piv;
You can find the demo Here

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,

SQL Server - How would I insert a RANK function to rows that are already sorted in ranked order?

So, apparently, I have everything right according to my professor except for one column that shows the rank of the columns shown in the code below. I'm thinking that, essentially, it just has to show the row numbers off to the left side in its own column. Here are the instructions:
The sales manager would now like you to create a report that ranks her
products by both their total sales and total sales quantity (each will
be its own column). Create a stored procedure that returns the
following columns but also with the two new rank columns added.
Product Name | Orders Count | Total Sales Value | Total Sales
Quantity
I know that it doesn't have that extra column in the assignment description, but I guess I need it. Here is what I have so far:
USE OnlineStore
GO
CREATE PROC spManagerProductSalesCount
AS
BEGIN
SELECT
P.Name AS 'Product Name',
Isnull(Count(DISTINCT O.OrderID), 0) AS 'Orders Count',
Sum(Isnull(O.OrderTotal, 0)) AS 'Total Sales Value',
Sum (Isnull(OI.OrderItemQuantity, 0)) AS 'Total Sales Quantity'
FROM
Product P
INNER JOIN
OrderItem OI ON P.ProductID = OI.ProductID
INNER JOIN
Orders O on O.OrderID = OI.OrderID
GROUP BY
P.Name
ORDER BY
'Total Sales Value' DESC, 'Total Sales Quantity' DESC
END
Update: It does need to be in a stored procedure and CTEs can/should be used. I could use some help with the CTEs. Those are pretty difficult for me.
This is just the select part of the stored proc but it should show you what to do:
declare #products table
(
Name varchar(50),
id int
)
declare #orderitems table
(
id int,
orderid int,
productid int,
orderitemquantity int
)
declare #orders table
(
orderid int,
ordertotal decimal(18,2)
)
insert into #products VALUES ('apple', 1)
insert into #products VALUES ('orange', 2)
insert into #products VALUES ('pear', 3)
insert into #products VALUES ('melon', 4)
insert into #orders values(1, 19.0)
insert into #orders values(2, 25.5)
insert into #orders values(3, 9.5)
insert into #orders values(4, 13.5)
insert into #orders values(5, 8.5)
insert into #orderitems VALUES(1, 1, 1, 20)
insert into #orderitems VALUES(2, 1, 2, 10)
insert into #orderitems VALUES(3, 2, 3, 5)
insert into #orderitems VALUES(4, 2, 4, 4)
insert into #orderitems VALUES(5, 3, 1, 10)
insert into #orderitems VALUES(6, 3, 2, 5)
insert into #orderitems VALUES(7, 4, 3, 3)
insert into #orderitems VALUES(8, 4, 4, 2)
insert into #orderitems VALUES(9, 5, 1, 5)
insert into #orderitems VALUES(10, 5, 4, 2)
;WITH summary as
(
SELECT p.Name as ProductName,
COUNT(o.orderid) as 'Orders Count',
ISNULL(Sum(o.ordertotal),0) AS 'Total Sales Value',
ISNULL(Sum(oi.orderitemquantity),0) AS 'Total Sales Quantity'
FROM #products p
INNER JOIN #orderitems oi on oi.productid = p.id
INNER JOIN #orders o on o.orderid = oi.orderid
GROUP BY p.Name
)
SELECT ProductName, [Orders Count], [Total Sales Value], [Total Sales Quantity],
RANK() OVER (ORDER BY [Total Sales Value] DESC) AS ValueRanking,
RANK() OVER (ORDER BY [Total Sales Quantity] DESC) AS QuantityRanking FROM summary
Notice a few things here. This code can be cut and pasted into a Management Studio query window and run as such. It starts with some table declarations and insert of sample data. When asking a question it is always useful if you do this part of the work; people are much more likely to answer, if the most boring bit is done!
COUNT() does not need ISNULL protection; it returns 0, if there are no values.
Given the final data, you will see that the ValueRanking and QuantityRankings are different (I fiddled the data to get this, just to illustrate the point). What this means is that the final result can only be ordered by one of them (or indeed by any other column - order by is not dependent on ranking).
HTH

Find minimum datetime while using FK in two different tables

I have 2 tables:
COURSE
------
Id
Name
TEST
------
Id
CourseId (FK to `COURSE.ID`)
DATETIME
NUMBERS
Suppose COURSE table with ID 1,2 (only 2 columns) and TEST table with 8 numbers of data having different DATETIME and CourseId of 1 (3 columns) and 2 (6 columns).
I want to find the minimum DATETIME,CourseID and Name by joining these 2 tables. The below query is giving a 2 output:
(SELECT min([DATETIME]) as DATETIME ,[TEST].CourseID,Name
FROM [dbo].[TEST]
left JOIN [dbo].[COURSE]
ON [dbo].[TEST].CourseID=[COURSE].ID GROUP BY CourseID,Name)
I want a single column output i.e. a single output column (minimum datetime along with Name and ID)..HOW can i achieve??
With 2 courses you are always going to get 2 rows when joining like this. It will give you the minimum date value for each course. The first way you can get a single row is to use TOP 1 in your query, which will simply give you the course with the earliest test date. The other way is to use a WHERE clause to filter it by a single course.
Please run this sample code with some variations of what you can do, notes included in comments:
CREATE TABLE #course ( id INT, name NVARCHAR(20) );
CREATE TABLE #Test
(
id INT ,
courseId INT ,
testDate DATETIME -- you shouldn't use a keyword for a column name
);
INSERT INTO #course
( id, name )
VALUES ( 1, 'Maths' ),
( 2, 'Science' );
-- note I used DATEADD(HOUR, -1, GETDATE()) to simply get some random datetime values
INSERT INTO #Test
( id, courseId, testDate )
VALUES ( 1, 1, DATEADD(HOUR, -1, GETDATE()) ),
( 2, 1, DATEADD(HOUR, -2, GETDATE()) ),
( 3, 1, DATEADD(HOUR, -3, GETDATE()) ),
( 4, 2, DATEADD(HOUR, -4, GETDATE()) ),
( 5, 2, DATEADD(HOUR, -5, GETDATE()) ),
( 6, 2, DATEADD(HOUR, -6, GETDATE()) ),
( 7, 2, DATEADD(HOUR, -7, GETDATE()) ),
( 8, 2, DATEADD(HOUR, -8, GETDATE()) );
-- returns minumum date for each course - 2 rows
SELECT MIN(t.testDate) AS TestDate ,
t.courseId ,
c.name
FROM #Test t
-- used inner join as could see no reason for left join
INNER JOIN #course c ON t.courseId = c.id
GROUP BY courseId , name;
-- to get course with minimum date - 1 row
SELECT TOP 1
MIN(t.testDate) AS TestDate ,
t.courseId ,
c.name
FROM #Test t
-- used inner join as could see no reason for left join
INNER JOIN #course c ON t.courseId = c.id
GROUP BY t.courseId , c.name
ORDER BY MIN(t.testDate); -- requires order by
-- to get minimum date for a specified course - 1 row
SELECT MIN(t.testDate) AS TestDate ,
t.courseId ,
c.name
FROM #Test t
-- used inner join as could see no reason for left join
INNER JOIN #course c ON t.courseId = c.id
WHERE t.courseId = 1 -- requires you specify a course id
GROUP BY courseId , name;
DROP TABLE #course;
DROP TABLE #Test;
In my understanding, you want to return the minimum date from the entire table with the course details of that day.
Please try the below script
SELECT TOP 1 MIN(t.testDate) OVER (ORDER BY t.testDate) AS TestDate ,
t.courseId ,
c.name
FROM Test t
INNER JOIN course c ON t.courseId = c.id
ORDER BY t.testDate

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)

Resources