SQL null date comparison - sql

I have two queries offered on employee database (that we check whether they are equivalent or not), and I need to show whether we need to use IS NULL value for determining if our table would give correct result or not. We have three attributes of the salary table, which are to_date and from_date and emp_no, where by agreement to_date will have NULL if salary row is still valid (i.e. salary is still up to date for emp_no, therefore to_date is NULL).
These are queries being compared:
SELECT COUNT(*)
FROM salaries
WHERE from_date <= '1996-12-31' AND to_date >= '1996-12-01';
and
SELECT COUNT(*)
FROM salaries
WHERE from_date <= '1996-12-31' AND (to_date >= '1996-12-01' OR to_date IS NULL) ;
Goal is to find number of employees who received salary in December and only condition to receive a salary is to be employeed have salary for at least one day in December.

You are looking for employees who worked at least one day in December 1996.
This looks like a good spot to use date function overlaps():
where (from_date, coalesce(to_date, now()))
overlaps ('1996-12-01'::date, interval '1' month)
Actually the above expression has an edge cas when an employee's last day of work is December 1st (in which case I think you want to count it in, while this doesn't). So instead we could just write:
where
from_date < '1997-01-01'::date
and (to_date is null or to_date >= '1996-12-01'::date)

Related

Compare date filed with month and year in Postgres

I have a date field in one of my tables and the column name is from_dt. Now I have to compare a month and year combination against this from_dt field and check whether the month has already passed. The current database function uses separate conditions for the month and the year, but this is wrong as it will compare month and year separately. The current code is like this
SELECT bill_rate, currency FROM table_name WHERE
emp_id = employee_id_param
AND EXTRACT(MONTH FROM from_dt) <= month_param
AND EXTRACT(YEAR FROM from_dt) <= year_param
Now the fromt_dt field has value 2021-10-11. If I give month_param as 01 and year_param as 2022, this condition will not work as the month 10 is greater than 1, which I have given. Basically, I need to check whether 01-2022 (Jan 2022) is greater than r equal to 2021-10-01(October 1st, 2021). It would be very much helpful if someone can shed some light here.
If you just want to check whether one date is >= then another:
# select '2022-01-01'::date >= '2021-10-11'::date;
?column?
----------
t
If you want to restrict to year/month then:
select date_trunc('month','2022-01-01'::date) >= date_trunc('month', '2021-10-11'::date);
?column?
----------
t
Where the date_trunc components are:
select date_trunc('month','2022-01-01'::date) ;
date_trunc
------------------------
2022-01-01 00:00:00-08
select date_trunc('month','2021-10-11'::date) ;
date_trunc
------------------------
2021-10-01 00:00:00-07
See Postgres date_trunc for more information.
Assuming the given year_param and month_param are integers you can use the make_date function to create the first of the year_month and date_trunc to get the first on the month from the table. Just compare those values. (See date functions) So:
select bill_rate, currency
from table_name
where emp_id = employee_id_param
and date_trunc('month',from_dt) =
make_date( year_param, month_param, 01);

Oracle date comparison in where clause issue

