Dates Datatype Operation - sql

Why does this SQL statement work:
SELECT SYSDATE - 1 - DATE '2019-01-01' FROM DUAL
and this one does not work?
SELECT 1 - SYSDATE - DATE '2019-01-01' FROM DUAL
I understand that you cannot operate a number with a date, but because of this, I don't understand why the first query works.

SYSDATE - 1 means one day before now. Oracle sees the attempt to substract (or add) a number to/from a date and automatically interprets the number as a full 24-hour day. That's why it works.
You cannot however reverse this: 1 - SYSDATE is meaningless in Oracle. It would have to mean "now before one day" which makes no sense. You can subtract/add numbers (days) to dates, but you cannot subtract a date from a number.

SELECT SYSDATE - 1 - DATE '2019-01-01' FROM DUAL
Works because the operators are evaluated in precedence order and then, for operators of the same precedence, from left-to-right so the query is, with brackets to show the order of evaluation:
SELECT ((SYSDATE - 1) - DATE '2019-01-01') FROM DUAL
And the inner bracket has the types DATE - NUMBER which results in a DATE type and then substituting that into the outer bracket, it has the types DATE - DATE which outputs NUMBER; so the query works.
SELECT 1 - SYSDATE - DATE '2019-01-01' FROM DUAL
Can be rewritten with brackets showing the evaluation order of operators as:
SELECT ((1 - SYSDATE) - DATE '2019-01-01') FROM DUAL
Which fails because 1 - SYSDATE is NUMBER - DATE which is not a valid operation.
If you want it to be syntactically correct then use brackets to change the precedence:
SELECT 1 - (SYSDATE - DATE '2019-01-01') FROM DUAL
Then SYSDATE - DATE '2019-01-01' is evaluated first and DATE - DATE = NUMBER and then NUMBER - NUMBER will work.

Related

Oracle SQL: Syntax for WHERE clause with one date between two others

