SQL Query to get oldest date - sql

I'm new to SQL and have a large database that contains IDs and Service Dates and I need to write a query to give me the first date each ID had a service.
I tried:
SELECT dbo.table.ID, dbo.otherTable.ServiceDate AS EasliestDate
FROM dbo.table INNER JOIN dbo.table.ID = dbo.otherTable.ID
But the output is every service for every ID, which has too many results to sort through. I want the output to only show the ID and the oldest service date. Any advice is appreciated.
EDIT: To be more precise, the output I am looking for is the ID and service date if the oldest service date is during the year that I specify. I.E. if ID = 1 has a service in 2015 and 2016 and I am searching for IDs in 2016 then ID = 1 should not appear in the results because there was an earlier service in 2015.
EDIT: Thanks everyone who helped with this! The answer I accepted did exactly what I asked. Major kudos to Patty though who who elaborated on how to further filter the outcome by year.

Use GROUP BY and MIN to get the first date for each ID:
SELECT dbo.table.ID,
MIN(dbo.otherTable.ServiceDate) AS EasliestDate
FROM dbo.table
INNER JOIN otherTable
ON dbo.table.ID = dbo.otherTable.ID
GROUP BY dbo.table.ID;
ADDENDUM
In reference to a question in the comments:
how would I also restrict it to show only those who had a service in a specific year?
It would depend on your exact requirements, consider the following set:
ID ServiceDate
--------------------
1 2014-05-01
1 2015-08-01
1 2016-07-07
2 2015-08-19
You would only want to include ID = 1 if the year you specified was 2016, but assuming you still wanted to return the first date of 2014-05-01 then you would need to add a having clause with a case statement to get this.
DECLARE #Year INT = 2016;
DECLARE #YearStart DATE = DATEADD(YEAR, #Year - 1900, '19000101'),
#YearEnd DATE = DATEADD(YEAR, #Year - 1900 + 1, '19000101');
SELECT #YearStart, #YearEnd
SELECT t.ID,
MIN(o.ServiceDate) AS EasliestDate
FROM dbo.table AS t
INNER JOIN otherTable AS o
ON o.ID = r.ID
GROUP BY t.ID
HAVING COUNT(CASE WHEN o.ServiceDate >= #YearStart
AND o.ServiceDate < #YearEnd THEN 1 END) > 0;
If you only want the earliest date in 2016 the a where clause would suffice
DECLARE #Year INT = 2016;
DECLARE #YearStart DATE = DATEADD(YEAR, #Year - 1900, '19000101'),
#YearEnd DATE = DATEADD(YEAR, #Year - 1900 + 1, '19000101');
SELECT #YearStart, #YearEnd
SELECT t.ID,
MIN(o.ServiceDate) AS EasliestDate
FROM dbo.table AS t
INNER JOIN otherTable AS o
ON o.ID = r.ID
WHERE o.ServiceDate >= #YearStart
AND o.ServiceDate < #YearEnd
GROUP BY t.ID;
It is worth noting there is a very good reason I have chosen to calculate the start of the year, and the start of the next year and used
WHERE o.ServiceDate >= #YearStart
AND o.ServiceDate < #YearEnd
Instead of just
WHERE DATEPART(YEAR, o.ServiceDate) = 2016;
In the former, an index on ServiceDate can be used whereas in the latter, the DATEPART calculation must be done on every record and this can cause significant performace issues.
ADDENDUM 2
To do the following:
The exact thing I want then would be IDs who's earliest service is in the year I specify.
Then you would need a having clause, just a different one to the one I posted before:
DECLARE #Year INT = 2016;
DECLARE #YearStart DATE = DATEADD(YEAR, #Year - 1900, '19000101'),
#YearEnd DATE = DATEADD(YEAR, #Year - 1900 + 1, '19000101');
SELECT #YearStart, #YearEnd
SELECT t.ID,
MIN(o.ServiceDate) AS EasliestDate
FROM dbo.table AS t
INNER JOIN otherTable AS o
ON o.ID = r.ID
GROUP BY t.ID
HAVING MIN(o.ServiceDate) >= #YearStart
AND MIN(o.ServiceDate) < #YearEnd;
ADDENDUM 3
CREATE VIEW dbo.YourView
AS
SELECT dbo.table.ID,
MIN(dbo.otherTable.ServiceDate) AS EasliestDate
FROM dbo.table
INNER JOIN otherTable
ON dbo.table.ID = dbo.otherTable.ID
GROUP BY dbo.table.ID;
Then you can apply your criteria to the view:
SELECT *
FROM dbo.YourView
WHERE EasliestDate >= '2015-01-01'
AND EasliestDate < '2016-01-01';

You have to include a WHERE in your current query:
SELECT dbo.table.ID, dbo.otherTable.ServiceDate AS EasliestDate
FROM dbo.table INNER JOIN dbo.table.ID = dbo.otherTable.ID
WHERE Month(dbo.otherTable.ServiceDate) = 1
Or you can search with Year(dbo.otherTable.ServiceDate) = 2016
Or you can use Day(dbo.otherTable.ServiceDate) = 1
Or an specific date.

use group by and min to get records. Else you can refer http://www.xaprb.com/blog/2006/12/07/how-to-select-the-firstleastmax-row-per-group-in-sql/ for better understanding.

You need to use a "Group by" statement. Try this:
SELECT dbo.table.ID, Max(dbo.otherTable.ServiceDate) AS LatestDate, Min(dbo.otherTable.ServiceDate as EarliestDate)
FROM dbo.table INNER JOIN dbo.table.ID = dbo.otherTable.ID
group by dbo.table.ID

Use nested statement to get the min date , and then just match based on ID.
select t1.ID from table1 t1 INNER JOIN
(
SELECT ID, MIN(servicedate) MinServiceDate
FROM table2
GROUP BY ID
) t2 ON t1.ID = t2.ID

Related

How to join a table which is derived from a with statement into a larger query

I have a large query which joins many different select queries to create a single table with one of the columns recording current stock. I now need to count the out of stock days in the next 99 days but since inbound stock and demand stock do not have a record for every day I have to create a table which shows the movement of stock for each day in the next 99 days by taking current stock plus the change of stock on each future dates. In order to get to this I have created the below stand alone query which returns the correct results for the change in stock per day, the problem that I now face is that I am unable to join this query to main query.
DECLARE #Date date
SET #Date = '2020-12-15'
;WITH cte AS
(
SELECT
CASE
WHEN DATEPART(Day, #Date) = 1 THEN #Date
ELSE DATEADD(day, DATEDIFF(day, 0, #Date) + 1, 0)
END AS myDate,
SKU
FROM
BS_BufferSetting
UNION ALL
SELECT
DATEADD(day, 1, myDate), SKU
FROM
cte
WHERE
DATEADD(day, 1, myDate) <= DATEADD(day, 99, #Date)
)
SELECT
myDate AS Date,
SKU,
ISNULL(Amount_IOP, 0) - ISNULL(Amount_DP, 0) AS Amount
FROM
CTE
LEFT OUTER JOIN
DP_Demand ON SKU = SKU_DP AND mydate = Date_DP
LEFT OUTER JOIN
IOP_Inbound ON SKU = SKU_IOP AND mydate = Date_IOP
The above query returns a result set like this:
Date SKU Amount
----------------------
2020-12-16 BEX -108
2020-12-17 BEX 0
2020-12-18 BEX -12
2020-12-19 BEX 0
Once I have linked this to the main query I will then need to work out how to add in the opening stock (from the main table) and formulate a running total but this is part two of the problem.
You can chain CTE's like this:
;with firstcte as
(
select 1 as MyColumn
),
AnotherCTEUsingTheFirstOne as
(
select MyColumn from firstcte
)
select * from AnotherCTEUsingTheFirstOne
so you should be able to;
DECLARE #Date date
SET #Date = '2020-12-15'
;WITH cte AS
(
SELECT
CASE
WHEN DATEPART(Day, #Date) = 1 THEN #Date
ELSE DATEADD(day, DATEDIFF(day, 0, #Date) + 1, 0)
END AS myDate,
SKU
FROM
BS_BufferSetting
UNION ALL
SELECT
DATEADD(day, 1, myDate), SKU
FROM
cte
WHERE
DATEADD(day, 1, myDate) <= DATEADD(day, 99, #Date)
),
secondCTE As
(
SELECT
myDate AS Date,
SKU,
ISNULL(Amount_IOP, 0) - ISNULL(Amount_DP, 0) AS Amount
FROM
CTE
LEFT OUTER JOIN
DP_Demand ON SKU = SKU_DP AND mydate = Date_DP
LEFT OUTER JOIN
IOP_Inbound ON SKU = SKU_IOP AND mydate = Date_IOP
)
SELECT * FROM ATable A INNER JOIN secondCTE B ON A.Col=B.Col

Filling empty dates with stored procedure in sql

I have found similar questions that have been answered but I can't seem to get it working. I have the following SQL query but I want to fill the missing dates with 0-values
SELECT
Lines.Item,
CAST(Lines.Date AS Date) AS SalesDate,
ABS(SUM(Lines.Invoiced)) AS QtySoldOnDate
FROM
Lines
WHERE
Lines.Invoiced < 0
AND Lines.Item = 'a158wa'
AND Lines.Date >= '2014-01-01'
AND Lines.Date <= '2014-12-31'
GROUP BY
Lines.Item, Lines.Date
I have found the following post, but I can't seem to get it working/figure out how to merge the two queries: What is the most straightforward way to pad empty dates in sql results (on either mysql or perl end)?
Any help would be greatly appreciated.
The easiest way is to get a list of 365 values for forming the dates. One way is with master..spt_values, something like this:
with dates as (
select dateadd(day, row_number() over (order by (select null)),
cast('2014-01-01' as date)) as thedate
from master..spt_values
)
SELECT l.Item, d.thedate AS SalesDate,
ABS(SUM(l.Invoiced)) AS QtySoldOnDate
FROM dates d left join
Lines l
ON l.Date = d.thedate and
l.Invoiced < 0 AND
l.Item = 'a158wa'
WHERE d.thedate BETWEEN '2014-01-01' AND Lines.Date <= '2014-12-31'
GROUP BY l.Item, d.theDate;
Note: You can also read the Number column directly from master..spt_values if you use type = 'P'. I'm likely to forget the type part, so I just used row_number(). Perhaps Microsoft could add a view called Numbers that did this for us.
You can Create storedprocedure with 2 input parameters and 1 output parameters
and you can check if row exists then do your job and in the else part you can set output parameter as o-values
CREATE PROCEDURE yourprocedure
#startDate Date,
#endDate Date,
#OutputPara as nvarchar(100) output
AS
BEGIN
IF EXISTS( Select Lines.Item FROM Lines [Lines.Date] Where [Lines.Date] >= #startDate AND [Lines.Date] <= #endDate)
BEGIN
SELECT
Lines.Item,
COALESCE(CAST(Lines.Date AS Date),'0') AS SalesDate,
ABS(SUM(Lines.Invoiced)) AS QtySoldOnDate
FROM
Lines
WHERE
Lines.Invoiced < 0
AND Lines.Item = 'a158wa'
AND [Lines.Date] >= #startDate
AND [Lines.Date] <= #endDate
GROUP BY
Lines.Item, [Lines.Date]
Set #OutputPara =' result exists'
END
ELSE
BEGIN
SET #OutputPara='0-results';
END
END
GO
I'm assuming you want the days where nothing is sold, to have its own row with 0 as it's value. This should do it perhaps with a few tweaks from you since I don't have your table or any data.
DECLARE #startDate DATE,
#endDate DATE;
SET #startDate = '2014-01-01';
SET #endDate = '2014-12-31';
--Generates table of each day in range
WITH cte_dates
AS
(
SELECT #startDate AS startDate
UNION ALL
SELECT DATEADD(DAY,1,startDate)
FROM cte_dates
WHERE startDate <= #endDate
)
SELECT cte_dates.startDate,
Lines.Item,
CAST([Lines.Date] AS Date) AS SalesDate,
ISNULL(ABS(SUM(Lines.Invoiced)),0) AS QtySoldOnDate
FROM cte_dates
--Left join makes it where if there is no date in Lines, then cte_dates will be there with nulls for columns in your table Lines
LEFT JOIN Lines
ON cte_dates.startDate = Lines.[Date]
WHERE
Lines.Invoiced < 0
AND Lines.Item = 'a158wa'
AND Lines.Date BETWEEN #startDate AND #endDate
GROUP BY Lines.Item,Lines.[Date],cte_dates.startDate
--It's a recursive CTE. This allows it recursively iterate enough times to generate the list of dates
OPTION (MAXRECURSION 0)
Theoretical results:
StartDate Item SalesDate QtySOldOnDate
---------------------------------------------------
2014-01-01 Item1 2014-01-01 500
2014-01-02 NULL NULL 0
2014-01-03 Item2 2014-01-03 250

How to group by month and year for entire calender year until this month in SQL

I have this query
DECLARE #DATE datetime
SELECT #Date = '2014-04-01'
SELECT #Date, COUNT(*) FROM Claim C
INNER JOIN Prop_Vehicles PV ON PV.Prop = C.Prop
WHERE PV.Vehicle IN (1,2) AND
C.DateCreate >= #DATE AND
ClaimCodeId =5
I want to group by month wise for the calnder year. For example
April 2014 - 200
May 2014 - 300
June 2014 - 500
.
.
october 2014 - 100
something like this. How to achieve it? Could someone help me how to split #Date into two fields and also group by month year wise until current month like I mentioned above?
I reckon datepart function would do? Let me also check that.
Thank you in advance.
In case some months don't have data then this would skip those months.
If you want all months data even if value is zero, then you need to construct months table and join with it
SELECT DATEADD(MONTH, DATEDIFF(MONTH,0,C.DateCreate), 0), COUNT(*) FROM Claim C
INNER JOIN Prop_Vehicles PV ON PV.Prop = C.Prop
and PV.Vehicle IN (1,2) AND
and C.DateCreate >= #DATE AND
AND ClaimCodeId =5
group by DATEADD(MONTH, DATEDIFF(MONTH,0,C.DateCreate), 0)
as per latest comment
here is the way to get all months data and also to display year and month
DECLARE #DATE datetime
SELECT #Date = '2014-04-01'
;with cte
as
(
select DATEADD(month, datediff(month,0,#Date), 0) as monthVal,1 as N
union all
select DATEADD(month, datediff(month,0,#Date)+N, 0) as monthVal, N+1
FROM cte
WHERE n <=5
)
SELECT DATENAME(year, monthval) as Year, datename(month,monthVal) as Month, COUNT(*) FROM
cte
left join Claim C
on DATEADD(month, datediff(month,0,C.DAteCreate)= cte.monthVal
INNER JOIN Prop_Vehicles PV ON PV.Prop = C.Prop
and PV.Vehicle IN (1,2) AND
and C.DateCreate >= #DATE AND
AND ClaimCodeId =5
group by DATENAME(year, monthval) , datename(month,monthVal)

Using select query for between where clause

I have to find out a date falls between two other dates which are selected from a different table in Microsoft SQL Server
I.e. I want to do something like
Select A.* from ( select member.key,
case when effective_date between (select month_start and month_end
from sales_month
where month=2 and year=2013) bucket_1
then 1 else 0 from member ) as A
where a.bucket_1 != 0
I have to duplicate case statement for different months. Any ideas / help?
Thanks
Shankar.
This could be done with a JOIN:
SELECT m.*
FROM member m
JOIN sales_month sm
ON sm.month = 2
AND sm.year = 2013
AND m.effective_date BETWEEN sm.month_start AND sm.month_end;
Use variables to hold the information.
DECLARE #StartDate DATETIME,
#EndDate DATETIME
SELECT #StartDate = month_start
, #EndDate = month_end
FROM sales_month
WHERE [month] = 2 AND [year] = 2013
SELECT *
from member
where effective_date BETWEEN #StartDate AND #EndDate
If there are no duplicates that you expect, you can use a join:
select m.*
from member m join
sales_month sm
on m.effective_date between sm.month_start and sm.month_end and
sm.month = 2 and sm.year = 2013
Otherwise, try a correlated subquery with exists:
select m.*
from member m
where exists (select 1 from sales_month where month=2 and year=2013 and m.effective_date between month_start and month_end)
Not sure why you need to get the month and year from a table. You're looking for all members whose effective date falls in February of 2013, right? What is sales_month doing for you here?
DECLARE #m INT, #y INT;
SELECT #m = 2, #y = 2013;
DECLARE #d DATETIME;
SET #d = DATEADD(MONTH, #m-1, DATEADD(YEAR, #y-1900, 0));
SELECT m.* -- don't use SELECT *!
FROM dbo.member AS m -- always use schema prefix!
WHERE effective_date >= #d
AND effective_date < DATEADD(MONTH, 1, #d); -- don't use BETWEEN!
If it needs to come from a table (e.g. your sales month is not the same as a calendar month for some reason), then use a join, but I'm not sure what your BETWEEN logic really means. I'll leave it in in case it's logical, but in most cases it's not.
SELECT m.*
FROM dbo.member AS m
INNER JOIN dbo.sales_month AS s
ON m.effective_date BETWEEN s.month_start AND s.month_end
WHERE s.[month] = 2 AND s.[year] = 2013; -- don't use reserved words as column names!
Relevant links:
Bad habits to kick : using SELECT * / omitting the column list
Bad habits to kick : avoiding the schema prefix
What do BETWEEN and the devil have in common?
Bad habits to kick : mis-handling date / range queries

Identify full vs half yearly datasets in SQL

I have a table with two fields of interest for this particular exercise: a CHAR(3) ID and a DATETIME. The ID identifies the submitter of the data - several thousand rows. The DATETIME is not necessarily unique, either. (The primary keys are other fields of the table.)
Data for this table is submitted every six months. In December, we receive July-December data from each submitter, and in June we receive July-June data. My task is to write a script that identifies people who have only submitted half their data, or only submitted January-June data in June.
...Does anyone have a solution?
For interest, this is what I wound up using. It was based off Stephen's answer, but with a few adaptations.
It's part of a larger script that's run every six months, but we're only checking this every twelve months - hence the "If FullYear = 1". I'm sure there's a more stylish way to identify the boundary dates, but this seems to work.
IF #FullYear = 1
BEGIN
DECLARE #FirstDate AS DATETIME
DECLARE #LastDayFirstYear AS DATETIME
DECLARE #SecondYear AS INT
DECLARE #NewYearsDay AS DATETIME
DECLARE #LastDate AS DATETIME
SELECT #FirstDate = MIN(dscdate), #LastDate = MAX(dscdate)
FROM TheTable
SELECT #SecondYear = DATEPART(yyyy, #FirstDate) + 1
SELECT #NewYearsDay = CAST(CAST(#SecondYear AS VARCHAR)
+ '-01-01' AS DATETIME)
INSERT INTO #AuditResults
SELECT DISTINCT
'Submitter missing Jan-Jun data', t.id
FROM TheTable t
WHERE
EXISTS (
SELECT 1
FROM TheTable t1
WHERE t.id = t1.id
AND t1.date >= #FirstDate
AND t1.date < #NewYearsDay )
AND NOT EXISTS (
SELECT 1
FROM TheTable t2
WHERE t2.date >= #NewYearsDay
AND t2.date <= #LastDate
AND t2.id = t.id
GROUP BY t2.id )
GROUP BY t.id
END
From your description, I wouldn't worry about the efficiency of the query since apparently it only needs to run twice a year!
There are a few ways to do this, which one is 'best' depends on the data that you have. The datediff (on max/min date values) you suggested should work, another option is to just count records for each submitted within each date range, e.g.
select * from (
select T.submitterId,
(select count(*)
from TABLE T1
where T1.datefield between [july] and [december]
and T1.submitterId = T.submitterId
group by T1.submitterId) as JDCount,
(select count(*)
from TABLE T2
where T2.datefield between [december] and [june]
and T2.submitterId = T.submitterId
group by T2.submitterId) as DJCount
from TABLE T) X
where X.JDCount <= 0 OR X.DJCount <= 0
Caveat: untested query off the top of my head; your mileage may vary.
I later realised that I was supposed to check to make sure that there was data for both July to December and January to June. So this is what I wound up in v2:
SELECT #avgmonths = AVG(x.[count])
FROM ( SELECT CAST(COUNT(DISTINCT DATEPART(month,
DATEADD(month,
DATEDIFF(month, 0, dscdate),
0))) AS FLOAT) AS [count]
FROM HospDscDate
GROUP BY hosp
) x
IF #avgmonths > 7
SET #months = 12
ELSE
SET #months = 6
SELECT 'Submitter missing data for some months' AS [WarningType],
t.id
FROM TheTable t
WHERE EXISTS ( SELECT 1
FROM TheTable t1
WHERE t.id = t1.id
HAVING COUNT(DISTINCT DATEPART(month,
DATEADD(month, DATEDIFF(month, 0, t1.Date), 0))) < #months )
GROUP BY t.id