I am seeing different results for a query whenever I rearrange the conditions of the where clause; I am trying to figure out why this is. I was given to try and figure out what is going on, so I could use some help to ascertain what may be causing an issue. To be clear, we are not receiving errors when running the select statement, but receiving unintended data results. This is an Oracle database (10-g I believe). We have two conditions for the same field. The first is to make sure the results are greater than 30 years ago (this database was not set my by myself, but the team that owns the application decided to change the created_date to 30 in the past was a good idea).....the second is to have a more specific date range of September 1st to October 1st.
SELECT created_date
FROM tableName
WHERE
created_date > sysdate - 11000 -- 30 years in the past
AND short_name like 'FOO%'
AND trunc(created_date) BETWEEN TO_DATE('09-01-2017', 'MM-DD-YYYY')
AND TO_DATE('10-01-2017', 'MM-DD-YYYY')
When running the query above, we receive results with a created_date outside of the condition at the end of the where clause.
However, when I place the comparison between the created_date and the sysdate AFTER the comparison between the two dates, we receive the correct results.
SELECT created_date
FROM tableName
WHERE
short_name like 'FOO%'
AND trunc(created_date) BETWEEN TO_DATE('09-01-2017', 'MM-DD-YYYY')
AND TO_DATE('10-01-2017', 'MM-DD-YYYY')
AND created_date > sysdate - 11000 -- 30 years in the past
Also, I removed the trunc portion of "AND trunc(created_date) BETWEEN TO_DATE('09-01-2017', 'MM-DD-YYYY') " to "AND created_date BETWEEN TO_DATE('09-01-2017', 'MM-DD-YYYY') " and received the intended results with the sysdate comparison before or afterward.
SELECT created_date
FROM tableName
WHERE
created_date > sysdate - 11000 -- 30 years in the past
AND short_name like 'FOO%'
AND created_date BETWEEN TO_DATE('09-01-2017', 'MM-DD-YYYY')
AND TO_DATE('10-01-2017', 'MM-DD-YYYY')
EDIT:
There are numerous records in the database. There is only 1 result with a created_date of 22-Sep-17.
SELECT created_date
FROM tableName
WHERE
created_date > sysdate - 11000 -- 30 years in the past
AND short_name like 'FOO%'
AND trunc(created_date) BETWEEN TO_DATE('09-01-2017', 'MM-DD-YYYY')
AND TO_DATE('10-01-2017', 'MM-DD-YYYY')
Produces 25 entries with a created_date of 17-Feb-04 and 1 with 22-Sep-17.
I understand sysdate - 11,000 is a little more than 30 years. The current requirement is to do a check for 11000 days and not 30 years. I was off in the year estimate. The sysdate is used because records that are considered old or updated will be placed with a created_date further than sysdate - 11000. I understand this makes no sense when there is already another date range condition in this query, but the solution has already been built and sent off. In order to make changes, I need to figure out WHY I am getting what I am getting, which is why I am posting here.
The second date range, in this case, 09-01-2017 and 10-01-2017, are parameters passed in by so they could be any range from any day in the past up until today. I understand it is a bad design, but I did not build it.
These will give two subtly different results for the values between
2017-10-01 00:00:01 and 2017-10-01 23:59:59. The query using TRUNC
will include that range where without TRUNC that range will be
excluded.
The intended result is to have on record returned back within the date range 22-Sep-17; however, we are receiving 25 records all with dates of 17-Feb-04....If I remove the created_date > sysdate - 11000 I receive the only one record as well.
The first is to make sure the results are greater than 30 years ago
You aren't subtracting 30 years, you are subtracting 11,000 days which is approximately (depending on leap years) 30 years and 42 days.
If you want to subtract 30 years then use:
ADD_MONTHS( date_value, -30 * 12 )
the second is to have a more specific date range of September 1st to October 1st.
This makes the first condition irrelevant - if it is between 2017-09-01 and 2017-10-01 then it will be within the last 30 years and you do not need that first condition.
I removed the trunc portion of "AND trunc(created_date) BETWEEN TO_DATE('09-01-2017', 'MM-DD-YYYY')" to "AND created_date BETWEEN TO_DATE('09-01-2017', 'MM-DD-YYYY')" and received the intended results with the sysdate comparison before or afterward.
These will give two subtly different results for the values between 2017-10-01 00:00:01 and 2017-10-01 23:59:59. The query using TRUNC will include that range where without TRUNC that range will be excluded.
Also, using TRUNC means that Oracle will not us an index on the created_date column (and would require a function-based index on TRUNC( created_date )). If you want to use an index then just use the column and test to see if it is greater or equal than the start of the range and less than the end of the range plus one day (to include up to 23:59:59 of that day).
The simplified equivalent of your TRUNC query is:
SELECT created_date
FROM tableName
WHERE short_name like 'FOO%'
AND created_date >= DATE '2017-09-01' -- range start
AND created_date < DATE '2017-10-01' + INTERVAL '1' DAY -- range end + 1 day

