Selecting data from 2 specific dates gives "Division by zero" error - postgresql

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'

Related

percentage difference when using CASE WHEN clause POSTGRES

Is this the correct syntax?
I am using the traditional method of (new/old)-1 to work out the % difference between yesterday's registrations and a week from yesterday's registrations. I am getting '0' which does not seem right...
select
sum(case when (timestampregistered::date = current_date - 1) then 1 else 0 end)
/ sum(case when timestampregistered::date = current_date - 8 then 1 else 0 end) - 1 as pct_diff
from customers_table
You are doing integer arithmetics, so your answers will be integers.
Try
[...]then 1.0 else 0.0 end

How can i get a week range for a given month in Postgress

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 need to force time to char, so that the deployment looks cleaner

First problem is that I have this output if I run the code in pgAdmin3:
But when deployed I get this:
Also can I replace negative time with text like this:
CASE WHEN TRUNC(EXTRACT(EPOCH FROM ((CASE WHEN wo.RESPONDEDTIME= 0
AND wo.RESOLVEDTIME= 0
THEN DATE_TRUNC('second', CURRENT_TIMESTAMP)
ELSE CASE WHEN wo.RESPONDEDTIME= 0
THEN TO_TIMESTAMP(wo.RESOLVEDTIME / 1000)
ELSE TO_TIMESTAMP(wo.RESPONDEDTIME / 1000)
END
END) - (TO_TIMESTAMP(wo.CREATEDTIME / 1000)
+ (((sla.fr_duebydays * 1440)
+ (sla.fr_duebyhours * 60)
+ sla.fr_duebyminutes) * interval '1 minute'))))/60) <0
THEN **'overdue'**
I've figured out both of my problems. The first one (the one with time-stamp formatting problems) had to do with the fact that I was interpreting PostgreSQL query in another application (JasperSoft) in this case.
The workaround was to_char the whole expression. Like this:
TO_CHAR
(
AGE( CASE WHEN wo.RESPONDEDTIME= 0 AND wo.RESOLVEDTIME= 0
THEN DATE_TRUNC('second', CURRENT_TIMESTAMP)
ELSE CASE WHEN wo.RESPONDEDTIME= 0
THEN TO_TIMESTAMP(wo.RESOLVEDTIME / 1000)
ELSE TO_TIMESTAMP(wo.RESPONDEDTIME / 1000)
END END ,
TO_TIMESTAMP(wo.CREATEDTIME / 1000) + (((sla.fr_duebydays * 1440) + (sla.fr_duebyhours * 60) + sla.fr_duebyminutes) * interval '1 minute')
), 'DD HH24:MI')
The other problem I had was with some negative time-stamp values that I would get from my query. The solution there was that once the expression was to_char, I included the whole thing into a "case" expression.
Like this:
CASE WHEN wo.RESPONDEDTIME <= 0 -- this case gets read of the negative values in the table
THEN
TO_CHAR
(
AGE( CASE WHEN wo.RESPONDEDTIME= 0 AND wo.RESOLVEDTIME= 0
THEN DATE_TRUNC('second', CURRENT_TIMESTAMP)
ELSE CASE WHEN wo.RESPONDEDTIME= 0
THEN TO_TIMESTAMP(wo.RESOLVEDTIME / 1000)
ELSE TO_TIMESTAMP(wo.RESPONDEDTIME / 1000)
END END ,
TO_TIMESTAMP(wo.CREATEDTIME / 1000) + (((sla.fr_duebydays * 1440) + (sla.fr_duebyhours * 60) + sla.fr_duebyminutes) * interval '1 minute')
), 'DD HH24:MI')
ELSE 'on time' END
AS "Response SLA Violation"

Postgresql: Change COUNT value based on condition

So this is my table in database:
Worker X have this work result BETWEEN '2015-06-01' AND '2015-06-06':
What I want to do is to count the number of work days but my condition is that if (nb_heures + nb_heures_s) > 4 I count it 1 day but if (nb_heures + nb_heures_s) <= 4 I count it 0.5 day.
So the result I must get from this table 5.5 work days and not 6.
I tried this query but it's not working well:
SELECT
count(CASE WHEN (nb_heures + nb_heures_s) > 4 THEN 1 END) as full_day_work,
count(CASE WHEN (nb_heures + nb_heures_s) <= 4 THEN 0.5 END) as half_day_work
FROM pointage_full pf
WHERE date_pointage BETWEEN '2015-06-01' AND '2015-06-06'
AND pf.id_salarie = 5
How can I reach my objectif ?
COUNT(expr) always returns a bigint, as it simply returns the number of rows for which expr is not NULL.
You can use SUM instead :
SELECT SUM(CASE WHEN (nb_heures + nb_heures_s) > 4 THEN 1 ELSE 0.5 END) as number_of_days
FROM pointage_full pf
WHERE date_pointage BETWEEN '2015-06-01' AND '2015-06-06';

Sum up items between setup of custom times

