Good afternoon everyone,
I have written a Stored Procedure that as it is works and executes relatively fast as it is just doing fairly simple calculation. I guess you could say my issue with the procedure itself is the number of repeated 'CASE Statement' both in the SELECT and ORDER BY clause. My TSQL knowledge is still fairly N00bish as I am still a 'P' plate at best. Is it possible to streamline my code further so that I only have the CASE WHEN calculation appearing once and I can continue to utilize it in multiple places? I believe this will be better for future proofing as well as I only need to make the changes at the root statement without having to change it in multiple locations!
#Officer_Name is a variable passed in from the user interface. As you can see both the F_YEAR (Fiscal Year), F_Quarter (Fiscal Quarter) Field calculation is repeated again in the Order By part of the statement and I am wondering if that can be avoided :) Many many thanks in advance for rescuing this damoiseau in distress, I hope there is a generous expert with a greater level of TSQL out there who can do me this favor! Much appreciated.
BEGIN
SELECT TOP (100) PERCENT
COUNT(DISTINCT(dbo.TableA.[Account ID])) AS Applications,
SUM(CASE WHEN [Client Claims] LIKE '%claim%' THEN 1 ELSE 0 END) AS Main_Client,
COUNT([TableA_ID]) AS Clients,
(CASE
WHEN [Finalised date] < '07/01/' + CONVERT(VARCHAR(4), DATEPART(Year,GETDATE()) - 2) THEN 'PAST CASES'
WHEN [Finalised date] BETWEEN '07/01/' + CONVERT(VARCHAR(4), DATEPART(Year,GETDATE()) - 2) AND '06/30/' + CONVERT(VARCHAR(4), DATEPART(Year,GETDATE()) - 1) THEN 'YEAR'
WHEN MONTH([Finalised date]) BETWEEN 1 AND 3 THEN ' Q3'
WHEN MONTH([Finalised date]) BETWEEN 4 AND 6 THEN ' Q4'
WHEN MONTH([Finalised date]) BETWEEN 7 AND 9 THEN ' Q1'
WHEN MONTH([Finalised date]) BETWEEN 10 AND 12 THEN ' Q2'
END) AS F_Quarter,
(CASE
WHEN [Finalised date] < '07/01/' + CONVERT(VARCHAR(4), DATEPART(Year,GETDATE()) - 2) THEN CONVERT(VARCHAR(4), DATEPART(Year,GETDATE()) - 2) + ' & Older'
WHEN MONTH([Finalised date]) BETWEEN 1 AND 6 THEN convert(char(4), YEAR([Finalised date]) - 0)
WHEN MONTH([Finalised date]) BETWEEN 7 AND 12 THEN convert(char(4), YEAR([Finalised date]) + 1)
ELSE convert(char(4), YEAR([Finalised date]))
END) AS F_YEAR
FROM dbo.TableB INNER JOIN
dbo.TableA ON dbo.TableB.[Account ID] = dbo.TableA.[Account ID] LEFT OUTER JOIN
dbo.Officers ON dbo.TableA.[Account Officer] = dbo.Officers.FullName
WHERE [Case Officer] = #Officer_Name AND [Finalisation] IS NOT NULL
GROUP BY
(CASE
WHEN [Finalised date] < '07/01/' + CONVERT(VARCHAR(4), DATEPART(Year,GETDATE()) - 2) THEN CONVERT(VARCHAR(4), DATEPART(Year,GETDATE()) - 2) + ' & Older'
WHEN MONTH([Finalised date]) BETWEEN 1 AND 6 THEN convert(char(4), YEAR([Finalised date]) - 0)
WHEN MONTH([Finalised date]) BETWEEN 7 AND 12 THEN convert(char(4), YEAR([Finalised date]) + 1)
ELSE convert(char(4), YEAR([Finalised date]))
END),
(CASE
WHEN [Finalised date] < '07/01/' + CONVERT(VARCHAR(4), DATEPART(Year,GETDATE()) - 2) THEN 'PAST CASES'
WHEN [Finalised date] BETWEEN '07/01/' + CONVERT(VARCHAR(4), DATEPART(Year,GETDATE()) - 2) AND '06/30/' + CONVERT(VARCHAR(4), DATEPART(Year,GETDATE()) - 1) THEN 'YEAR'
WHEN MONTH([Finalised date]) BETWEEN 1 AND 3 THEN ' Q3'
WHEN MONTH([Finalised date]) BETWEEN 4 AND 6 THEN ' Q4'
WHEN MONTH([Finalised date]) BETWEEN 7 AND 9 THEN ' Q1'
WHEN MONTH([Finalised date]) BETWEEN 10 AND 12 THEN ' Q2'
END)
ORDER BY F_YEAR DESC, F_Quarter
END
You can put your CASE expression in a CTE and refer to it multiple times by its alias in the query to follow. However, since your CASE expression for F_YEAR is different from the one for F_Quarter there is no way to only use one CASE expression for the entire query. In pseudo-code, you can do this:
WITH cte AS (
SELECT ...
, {CASE Expression for year} AS F_Year
, {CASE Expression for quarter} AS F_Quarter
FROM...
)
SELECT ... F_Year, F_Quarter
FROM ... WHERE ...
GROUP BY F_Year, F_Quarter ...
Related
This is my current implementation
SELECT
date_trunc('month', do_date::date)::date as starting_of_the_month,
(date_trunc('month', do_date::date) + interval '1 month' - interval '1 day')::date as ending_of_the_month,
case when 1 + FLOOR((EXTRACT(DAY FROM do_date) - 1) / 7) = 1
THEN date_trunc('week', do_date)::date || ' - ' ||
(date_trunc('week', do_date) + '6 days') ::date end as week1,
case when 1 + FLOOR((EXTRACT(DAY FROM do_date) - 1) / 7) = 2
THEN date_trunc('week', do_date)::date || ' - ' ||
(date_trunc('week', do_date) + '6 days') ::date end as week2,
case when 1 + FLOOR((EXTRACT(DAY FROM do_date) - 1) / 7) = 3
THEN date_trunc('week', do_date)::date || ' - ' ||
(date_trunc('week', do_date) + '6 days') ::date end as week3,
case when 1 + FLOOR((EXTRACT(DAY FROM do_date) - 1) / 7) = 4
THEN date_trunc('week', do_date)::date || ' - ' ||
(date_trunc('week', do_date) + '6 days') ::date end as week4,
case when 1 + FLOOR((EXTRACT(DAY FROM do_date) - 1) / 7) = 5
THEN date_trunc('week', do_date)::date || ' - ' ||
(date_trunc('week', do_date) + '6 days') ::date end as week5
FROM sales_dos
WHERE date_trunc('month', do_date::date)::date >= '2021-02-01' AND date_trunc('month', do_date::date)::date < '2021-02-28'
This is my output for now :
I want the output to display as below :
Week 1 : 2021-02-01 - 2021-02-07
Week 2 : 2021-02-08 - 2021-02-14
Week 3 : 2021-02-15 - 2021-02-21
Week 4 : 2021-02-22 - 2021-02-28
Week 5 : -
Here is another way to do it (example for January 2021).
with
t as (select date_trunc('month', '2021-03-11'::date) as aday), -- any date in Jan-2021
s as
(
select d::date, d::date + 6 ed, extract('isodow' from d) wd
from t, generate_series (aday, aday + interval '1 month - 1 day', interval '1 day') d
)
select format ('Week %s', extract(day from d)::integer / 7 + 1) as weekname, d, ed
from s
where wd = 1;
So what you are looking for is a hybrid ISO with standard Calendar. You are taking the ISO week starting and ending period, but instead of all weeks being exactly 7 days you potentially truncate the 1st and/or last weeks.
The change to need for this is not actually extensive. For initial query returns the in the ISO week begin date instead of the 1st of the month. Then the main query then checks for week 1 and if so produces the 1st of the month. The only twist is determining the ISO week begin date. For this I've just included a function I have had for some time specifically for that. The change to the week_days function are marked --<<<.
create or replace function iso_first_of_week(date_in date)
returns date
language sql
immutable strict
/*
Given a date return the 1st day of the week according to ISO-8601.
I.e. Return the Date if it is Monday otherwise return the preceding Monday
*/
AS $$
with wk_adj(l_days) as (values (array[0,1,2,3,4,5,6]))
select date_in - l_days[ extract (isodow from date_in)::integer ]
from wk_adj;
$$;
create or replace
function week_dates( do_date_in date)
returns table (week_num integer, first_date date, last_date date)
language sql
immutable strict
as $$
with recursive date_list(week_num,first_date,terminate_date) as
( select 1
, iso_first_of_week(do_date_in)::timestamp --<<<
, (date_trunc('month', do_date_in) + interval '1 month' - interval '1 day')::timestamp
union all
select week_num+1, (first_date+interval '7 day'), terminate_date
from date_list
where first_date+interval '6 day' < terminate_date::timestamp
)
select week_num
, case when week_num = 1 --<<<
then date_trunc('month', do_date_in)::date --<<<
else first_date::date --<<<
end --<<<
, case when (first_date+interval '6 day')::date > terminate_date
then terminate_date::date
else (first_date+interval '6 day')::date
end last_date
from date_list;
$$;
---------- Original Reply
You can use a recursive query CTE to get the week number and first date for each week of the month specified. The main query calculates the ending date, shorting the last if necessary. Then wrap that into a SQL function to return the week number and date range for each week. See example.
create or replace
function week_dates( do_date_in date)
returns table (ween_num integer, first_date date, last_date date)
language sql
immutable strict
as $$
with recursive date_list(week_num,first_date,terminate_date) as
( select 1
, date_trunc('month', do_date_in)::timestamp
, (date_trunc('month', do_date_in) + interval '1 month' - interval '1 day')::timestamp
union all
select week_num+1, (first_date+interval '7 day'), terminate_date
from date_list
where first_date+interval '6 day' < terminate_date::timestamp
)
select week_num
, first_date::date
, case when (first_date+interval '6 day')::date > terminate_date
then terminate_date::date
else (first_date+interval '6 day')::date
end last_date
from date_list;
$$;
Response to: "How can i put the output in a single row with week1, week2, week3, week4 and week5". This is essentially the initial output that did not satisfy what you wanted. The term for this type action is PIVOT and is generally understood. It stems from transforming row orientation to column orientation. It is not overly difficult but it is messy.
IMHO this is something that belongs in the presentation layer and is not suitable for SQL. After all you are rearranging the data structure for presentation purposes. Let the database server use its natural format, use the presentation layer to reformat. This allows reuse of the queries instead of rewriting when the presentation is changed or another view of the same data is required.
If you actually want this then just use your initial query, or see the answer from
#Bohemian. However the below shows how this issue can be handled with just SQL (assuming the function week_dates was created).
select week1s
, case when week5e is null
then week4e
else week5e
end "end of month"
, week1s || ' - ' || week1e
, week2s || ' - ' || week2e
, week3s || ' - ' || week3e
, week4s || ' - ' || week4e
, week5s || ' - ' || week5e
from ( select max(case when (week_num=1) then first_date else NULL end) as week1s
, max(case when (week_num=1) then last_date else NULL end) as week1e
, max(case when (week_num=2) then first_date else NULL end) as week2s
, max(case when (week_num=2) then last_date else NULL end) as week2e
, max(case when (week_num=3) then first_date else NULL end) as week3s
, max(case when (week_num=3) then last_date else NULL end) as week3e
, max(case when (week_num=4) then first_date else NULL end) as week4s
, max(case when (week_num=4) then last_date else NULL end) as week4e
, max(case when (week_num=5) then first_date else NULL end) as week5s
, max(case when (week_num=5) then last_date else NULL end) as week5e
from week_dates(current_date)
) w ;
As before I have wrapped the above in a SQL function and provide an example here.
I would first simplify to:
extract(day from do_date)::int / 7 + 1 as week_in_month
then pivot on that using crosstab().
I have 2 same queries (to return "MonthName Year" and count) as below, but only the date range in the WHERE condition is different. Query 1 gets only the June month count, while Query 2 gets count from Apr to Jul, where the Jun month count (in Query 2) is not same as June month count from Query 1. Please advise.
Query 1:
SELECT DATENAME(MONTH, SubmissionDate) + ' ' + DateName(Year, SubmissionDate) AS MonthNumber, COUNT(1) AS InquiryCount
, Cast(Datename(MONTH,SubmissionDate) + ' ' + Datename(YEAR,SubmissionDate) AS DATETIME) AS tmp
FROM [dbo].[InvestigationDetails] (nolock)
WHERE SubmissionDate>= '06/01/2016'
AND SubmissionDate <= '06/30/2016'
GROUP BY DATENAME(MONTH, SubmissionDate) + ' ' + DateName(Year, SubmissionDate), DateName(Year, SubmissionDate)
ORDER BY tmp ASC
Query 2:
SELECT DATENAME(MONTH, SubmissionDate) + ' ' + DateName(Year, SubmissionDate) AS MonthNumber, DateName(Year, SubmissionDate), COUNT(1) AS InquiryCount
, Cast(Datename(MONTH,SubmissionDate) + ' ' + Datename(YEAR,SubmissionDate) AS DATETIME) AS tmp
FROM [dbo].[InvestigationDetails] (nolock)
WHERE SubmissionDate>= '04/01/2016'
AND SubmissionDate <= '07/31/2016'
GROUP BY DATENAME(MONTH, SubmissionDate) + ' ' + DateName(Year, SubmissionDate), DateName(Year, SubmissionDate)
ORDER BY tmp ASC
Thanks,
Jay
SubmissionDate must be of type DATETIMEand thus, you are missing all values for your last day, 06/30/2016, since this equates to 06/30/2016 00:00:00. This means any records that have SubmissionDate with a time > 00:00:00 on 6/30/2016 will be excluded. For example, 6/30/2016 12:44:22 wouldn't be included in your results with your current logic.
Use one of these instead:
AND SubmissionDate < '07/01/2016'
AND SubmissionDate <= '06/30/2016 23:59:59.999'
The first method is preferred since you will get all records before 7/1/2016, which includes 6/30/2016 23:59:59.999. Of course, you should be aware of how precise DATETIME can be in SQL Server. Run the code below to see what I mean.
declare #dt datetime2 = getdate()
select #dt --more precise with datetime2
select getdate() --not as precise
I have a table that has the datetime pieces (year, month, day, hour, minute, second, millisecond) stored as integers. I'd like to concatenate them into a single datetime column.
I've tried various approaches but none work - there seems to be no simple way to put these items together?
You can convert each part to a varchar and concatenate them together in the format of an ISO datetime string. Then use Convert to convert the string to a DateTime.
Here is an example. You would need to replace each hard coded integer with the name of the column from your table.
SELECT CONVERT(DATETIME, CAST(2016 AS VARCHAR(4)) -- year
+ '-' + CAST('0' + CAST(8 AS VARCHAR(2)) AS VARCHAR(2)) -- month
+ '-' + RIGHT('0' + CAST(13 AS VARCHAR(2)), 2) -- day of month
+ 'T' + RIGHT('0' + CAST(16 AS VARCHAR(2)), 2) -- hours (I assume its military time (24 hours))
+ ':' + RIGHT('0' + CAST(32 AS VARCHAR(2)), 2) -- minutes
+ ':' + RIGHT('0' + CAST(07 AS VARCHAR(2)), 2) -- seconds
+ '.' + RIGHT('000' + CAST(64 AS VARCHAR(3)), 3)) AS MyDate -- milliseconds
FROM yourTable
Or with column names (assumed)
SELECT CONVERT(DATETIME, CAST(yt.Year AS VARCHAR(4)) -- year
+ '-' + CAST('0' + CAST(yt.Month AS VARCHAR(2)) AS VARCHAR(2)) -- month
+ '-' + RIGHT('0' + CAST(yt.Day AS VARCHAR(2)), 2) -- day of month
+ 'T' + RIGHT('0' + CAST(yt.Hours AS VARCHAR(2)), 2) -- hours (I assume its military time (24 hours))
+ ':' + RIGHT('0' + CAST(yt.Minutes AS VARCHAR(2)), 2) -- minutes
+ ':' + RIGHT('0' + CAST(yt.Seconds AS VARCHAR(2)), 2) -- seconds
+ '.' + RIGHT('000' + CAST(yt.Milliseconds AS VARCHAR(3)), 3)) AS MyDate -- milliseconds
FROM yourTable yt
One more note. Microsoft recommends that you use DateTime2 instead of DateTime to persist date time values starting with Sql Server 2008 (which you tagged in your question).
Prior to sql server 2012, you can use a series of nested DATEADD() functions to mimic DATETIMEFROMPARTS() function
Create and populate sample data (In your next question, please save us this step)
DECLARE #T as table
(
cYear int,
cMonth int,
cDay int,
cHour int,
cMinute int,
cSecond int,
cMillisecond int
)
INSERT INTO #T VALUES(2016, 6, 22, 16, 34, 25, 3)
The query:
SELECT *,
DATEADD(MILLISECOND, cMillisecond,
DATEADD(SECOND, cSecond,
DATEADD(MINUTE, cMinute,
DATEADD(HOUR, cHour,
DATEADD(DAY, cDay -1,
DATEADD(MONTH, cMonth - 1,
DATEADD(YEAR, cYear - 2000, '2000-01-01')
)
)
)
)
)
) As TheDate
FROM #T
Results:
cYear cMonth cDay cHour cMinute cSecond cMillisecond TheDate
----- ------ ---- ----- ------- ------- ------------- -----------------------
2016 6 22 16 34 25 3 2016-06-22 16:34:25.003
Note that the base date I'm using is January 1st 2000, therefor you need to subtract 2000 from the year, 1 from the month and 1 from the days.
I have a table with the following structure: -
day, id
2016-03-13, 123
2016-03-13, 123
2016-03-13, 231
2016-03-14, 231
2016-03-14, 231
2016-03-15, 129
And I'd like to build a table that looks like: -
id, d1, d7, d14
123, 1, 1, 1
231, 1, 2, 2
129, 1, 1, 1
Essentially for a given id, list the number of days which have an entry within a time window. So if id 123 has 10 entries within the last 14 days - d14 would be 10.
So far I have: -
SELECT
day,
id
FROM
events
WHERE
datediff (DAY, day, getdate()) <= 7
GROUP BY
day,
id
This query will do:
SELECT
id,
COUNT(DISTINCT CASE WHEN current_date - day <= 1 THEN 1 END) d1,
COUNT(DISTINCT CASE WHEN current_date - day <= 7 THEN 1 END) d7,
COUNT(DISTINCT CASE WHEN current_date - day <= 14 THEN 1 END) d14
FROM
events
GROUP BY
id
ORDER BY
id
Or, since PostgreSQL 9.4, slightly more concise:
SELECT
id,
COUNT(DISTINCT day) FILTER (WHERE current_date - day <= 1) d1,
COUNT(DISTINCT day) FILTER (WHERE current_date - day <= 7) d7,
COUNT(DISTINCT day) FILTER (WHERE current_date - day <= 14) d14
FROM
events
GROUP BY
id
ORDER BY
id
try this:
SELECT id
, count(case when DAY = getdate() then 1 else null end) as d1
, count(case when DAY + 7 >= getdate() then 1 else null end) as d7
, count(case when DAY + 14 >= getdate() then 1 else null end) as d14
FROM events
WHERE DAY between DAY >= getdate() - 14
--or if you can have day > today ... and DAY between getdate() - 14 and getdate()
GROUP By id
I want to return data from yesterday and 8 days ago.
To do this I use the following line in my query:
WHERE (o.status_date::date = now()::date - INTERVAL '8 days')
OR (o.status_date::date = now()::date - INTERVAL '1 day')
However, this returns a "Division by zero" error. When I use only one of the two, so for example:
WHERE (o.status_date::date = now()::date - INTERVAL '8 days')
I get no error...
I don't understand where the error comes from, or perhaps I'm making a very straightforward mistake. Any help is appreciated!
Edited, these are the calculations done in my query:
SUM(CASE WHEN o.status_id = '12' THEN 1 ELSE 0 END) AS failed_63,
SUM(CASE WHEN o.status_id IN ('6','11','12','14','22','24') THEN 1 ELSE 0 END) AS total_orders,
ROUND(
(SUM(CASE WHEN o.status_id = '12' THEN 1 ELSE 0 END) * 100)::numeric /
(SUM(CASE WHEN o.status_id IN ('11','12','14','22','24') THEN 1 ELSE 0 END)), 2) AS perc_fail,
COUNT(DISTINCT i.order_id) AS order_issues,
ROUND(
(COUNT(DISTINCT i.order_id) * 100)::numeric / (SUM(CASE WHEN o.status_id IN ('11','12','14','22','24') THEN 1 ELSE 0 END)), 2) AS issue_rate,
SUM(CASE WHEN o.status_id = '6' THEN 1 ELSE 0 END) AS overdue_53,
ROUND(
(SUM(CASE WHEN o.status_id = '6' THEN 1 ELSE 0 END) * 100)::numeric /
(SUM(CASE WHEN o.status_id IN ('6','11','12','14','22','24') THEN 1 ELSE 0 END)), 2) AS perc_overdue,
ROUND(
(AVG(dop.vendor_confirmation_time)::numeric / 60), 2) AS avg_v_confirmation_time,
CASE
WHEN (AVG(dop.vendor_confirmation_time)::numeric / 60) < 3 THEN 'good'
WHEN (AVG(dop.vendor_confirmation_time)::numeric / 60) IS NULL THEN 'n/a'
ELSE 'bad'
END AS vendor_response
You have several cases in your query where your divisor might be 0, as in:
SUM(CASE WHEN o.status_id IN ('6','11','12','14','22','24') THEN 1 ELSE 0 END)
The best way to solve this is to use a sub-query to calculate all the sums, which are repeated anyway, and then do the division and rounding in the main query, where the divisor is not 0:
SELECT
sum12 AS failed_63,
sum6 + sum12 + sum11_24 AS total_orders,
CASE WHEN sum12 + summ11_24 > 0 THEN round(sum12 * 100. / (sum11_24 + sum 12), 2)
ELSE NULL END AS perc_fail,
order_issues,
CASE WHEN sum12 + summ11_24 > 0 THEN round(order_issues * 100. / (sum12 + sum11_24), 2)
ELSE NULL END AS issue_rate,
sum6 AS overdue_53,
CASE WHEN sum6 + sum12 + sum11_24 > 0 THEN round(sum6 / (sum6 + sum12 + sum11_24), 2)
ELSE NULL END AS perc_overdue,
round(avg_v_confirmation_time, 2) AS avg_v_confirmation_time,
CASE
WHEN (avg_v_confirmation_time) < 3 THEN 'good'
WHEN (avg_v_confirmation_time) IS NULL THEN 'n/a'
ELSE 'bad'
END AS vendor_response
FROM (
SELECT
sum(CASE WHEN o.status_id = '6' THEN 1 ELSE 0 END) AS sum6,
sum(CASE WHEN o.status_id = '12' THEN 1 ELSE 0 END) AS sum12,
sum(CASE WHEN o.status_id IN ('11','14','22','24') THEN 1 ELSE 0 END) AS sum11_24,
count(DISTINCT i.order_id) AS order_issues,
avg(dop.vendor_confirmation_time::numeric / 60) AS avg_v_confirmation_time
FROM o, i, dop
WHERE ... ) sub
In this case I set all columns where the divisor would be 0 to NULL; change as appropriate.
For future questions:
List your PostgreSQL version
Post the entire query with table qualifiers for all columns
Preferably, post the table structure
I still don't know why my first line didn't work, but I've now found a work-around by using the following:
WHERE o.status_date::date BETWEEN CURRENT_DATE - INTERVAL '8 days' AND CURRENT_DATE - INTERVAL '1 day'
AND o.status_date::date NOT BETWEEN CURRENT_DATE - INTERVAL '7 days' AND CURRENT_DATE - INTERVAL '2 days'