checking age of a person, the person has date of birth stored as a date

select
//
from
//
where
//this is the place i need help with
I have a table person with column dob date.
I want to be able to select rows of people aged 1 or more.
some mock date:
name dob
person 1 13-DEC-2014
person 2 24-JAN-2011
person 3 05-MAY-2013
person 4 17-APR-2014
person 5 21-DEC-2013
person 6 11-NOV-2014
in this scenario i would expect the names 'person 2', 'person 3' and 'person 5' to be listed in the output. i know how to do the select and from statement in my scenario, just not a where. any help would be greatly appreciated.
select *
from person
where dob <= add_months( trunc(sysdate), -12 )
will return everyone whose birth date is more than 12 months before the current date. sysdate returns the current date and time. trunc removes the time component (setting it to midnight). Then add_months subtracts 12 months.
I would use ADD_MONTHS():
SELECT * FROM person
WHERE dob <= TRUNC(ADD_MONTHS(SYSDATE, -12));
In the above I'm actually adding -12 months (or 1 year -- equivalent to subtracting 12 months) to the value of SYSDATE.
Calculate the threshold and compare dob to that:
where dob <= add_months(sysdate, -12))
Using a constant expression for the threshold would also mean that if an index existed on dob then it would be a candidate for usage. Even if an index didn't exist, it would still be much more efficient than ccukating a date from (every value of) dob and comparing it to today.
to_char(sysdate,'YYYY') - to_char(dob,'YYYY')>1

Find year of birth from given age in sql

can somebody help me with this problem, I know I must use sysdate. For example I have entity EMPLOYEE with ATRIBUTES Emp.ID and Age.
If we supply ADD_MONTHS with a negative number it subtracts that many months from the given date. Multiplying the AGE by -12 gives us the number of months we need to subtract from the current date to derive the approximate birthday.
SELECT Emp.ID
, TO_CHAR(
ADD_MONTHS(sysdate, (Emp.Age*-12))
, 'YYYY') as year_of_birth
FROM employee Emp;
This will not be accurate as exact month is not known
SELECT id, Name, TRUNC(sysdate - age*365) as DOB FROM Employee

Choose active employes per month with dates formatted dd/mm/yyyy

