Find minimum datetime while using FK in two different tables - sql-server

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

Related

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

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

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)

find products that have no sales against them which are nine months old or older

I need to find all product codes that have no sales against them for the last 9 months so I can delete them.
This is what I have so far:
SELECT PI.ID as 'Product ID', ItemDescription, SOD.ID as 'Sales ID', so.despatchdate
FROM ProductItem as PI LEFT OUTER JOIN saleorderdetail as SOD
ON PI.ID = SOD.productitemid
inner join saleorder as SO
on SO.id = sod.saleorderid
where SO.despatchdate <= (getdate() - 273);
The result is not correct.
Use not exists:
select * from ProductItem pi
where not exists(select * from saleorderdetail sod
inner join saleorder so so.id = sod.saleorderid
where pi.ID = sod.productitemid and so.despatchdate >= (getdate() - 273))
The following will be more correct:
select * from ProductItem pi
where not exists(select * from saleorderdetail sod
inner join saleorder so so.id = sod.saleorderid
where pi.ID = sod.productitemid and dateadd(m, -9, getdate()))
declare #products table
(
Id int primary key not null
, Name nvarchar(32) not null
)
insert #products values( 1, 'Has Recent Sales Order' )
insert #products values( 2, 'Does Not Have Recent Sales Order' )
declare #sos table
(
Id int primary key not null,
DespatchDate datetime not null
)
insert #sos values ( 1, getdate() )
insert #sos values ( 2, DATEADD( month, -10, getdate() ) )
declare #sods table
(
ProductId int not null,
SaleOrderId int not null
)
insert #sods values( 1, 1 )
insert #sods values( 2, 2 )
-- select only those products that do not have a recent sales order
SELECT
PI.ID as 'Product ID'
, PI.Name -- for demonstrative purposes only
FROM
#products as PI
LEFT OUTER JOIN #sods as SOD
ON PI.ID = SOD.ProductId
left outer join #sos as SO
on SO.id = sod.saleorderid
-- retreive sales orders within past 9 months only
and DATEADD( month, -9, getdate() ) <= SO.despatchdate
group by
PI.ID
, PI.Name -- for demonstrative purposes only
having
-- only sales orders in past 9 months were returned
-- so filter for no sales orders
count(so.id) = 0

Consecutive Day Query