Apologies in advance, feel like I'm missing something fundamental here.
I'm limiting a query WHERE one field (already in date format) is between two others:
SELECT Stuff
FROM Table
WHERE datefield BETWEEN (currentdate - 28) AND (current_date - 1)
This returns nothing. Now if I format both as dates explicitly and look for a single date:
SELECT Stuff FROM Table
WHERE TO_DATE(datefield, 'YYYY-MM-DD') = TO_DATE((current_date - 1), 'YYYY-
MM-DD')
That returns the single day's results as intended. However, if I then try something along the lines of:
SELECT Stuff FROM Table
WHERE TO_DATE(datefield, 'YYYY-MM-DD') >= TO_DATE((current_date - 28),'YYYY-
MM-DD')
This returns the entire table, including dates from long before (current_date - 28)
And finally if I try the BETWEEN after explicitly formatting:
SELECT Stuff FROM Table
WHERE TO_DATE(datefield, 'YYYY-MM-DD') BETWEEN TO_DATE((current_date - 28)
,'YYYY-MM-DD') AND TO_DATE((current_date - 1) ,'YYYY-MM-DD')
This returns nothing again.
Any help would be much appreciated.
SELECT Stuff
FROM Table
WHERE datefield BETWEEN (currentdate - 28) AND (current_date - 1)
Will return values between the 28 days before the current date at the same time of day as now and 1 day before the current date at the same time of day as now. So, if you run this at 10:23 today then it will get results before 10:23 yesterday and if your results for yesterday were all entered in the afternoon then they will not be included in the results.
SELECT Stuff
FROM Table
WHERE TO_DATE(datefield, 'YYYY-MM-DD') = TO_DATE((current_date - 1), 'YYYY-MM-DD')
TO_DATE( date_string, format_model ) takes a string as the first argument so Oracle will implicitly convert your date to a string using the NLS_DATE_FORMAT session parameter. So your query is effectively:
SELECT Stuff
FROM Table
WHERE TO_DATE(
TO_CHAR(
datefield,
( SELECT VALUE FROM NLS_SESSION_PARAMETERS WHERE PARAMETER = 'NLS_DATE_FORMAT' )
),
'YYYY-MM-DD'
)
=
TO_DATE(
TO_CHAR(
(current_date - 1),
( SELECT VALUE FROM NLS_SESSION_PARAMETERS WHERE PARAMETER = 'NLS_DATE_FORMAT' )
),
'YYYY-MM-DD'
)
Firstly, don't ever rely on an implicit conversion. Secondly, depending on what your NLS_DATE_FORMAT session parameter is your query will either: raise an exception if the format model does not match YYYY-MM-DD; or give you gibberish dates (i.e. if your default format model is RR-MM-DD then you'll lose the centuries from your dates; if it is DD-MM-YY then you'll swap days and years - which might "work" [i.e. not raise an exception] for the next decade or so and then blow up horribly in February 2029); or it will work. However, this is a session parameter so users can change it and your query will randomly stop working without changing your SQL.
Assuming that your NLS_DATE_FORMAT is YYYY-MM-DD then this will effectively truncate your dates and you could do it much simpler using:
SELECT Stuff
FROM Table
WHERE TRUNC(datefield) = TRUNC(current_date - 1)
This will get all the results (regardless of time of day) for yesterday.
What you probably want is:
SELECT Stuff
FROM Table
WHERE datefield >= TRUNC( currentdate - 28 )
AND datefield < TRUNC( current_date )
As it will get all the results from Midnight 28 days ago to before midnight today.
If this doesn't work then you need to check your date values:
SELECT TO_CHAR( datevalue, 'YYYY-MM-DD HH24:MI:SS' )
FROM table;
You will possibly find that the dates are stored with the year in the 1st century AD (i.e. 0018-10-24 09:50:00 as the value) and this will (probably) be because dates were uploaded as a string with a 2-digit year (YY-MM-DD) when a 4-digit year (YYYY-MM-DD) was expected so Oracle will assume the century is 0.

uisng to_date function still get date format picture ends before converting entire input string error

I have the following code where I want to see if a date is less than a year ago:
select id
from mytable
where id= :p_id
and (to_date(trunc(sysdate), 'yyyy-mm-dd') - to_date(datewhen, 'yyyy-mm-dd')) < 365;
I keep getting the error:
ORA-01830: date format picture ends before converting entire input
string
Looking at other question with the same error on StackOverflow I see the solution usually is to use the to_date function which I am doing so I am unsure why this is occuring. The datewhen field is of type Date.
Do not use to_date() with the columnes of DATE data type. to_date() converts character string to a value of DATE data type. It makes no sense to convert the DATE to DATE. In a first step datewhen column of type DATE will be implicitly converted into a character data type by using the default date format (that's most probably not 'yyyy-mm-dd') and this is the culprit of the ORA-01830 error.
So your statement should look something like this:
select id from mytable where id = :p_id and (trunc(sysdate) - trunc(datewhen)) < 365;
I'd calculate the difference in the months or years instead of days:
... where months_between(sysdate, datewhen) < 12
If your datewhen column is char/varchar formatted as yyyy-mm-dd then you have to do the to_date conversion on datewhen, but not on SYSDATE: it's already a date and doesn't need to be converted.
To filter on a date within the past 365 days, compare it to SYSDATE - 365:
select id
from mytable
where id = :p_id
and to_date(datewhen, 'yyyy-mm-dd') > sysdate - 365;
But a year isn't always 365 days: on leap years it's 366 days. To get a one year ago value that's always correct, subtract an interval of one year from the current date:
select id
from mytable
where id = :p_id
and datewhen > sysdate - interval '1' year;
One more thing: the Oracle DATE type isn't just a date; it's a date and a time. SYSDATE returns the current date and time. Try this query:
select to_char(sysdate, 'yyyy-mm-dd hh24:mi:ss') from dual;
Unless you run this at exactly midnight you'll see a time component as well.
Say your query runs on 2 September 2017 at 10 AM and you're looking for a date within the past year. You'd expect to get the date 3 September 2016, but you wouldn't because at 10 AM SYSDATE is 3 September 2016 at 10:00:00. That's greater than the plain date 3 September 2016, which is 3 September 2016 at 0:00:00, so records with a datewhen of `2016-09-03' won't be included.
To ignore the time component of an Oracle DATE value, use TRUNC. Your final query should look something like this:
select id
from mytable
where id = :p_id
and datewhen > trunc(sysdate) - interval '1' year;
you use TO_DATE function when the value in character format
Syntax
The syntax for the TO_DATE function in Oracle/PLSQL is:
TO_DATE( string1 [, format_mask] [, nls_language] )

How does date manipulation/comparison/grouping work in SQL queries?

I need to analyze an SQL query (and construct its equivalent in MDX). I'm not familiar with SQL and can't access the database, so there are 5 simple things I can't figure out:
What does the part WHERE idate BETWEEN trunc(SYSDATE, 'iw')-7 AND trunc(SYSDATE, 'iw')-3 mean? Specifically:
What does subtracting 7 from trunc(SYSDATE, 'iw') do? Subtract 7 weeks or 7 days? I understand the trunc(...) expression is a value 0-53 corresponding to the week of the year, but it seems to clash with the label "previous week" and stated purpose of the query.
How does SQL compare dates? Are the values from trunc(...) evaluated as dates during comparison?
The query seems to group rows together if they happened in the same minute. However, the few rows of output I can see have 10-minute granularity (00:00, 00:10, 00:20, etc.) Is there something in the query that groups rows into 10 minute intervals, or is this a result of the input data?
Why are calls to substr() and to_char() and needed in the group by condition? What would happen if trunc(idate, 'HH24:MI') was used instead?
What does the pm do? There is also a cm that seems to have a similar function. Are these part of the temporary table names?
Finally, how do the hash marks (#) affect this query? I read it might be to signify temporary tables. If so, are these temporary tables created manually, or does something in the query cause them to be created?
For reference here is the query. (On a Oracle database, if it makes any difference.) Its purpose is to "analyze how firewall accept events are trending compared to last week":
SELECT 'Previous Week Average' AS term ,
Substr(To_char(idate, 'HH24:MI'), 0, 4)
|| '0' AS event_time ,
Round(Avg(tot_accept)) AS cnt
FROM (
SELECT *
FROM st_event_100_#yyyymm-1m#
WHERE idate BETWEEN trunc(SYSDATE, 'iw')-7 AND trunc(SYSDATE, 'iw')-3 #stat_monitor_group_query#
UNION ALL
SELECT *
FROM st_event_100_#yyyymm#
WHERE idate BETWEEN trunc(SYSDATE, 'iw')-7 AND trunc(SYSDATE, 'iw')-3 #stat_monitor_group_query# ) pm
GROUP BY substr(to_char(idate, 'HH24:MI'), 0, 4)
|| '0'
UNION ALL
SELECT 'Today' AS term ,
substr(to_char(idate, 'HH24:MI'), 0, 4)
|| '0' AS event_time ,
round(avg(tot_accept)) AS cnt
FROM st_event_100_#yyyymm# cm
WHERE idate >= trunc(SYSDATE) #stat_monitor_group_query#
GROUP BY substr(to_char(idate, 'HH24:MI'), 0, 4)
|| '0'
ORDER BY term DESC,
event_time ASC
iw truncates the date to the first day of the calendar week as defined by the ISO 8601 standard, which is Monday. When you subtract numbers from the date, it is always the number of days. So, idate BETWEEN trunc(SYSDATE, 'iw')-7 AND trunc(SYSDATE, 'iw')-3 gives you those dates that fall between previous week's Monday and Friday.
to_char(idate, 'HH24:MI') gives you the time(hour and minute) part in 24hr format. Ex: 14:33. By using substrin to extract only 4 characters, you are actually getting 14:3. So yes, this groups with a granularity of 10 mins.
You cannot write trunc(idate, 'HH24:MI'). It can only have 1 precision specifier.
If you write trunc(idate,'HH24'), it truncates to the hour. If you use MI, it truncates to the minute. So, to truncate it to 10 mins is a little tricky.
pm is just an alias for the whole subquery.
SELECT *
FROM st_event_100_#yyyymm-1m#
......
WHERE idate BETWEEN trunc(SYSDATE, 'iw')-7 AND trunc(SYSDATE, 'iw')-3 #stat_monitor_group_query#
# is part of the table anme in your query. It has no significance as such. But, it might be project/company specific.

Get the records of last month in SQL

I want to get the records of last month based on my db table [note] field "date_created".
What's the sql to do this?
last month - 2015-08-30 to 2015-09-30
I used below query used from link but get last three month records from table but getting error ORA-30089: missing or invalid
select *
from note
where to_date(DATE_CREATED) > to_date(CURRENT_DATE) - to_date(INTERVAL '3 months')
Even I used below query but still getting error : ORA-01861: literal does not match format string
select *
from note
where to_date(DATE_CREATED) BETWEEN '2015-08-30 00:00:00.0' AND '2015-09-30 00:00:00.0'
It is not Oracle syntax.
If you want to use INTERVAL you can use only YEAR_TO_MONTH or DAY_TO_SECOND interval.
It will looks like this (for last 3 months):
select *
from note
where to_date(DATE_CREATED) > to_date(CURRENT_DATE) - INTERVAL '0-3' YEAR TO MONTH
Or your can use standard function ADD_MONTHS:
select *
from note
where to_date(DATE_CREATED) > ADD_MONTHS( to_date(CURRENT_DATE), -3)
ADD_MONTHS will add months to your date. It could be positive or negative
Try using TO_DATE on the literal strings as well.
select * from note where to_date(DATE_CREATED) BETWEEN TO_DATE('2015-08-30', 'YYYY-MM-DD') AND TO_DATE('2015-09-30', 'YYYY-MM-DD');

How to convert a sysdate month value to number in oracle?

Im trying to return the CARDS of my CARD table that will expire in the next month. But the problem is that the table has two columns to represent the card date. The columns are EXPIREDAY and EXPIREMONTH ,both are numbers. So when i do that query i get an error:
select * from CARD WHERE EXPIREDAY <= sysdate - interval '2' DAY;
//Oracle error: ORA-00932: inconsistent datatypes: expected NUMBER got DATE
Is there a way to convert the sysdate - interval '2' DAY as Number data type?
Thanks!
If you want to compare the values as strings you can use this to convert the SYSDATE
SELECT TO_CHAR(sysdate, 'MM') || TO_CHAR(sysdate, 'DD') MONTH_NUM FROM DUAL
-- gives you "0922"
and this for your numeric columns which will pad with leading zeros if you only have a single digit
SELECT TO_CHAR(9, 'FM00') || TO_CHAR(22, 'FM00') MONTH_NUM FROM DUAL
-- also gives you "0922"
If you have control over the table schema it would be best practise to store both the DAY and MONTH values in a single numeric field, so that 9-SEP would be stored in this column as the numeric value 0922 where the month is first so that the natural ordering is used.
A simple and not necessarily very efficient approach is to convert the day and month values into an actual date, using to_date(), and then compare that with your target date range:
select * from card
where to_date(lpad(expireday, 2, '0')
||'/'|| lpad(expiremonth, 2, '0'), 'DD/MM')
between sysdate and add_months(sysdate, 1);
Which appears to work. But this will have problems if the dates span the end of the year. Because your table doesn't specify the year, you either have to work one out, or allow to_date to default it to the current year. And if you let it default then it won't work. For example, if you have values for December and January in your table, and run this query in December, then the January dates will be seen as January 2014, and won't be counted as being in the next month. So you'll need to do more to pick the right year.
This treats any month numbers before the current one as being next year, which may be good enough for you as you only have a one-month window:
select * from card
where to_date(lpad(expireday, 2, '0')
||'/'|| lpad(expiremonth, 2, '0')
||'/'|| (extract(year from sysdate) +
case when expiremonth < extract(month from sysdate) then 1 else 0 end),
'DD/MM/YYYY')
between sysdate and add_months(sysdate, 1);
SQL Fiddle using a date range from December to January.
And you can see the ways the two columns are being combined to form a date in this Fiddle.
As so often, the moral is... store things as the right data type. Store dates as dates, not as string or numbers.
Im trying to return the CARDS of my CARD table that will expire in the next month. But the problem is that the table has two columns to represent the card date.
Assuming:
you are using floating months (say: from 23 dec. to 23 jan.) and
your table somehow only contains one (floating ?) year of data
Why can't you use simple arithmetics? Like that:
-- some constant definitions for testing purpose
with cst as (
select EXTRACT(DAY from TO_DATE('23/12','DD/MM')) as theDay,
EXTRACT(MONTH from TO_DATE('23/12','DD/MM')) as theMonth
from dual)
-- the actual query
select card.* from card,cst
where (expiremonth = theMonth AND expireday > theDay)
or (expiremonth = 1+MOD(theMonth,12) AND expireday <= theDay);
-- ^^^^^^^^^^^^^^^^^^
-- map [01 .. 12] to [02 .. 12, 01] (i.e.: next month)
This will simply select all "pseudo-dates" from tomorrow to the end of the month, as well as any one before (and including) the current day# next month.
See this example.
For something a little bit more generic, but probably more efficient than converting all your values TO_DATE, you might want to try that:
-- the calendar is the key part of the query (see below)
with calendar as (
select extract(month from sysdate + level) as theMonth,
extract(day from sysdate + level) as theDay
from DUAL connect by ROWNUM <= 8)
-- ^
-- adjust to the right number of days you are looking for
select card.* from card join calendar
on expiremonth = theMonth and expireDay = theDay
The idea here is to simply build a calendar with all the upcoming days and then join your data table on that calendar. See an example here.
Try using to_char(sysdate - interval '2' DAY,'ddmmyyyy') to convert to character type. The date format('ddmmyyyy') will depend of the value of expiredate