I'm having a hard time explaining this through writing, so please be patient.
I'm making this project in which I have to choose a month and a year to know all the active employees during that month of the year.. but in my database I'm storing the dates when they started and when they finished in dd/mm/yyyy format.
So if I have an employee who worked for 4 months eg. from 01/01/2013 to 01/05/2013 I'll have him in four months. I'd need to make him appear 4 tables(one for every active month) with the other employees that are active during those months. In this case those will be: January, February, March and April of 2013.
The problem is I have no idea how to make a query here or php processing to achieve this.
All I can think is something like (I'd run this query for every month, passing the year and month as argument)
pg_query= "SELECT employee_name FROM employees
WHERE month_and_year between start_date AND finish_date"
But that can't be done, mainly because month_and_year must be a column not a variable.
Ideas anyone?
UPDATE
Yes, I'm very sorry that I forgot to say I was using DATE as data type.
The easiest solution I found was to use EXTRACT
select * from employees where extract (year FROM start_date)>='2013'
AND extract (month FROM start_date)='06' AND extract (month FROM finish_date)<='07'
This gives me all records from june of 2013 you sure can substite the literal variables for any variable of your preference
There is no need to create a range to make an overlap:
select to_char(d, 'YYYY-MM') as "Month", e.name
from
(
select generate_series(
'2013-01-01'::date, '2013-05-01', '1 month'
)::date
) s(d)
inner join
employee e on
date_trunc('month', e.start_date)::date <= s.d
and coalesce(e.finish_date, 'infinity') > s.d
order by 1, 2
SQL Fiddle
If you want the months with no active employees to show then change the inner for a left join
Erwin, about your comment:
the second expression would have to be coalesce(e.finish_date, 'infinity') >= s.d
Notice the requirement:
So if I have an employee who worked for 4 months eg. from 01/01/2013 to 01/05/2013 I'll have him in four months
From that I understand that the last active day is indeed the previous day from finish.
If I use your "fix" I will include employee f in month 05 from my example. He finished in 2013-05-01:
('f', '2013-04-17', '2013-05-01'),
SQL Fiddle with your fix
Assuming that you really are not storing dates as character strings, but are only outputting them that way, then you can do:
SELECT employee_name
FROM employees
WHERE start_date <= <last date of month> and
(finish_date >= <first date of month> or finish_date is null)
If you are storing them in this format, then you can do some fiddling with years and months.
This version turns the "dates" into strings of the form "YYYYMM". Just express the month you want like this and you can do the comparison:
select employee_name
from employees e
where right(start_date, 4)||substr(start_date, 4, 2) <= 'YYYYMM' and
(right(finish_date, 4)||substr(finish_date, 4, 2) >= 'YYYYMM' or finish_date is null)
NOTE: the expression 'YYYYMM' is meant to be the month/year you are looking for.
First, you can generate multiple date intervals easily with generate_series(). To get lower and upper bound add an interval of 1 month to the start:
SELECT g::date AS d_lower
, (g + interval '1 month')::date AS d_upper
FROM generate_series('2013-01-01'::date, '2013-04-01', '1 month') g;
Produces:
d_lower | d_upper
------------+------------
2013-01-01 | 2013-02-01
2013-02-01 | 2013-03-01
2013-03-01 | 2013-04-01
2013-04-01 | 2013-05-01
The upper border of the time range is the first of the next month. This is on purpose, since we are going to use the standard SQL OVERLAPS operator further down. Quoting the manual at said location:
Each time period is considered to represent the half-open interval
start <= time < end [...]
Next, you use a LEFT [OUTER] JOIN to connect employees to these date ranges:
SELECT to_char(m.d_lower, 'YYYY-MM') AS month_and_year, e.*
FROM (
SELECT g::date AS d_lower
, (g + interval '1 month')::date AS d_upper
FROM generate_series('2013-01-01'::date, '2013-04-01', '1 month') g
) m
LEFT JOIN employees e ON (m.d_lower, m.d_upper)
OVERLAPS (e.start_date, COALESCE(e.finish_date, 'infinity'))
ORDER BY 1;
The LEFT JOIN includes date ranges even if no matching employees are found.
Use COALESCE(e.finish_date, 'infinity')) for employees without a finish_date. They are considered to be still employed. Or maybe use current_date in place of infinity.
Use to_char() to get a nicely formatted month_and_year value.
You can easily select any columns you need from employees. In my example I take all columns with e.*.
The 1 in ORDER BY 1 is a positional parameter to simplify the code. Orders by the first column month_and_year.
To make this fast, create an multi-column index on these expressions. Like
CREATE INDEX employees_start_finish_idx
ON employees (start_date, COALESCE(finish_date, 'infinity') DESC);
Note the descending order on the second index-column.
If you should have committed the folly of storing temporal data as string types (text or varchar) with the pattern 'DD/MM/YYYY' instead of date or timestamp or timestamptz, convert the string to date with to_date(). Example:
SELECT to_date('01/03/2013'::text, 'DD/MM/YYYY')
Change the last line of the query to:
...
OVERLAPS (to_date(e.start_date, 'DD/MM/YYYY')
,COALESCE(to_date(e.finish_date, 'DD/MM/YYYY'), 'infinity'))
You can even have a functional index like that. But really, you should use a date or timestamp column.