We need to count the number of items that occur 10 minutes before and 10 minutes after the hour, by day. We have a table that tracks the items individually. Ideally i would like to have the output be something like the below, but and totally open to other suggestions.
Table - Attendance
Att_item timestamp
1 2012-09-12 18:08:00
2 2012-09-01 23:26:00
3 2012-09-23 09:33:00
4 2012-09-11 09:43:00
5 2012-09-06 05:57:00
6 2012-09-17 19:26:00
7 2012-09-06 10:51:00
8 2012-09-19 09:42:00
9 2012-09-06 13:55:00
10 2012-09-05 07:26:00
11 2012-09-02 03:08:00
12 2012-09-19 12:17:00
13 2012-09-12 18:14:00
14 2012-09-12 18:14:00
Output
Date Timeslot_5pm Timeslot_6pm Timeslot_7pm
9/11/2012 11 22 22
9/12/2012 30 21 55
9/13/2012 44 33 44
Your requirements are not totally clear, but if you only want to count the number of records in the 20 minute window:
select cast(tstmp as date) date,
sum(case when datepart(hour, tstmp) = 1 then 1 else 0 end) Timeslot_1am,
sum(case when datepart(hour, tstmp) = 2 then 1 else 0 end) Timeslot_2am,
sum(case when datepart(hour, tstmp) = 3 then 1 else 0 end) Timeslot_3am,
sum(case when datepart(hour, tstmp) = 4 then 1 else 0 end) Timeslot_4am,
sum(case when datepart(hour, tstmp) = 5 then 1 else 0 end) Timeslot_5am,
sum(case when datepart(hour, tstmp) = 6 then 1 else 0 end) Timeslot_6am,
sum(case when datepart(hour, tstmp) = 7 then 1 else 0 end) Timeslot_7am,
sum(case when datepart(hour, tstmp) = 8 then 1 else 0 end) Timeslot_8am,
sum(case when datepart(hour, tstmp) = 9 then 1 else 0 end) Timeslot_9am,
sum(case when datepart(hour, tstmp) = 10 then 1 else 0 end) Timeslot_10am,
sum(case when datepart(hour, tstmp) = 11 then 1 else 0 end) Timeslot_11am,
sum(case when datepart(hour, tstmp) = 12 then 1 else 0 end) Timeslot_12pm,
sum(case when datepart(hour, tstmp) = 13 then 1 else 0 end) Timeslot_1pm,
sum(case when datepart(hour, tstmp) = 14 then 1 else 0 end) Timeslot_2pm,
sum(case when datepart(hour, tstmp) = 15 then 1 else 0 end) Timeslot_3pm,
sum(case when datepart(hour, tstmp) = 16 then 1 else 0 end) Timeslot_4pm,
sum(case when datepart(hour, tstmp) = 17 then 1 else 0 end) Timeslot_5pm,
sum(case when datepart(hour, tstmp) = 18 then 1 else 0 end) Timeslot_6pm,
sum(case when datepart(hour, tstmp) = 19 then 1 else 0 end) Timeslot_7pm,
sum(case when datepart(hour, tstmp) = 20 then 1 else 0 end) Timeslot_8pm,
sum(case when datepart(hour, tstmp) = 21 then 1 else 0 end) Timeslot_9pm,
sum(case when datepart(hour, tstmp) = 22 then 1 else 0 end) Timeslot_10pm,
sum(case when datepart(hour, tstmp) = 23 then 1 else 0 end) Timeslot_11pm
from yourtable
where datepart(minute, tstmp) >= 50
or datepart(minute, tstmp) <= 10
group by cast(tstmp as date)
If you want to count the number of records within each hour plus the records that are in the >=50 and <= 10 timeframe, then you will have to adjust this.
This does just one column (well 4 but you get my point).
select DATEPART(YYYY, FTSdate) as [year], DATEPART(mm, FTSdate) as [month]
, DATEPART(dd, FTSdate) as [day], DATEPART(hh, FTSdate) as [hour], COUNT(*)
from [Gabe2a].[dbo].[docSVsys]
where DATEPART(mi, FTSdate) >= 50 or DATEPART(mi, FTSdate) <= 10
group by DATEPART(YYYY, FTSdate), DATEPART(mm, FTSdate), DATEPART(dd, FTSdate), DATEPART(hh, FTSdate)
order by DATEPART(YYYY, FTSdate), DATEPART(mm, FTSdate), DATEPART(dd, FTSdate), DATEPART(hh, FTSdate)
Separate columns.
select DATEPART(YYYY, FTSdate) as [year], DATEPART(mm, FTSdate) as [month]
, DATEPART(dd, FTSdate) as [day]
, sum(case when DATEPART(hh, FTSdate) = '0' then 1 else 0 end) as [0:00] -- midnight
, sum(case when DATEPART(hh, FTSdate) = '1' then 1 else 0 end) as [1:00]
, sum(case when DATEPART(hh, FTSdate) = '2' then 1 else 0 end) as [2:00]
, sum(case when DATEPART(hh, FTSdate) = '3' then 1 else 0 end) as [3:00]
, sum(case when DATEPART(hh, FTSdate) = '4' then 1 else 0 end) as [4:00]
from [Gabe2a].[dbo].[docSVsys]
where DATEPART(mi, FTSdate) >= 50 or DATEPART(mi, FTSdate) <= 10
group by DATEPART(YYYY, FTSdate), DATEPART(mm, FTSdate), DATEPART(dd, FTSdate)
order by DATEPART(YYYY, FTSdate), DATEPART(mm, FTSdate), DATEPART(dd, FTSdate)