I have a table of contactIDs and datetimes, the time being when a letter was generated for the contact. Each contact can only have one letter generated a day. I want to write a query to select any contact that has had letters generated on more than one consecutive day.
I guess I'd need to increment the datetime as records are found but how would I do this separately for each contact?
select contactid from ContactTable a inner join Contacttable B on a.contctid=b.contactid and datediff(day,a.date,b.date)=1
I decided to utilise a calendar table. Use your favourite search engine to find a script to create a calendar table.
Alternatively, here's one I made earlier
So here's the query I have rolled with in full, I will explain the detail of it afterwards
DECLARE #your_table table (
contact_id int
, created_on datetime
);
INSERT INTO #your_table (contact_id, created_on)
SELECT 9, '2014-01-02 06:00'
UNION ALL SELECT 9, '2014-01-02 18:00'
UNION ALL SELECT 9, '2014-01-05 08:00'
UNION ALL SELECT 9, '2014-01-07 01:00'
UNION ALL SELECT 3, '2014-01-02 00:01'
UNION ALL SELECT 3, '2014-01-03 23:59' -- Over 24 hours but a "day" different
UNION ALL SELECT 7, '2014-01-04 01:00'
UNION ALL SELECT 7, '2014-01-06 01:00'
UNION ALL SELECT 7, '2014-01-08 01:00'
UNION ALL SELECT 7, '2014-01-09 01:00'
UNION ALL SELECT 7, '2014-01-10 01:00'
UNION ALL SELECT 7, '2014-01-11 01:00'
;
; WITH x AS (
SELECT your_table.contact_id
, your_table.created_on
, calendar.the_date
, Row_Number() OVER (PARTITION BY your_table.contact_id ORDER BY calendar.the_date) As sequence
FROM #your_table As your_table
INNER
JOIN dbo.calendar
ON your_table.created_on >= calendar.the_date
AND your_table.created_on < DateAdd(dd, 1, calendar.the_date)
)
, y AS (
SELECT curr.contact_id
, curr.created_on
, curr.the_date As the_date
, prev.the_date As previous_date
, DateDiff(dd, prev.the_date, curr.the_date) As difference_in_days
FROM x As curr
LEFT
JOIN x As prev
ON curr.contact_id = prev.contact_id
AND curr.sequence = prev.sequence + 1
)
SELECT contact_id
, created_on
, the_date
, previous_date
, difference_in_days
FROM y
WHERE difference_in_days = 1
Because you didn't provide any sample data that's where I had to start, so the query is self-contained using a table variable (#your_table) as its source.
Once populated we start out with a couple of Common-Table Expressions (CTE for short). Read up here if you're not familiar with the concept: http://msdn.microsoft.com/en-us/library/ms175972.aspx . There's not a lot of difference between these and subqueries.
Our first CTE (x) joins #your_table to the calendar table. It does this by returning the single row from the calendar on which the created_on date lies, by checking that it is greater than (or equal to) the calendar date and less than the next calendar date (DateAdd()).
Once complete we use the windowed function - Row_Number() to provide some sequencing.
We partition (i.e. reset the sequence) for each contact_id and sort the sequence by the created_on date.
Moving on to the second CTE (y): we perform a self-join on CTE x joining each contact record with its "previous" based on the sequencing.
This allows us to work out the difference in days (DateDiff()) between the current and the previous records.
Finally we reduce our resultset to only those records where the difference in days is 1 i.e. contacts on consecutive days

SQL Server - Get customers with nth order in specific date range

I'm tasked with the following:
Select a list of all customers who had their nth order during a certain date range (usually a specific month).
This list needs to contain: customer id, sum of first n orders
My tables are something like this:
[dbo.customers]: customerID
[dbo.orders]: orderID, customerID,
orderDate, orderTotal
Here is what I've tried so far:
-- Let's assume our threshold (n) is 10
-- Let's assume our date range is April 2013
-- Get customers that already had n orders before the beginning of the given date range.
DECLARE #tmpcustomers TABLE (tmpcustomerID varchar(8))
INSERT INTO
#tmpcustomers
SELECT
c.customerID
FROM
orders o
INNER JOIN customers c ON o.customerID = c.customerID
WHERE
o.orderDate < '2013-04-01'
GROUP BY c.customerID
HAVING (COUNT(o.orderID) >= 10)
-- Now get all customers that have n orders sometime within the given date range
-- but did not have n orders before the beginning of the given date range.
SELECT
a.customerID, SUM(orderTotal) AS firstTenOrderTotal
SELECT
o.customerID, o.orderID, o.orderTotal
FROM
orders o
INNER JOIN customers c ON c.customerID = o.customerID
WHERE
a.customerID NOT IN ( SELECT tmpcustomerID FROM #tmpcustomers )
AND
o.orderDate > '2013-04-01'
AND
o.orderDate < '2013-05-01'
GROUP BY c.customerID
HAVING COUNT(o.orderID) >= 10
This seems to work but it's clunky and slow. Another big problem is that the firstTenOrderTotal is actually the SUM of the total amount of orders by the end of the given date range and not necessarily the first 10.
Any suggestions for a better approach would be much appreciated.
In the insert to #tmpcustomers, why are you joining back to the customer table? The order table already has the customerID that you want. Also, why are you looking for orders where the order date is before your date range? Don't you just want customers with more than n orders between a date range? This will make the second query easier.
By only having the customers with n or more orders in the table variable #tmpcustomers, you should just be able to join it and the orders table in the second query to get the sum of all the orders for those customers where you would once again limit order table records to your date range (so you do not get orders outside of that range). This will remove the having statement and the join to the customers table in your final result query.
Give this a try. Depending on your order distribution it may perform better. In this query im assembling the list of orders in the range, and then looking back to count the number of prior orders (also grabbing the orderTotal).
note: I am assuming the orderID increments as orders are placed.
If this isnt the case just use a row_number over the date to project the sequence into the query.
declare #orders table (orderID int primary key identity(1,1), customerID int, orderDate datetime, orderTotal int)
insert into #orders (customerID, orderDate, orderTotal)
select 1, '2013-01-01', 1 union all
select 1, '2013-01-02', 2 union all
select 1, '2013-02-01', 3 union all
select 2, '2013-01-25', 5 union all
select 2, '2013-01-26', 5 union all
select 2, '2013-02-02', 10 union all
select 2, '2013-02-02', 10 union all
select 2, '2013-02-04', 20
declare #N int, #StartDate datetime, #EndDate datetime
select #N = 3,
#StartDate = '2013-02-01',
#EndDate = '2013-02-20'
select o.customerID,
[total] = o.orderTotal + p.total --the nth order + total prior
from #orders o
cross
apply ( select count(*)+1, sum(orderTotal)
from #orders
where customerId = o.customerID and
orderID < o.orderID and
orderDate <= o.orderDate
) p(n, total)
where orderDate between #StartDate and #EndDate and p.n = #N
Here is my suggestion:
Use Northwind
GO
select ords.OrderID , ords.OrderDate , '<-->' as Sep1 , derived1.* from
dbo.Orders ords
join
(
select CustomerID, OrderID, ROW_NUMBER() OVER(PARTITION BY CustomerID ORDER BY OrderId DESC) AS ThisCustomerCardinalOrderNumber from dbo.Orders
) as derived1
on ords.OrderID = derived1.OrderID
where
derived1.ThisCustomerCardinalOrderNumber = 3
and ords.OrderDate between '06/01/1997' and '07/01/1997'
EDIT:::::::::
I took my CTE example, and reworked it for multiple Customers (seen below).
Give it the college try.
Use Northwind
GO
declare #BeginDate datetime
declare #EndDate datetime
select #BeginDate = '01/01/1900'
select #EndDate = '12/31/2010'
;
WITH
MyCTE /* http://technet.microsoft.com/en-us/library/ms175972.aspx */
( ShipName,ShipAddress,ShipCity,ShipRegion,ShipPostalCode,ShipCountry,CustomerID,CustomerName,[Address],
City,Region,PostalCode,Country,Salesperson,OrderID,OrderDate,RequiredDate,ShippedDate,ShipperName,
ProductID,ProductName,UnitPrice,Quantity,Discount,ExtendedPrice,Freight,ROWID) AS
(
SELECT
ShipName ,ShipAddress,ShipCity,ShipRegion,ShipPostalCode,ShipCountry,CustomerID,CustomerName,[Address]
,City ,Region,PostalCode,Country,Salesperson,OrderID,OrderDate,RequiredDate,ShippedDate,ShipperName
,ProductID ,ProductName,UnitPrice,Quantity,Discount,ExtendedPrice,Freight
, ROW_NUMBER() OVER (PARTITION BY CustomerID ORDER BY OrderDate , ProductName ASC ) as ROWID /* Note that the ORDER BY (here) is directly related to the ORDER BY (near the very end of the query) */
FROM
dbo.Invoices inv /* “Invoices” is a VIEW, FYI */
where
(inv.OrderDate between #BeginDate and #EndDate)
)
SELECT
/*
ShipName,ShipAddress,ShipCity,ShipRegion,ShipPostalCode,ShipCountry,CustomerID,CustomerName,[Address],
City,Region,PostalCode,Country,Salesperson,OrderID,OrderDate,RequiredDate,ShippedDate,ShipperName,
ProductID,ProductName,UnitPrice,Quantity,Discount,ExtendedPrice,Freight,
*/
/*trim the list down a little for the final output */
CustomerID ,OrderID , OrderDate, (ExtendedPrice + Freight) as ComputedTotal
/*The below line is the “trick”. I reference the above CTE, but only get data that is less than or equal to the row that I am on (outerAlias.ROWID)*/
, (Select SUM (ExtendedPrice + Freight) from MyCTE innerAlias where innerAlias.ROWID <= outerAlias.ROWID and innerAlias.CustomerID = outerAlias.CustomerID) as RunningTotal
, ROWID as ROWID_SHOWN_FOR_KICKS , OrderDate as OrderDate
FROM
MyCTE outerAlias
GROUP BY CustomerID ,OrderID, OrderDate, ProductName,(ExtendedPrice + Freight) ,ROWID,OrderDate
/*Two Order By Options*/
ORDER BY outerAlias.CustomerID , outerAlias.OrderDate , ProductName
/* << Whatever the ORDER BY is here, should match the “ROW_NUMBER() OVER ( ORDER BY ________ ASC )” statement inside the CTE */
/*ORDER BY outerAlias.ROWID */ /* << Or, to keep is more “trim”, ORDER BY the ROWID, which will of course be the same as the “ROW_NUMBER() OVER ( ORDER BY” inside the CTE */

Resources