Comparing date stored in date and varchar columns - sql

I have a simple table, in which I have to store date information both in DATE and VARCHAR2 columns:
CREATE TABLE date_test(date_in_varchar VARCHAR2(30), date_in_date DATE);
INSERT INTO date_test values ('06/21/2017 01:30:00 AM', to_date('06/21/2017 01:01:03 AM', 'MM/DD/YYYY HH:MI:SS AM'));
INSERT INTO date_test values ('06/22/2017 02:45:00 AM', to_date('06/22/2017 05:06:01 AM', 'MM/DD/YYYY HH:MI:SS AM'));
INSERT INTO date_test values ('06/23/2017 05:51:20 AM', to_date('06/23/2017 12:00:05 AM', 'MM/DD/YYYY HH:MI:SS AM'));
As you can see, format of data is 'MM/DD/YYYY HH:MI:SS AM'. I have to prepare comparisons between date passed in variable l_attr_val format 'MM/DD/YYYY' and ones stored in the table. Now my question is: What is the best and safest way, to write queries for filters: equals, like, greater, lower, to be sure, that I won't face implicit conversion? I assume, that query would be different for values stored as date and for values stored as varchar.
CASE 1: Date is stored in DATE column and it has information about day, month, year, hour, minutes, seconds. I also have a variable (of VARCHAR2 type), that has information of only day, month, year. (l_attr_val = '06/21/2017'). Now I want to make an equality filter between this variable and values stored in table, based only on date, without time (I get 21st of June 2017 in variable and I want to see all records from 21st of June 2017, no matter the time).
SELECT * FROM date_test WHERE date_in_date = TO_DATE('06/22/2017', 'MM/DD/YYYY');
It gives me 0 rows because of the times.
CASE 2, separate from CASE 1:
CASE 1: Date is stored in VARCHAR2 column and it has information about day, month, year, hour, minutes, seconds. I also have a variable (of VARCHAR2 type), that has information of only day, month, year. (l_attr_val = '06/21/2017'). Now I want to make an equality filter between this variable and values stored in table, based only on date, without time (I get 21st of June 2017 in variable and I want to see all records from 21st of June 2017, no matter the time).
SELECT * FROM date_test WHERE TO_DATE(date_in_varchar, 'MM/DD/YYYY HH:MI:SS AM' = TO_DATE('06/22/2017', 'MM/DD/YYYY');
So, what is the safest way to compare dates in above mentioned cases?

Case 1: Properly stored dates (date type)
You could convert your date_in_date column to drop time part but this would not take advantage of non-functional index on this colum so I would advise not to go that way. Instead, if 06/22/2017 is your VARCHAR2 type variable you could also modify it on the fly to have one more value which is incremented by 1 day. This would make use of plain index on date_in_date function and the query would look like:
SELECT * FROM date_test
WHERE date_in_date >= TO_DATE('06/22/2017', 'MM/DD/YYYY')
AND date_in_date < TO_DATE('06/23/2017', 'MM/DD/YYYY')
You could also add +1 after the second part and use the same variable to save you the time for doing math by yourself on a variable.
Case 2: Dates stored as varchars
You need to convert your date_in_varchar to date without time part (or with it, and then partially apply logic proposed for case 1) and also convert your input variable the same way. If you decide to convert it to date without time part then below query would take advantage of functional index (but not a plain one):
SELECT * FROM date_test
WHERE TO_DATE(SUBSTR(date_in_varchar, 0, 12), 'MM/DD/YYYY') = TO_DATE('06/22/2017', 'MM/DD/YYYY')
Should you decide on converting it to a date with time part query would be similar to case 1, but with proper formating. I would advise you to create a functional index on this conversion to date with trimming the varchar to characters containing only date if your queries are going to be accurate up to one day.

If there are indexes on these columns I would use:
Case 1:
select * from date_test
where date_in_date >= to_date('06/22/2017', 'MM/DD/YYYY')
and date_in_date < to_date('06/22/2017', 'MM/DD/YYYY') + 1;
Case 2:
select * from date_test where date_in_varchar like '06/22/2017%'

After some more research I used below approach:
Case 1: Properly stored dates (date type)
SELECT * FROM date_test
WHERE TRUNC(date_in_date)= to_date('06/22/2017', 'MM/DD/YYYY', 'nls_date_language = ENGLISH');
Case 2: Dates stored as varchars
SELECT * FROM date_test
WHERE TRUNC(TO_DATE(date_in_varchar, 'MM/DD/YYYY HH:MI:SS AM')) = to_date('06/22/2017', 'MM/DD/YYYY', 'nls_date_language = ENGLISH');

Related

Filtering a dataset by date and time Oracle SQL through Power BI

I'm having trouble with filtering a date and time for anything two hours before and sooner. I tried this:
SELECT *
FROM
table
where
date >= sysdate - 1
AND
TO_DATE( Time, 'HH24:MI:SS' ) >= TO_DATE( sysdate, 'HH24:MI:SS' ) - 2
But I'm getting an inconsistent type error which is what I thought I was handling with the TO_DATE() function but I guess not.
sysdate is already a date (and time), so TO_DATE( sysdate, 'HH24:MI:SS' ) doesn't make any sense.
You didn't provide your data types for your date and time columns in table, so I'm going to assume they're both varchar2(10) with formats MM/DD/YYYY and HH24:MI:SS respectively.
I'm also going to go ahead and change your example table and column names, since they're invalid names to use in a real query.
-- example data
with my_table as (select '06/13/2019' as date_column, '09:40:34' as time_column from dual)
-- your query
SELECT *
FROM
my_table
where
to_date(date_column || ' ' || time_column, 'MM/DD/YYYY HH24:MI:SS') >= sysdate - 2/24
What I'm doing here is to combine your date and time strings into one date-time string, then converting it to an Oracle date type (actually date+time). Then we compare it to sysdate - 2/24, which says to take the current time and subtract 2/24ths of a day, which is 2 hours.
For this example, you might need to change the example data date_column and time_column values to something from the past 2 hours, depending on when you run this and what time zone you're in.

Compare timestamps in Oracle

I have a column which is used to store date and time data in a VARCHAR2 column in my DB in Oracle 11g Express in the format of:
9/30/2016 14:00:00
I was trying out ways to get data between time ranges. I found the following 2 ways:
select *
from dummy
WHERE starttime > '9/30/2016 14:00:00'
AND starttime < '9/30/2016 17:00:00'
order by starttime;
select *
from dummy
WHERE to_timestamp(starttime, 'mm/dd/yyyy hh24:mi:ss') > TO_TIMESTAMP('9/30/2016 14:00:00', 'mm/dd/yyyy hh24:mi:ss')
AND TO_TIMESTAMP(starttime, 'mm/dd/yyyy hh24:mi:ss') < TO_TIMESTAMP('9/30/2016 17:00:00', 'mm/dd/yyyy hh24:mi:ss');
I was wondering how the first method works too as the column starttime is stored in VARCHAR format and without converting to a Timestamp the comparison still works. Could someone explain to me how/ why that happens or if there is some corner case for which it will not work? Thanks.
This works because you have no issue with months and years or one-digit vs. two-digit days. Think of any strings that are inside the range '9/30/2016 14:00:00' to '9/30/2016 17:00:00'. They will all have to start with '9/30/2016 1'.
If the range where, say '9/30/2016 14:00:00' to '10/30/2016 17:00:00', you wouldn't find any record at all, because the string would have to start with somthing >= '9' and <= '1' which is not possible.
So it is the narrow range within a particular day that saved you here :-)
Storing the values in a VARCHAR column means that you will do a string comparison:
SELECT *
FROM dummy
WHERE starttime > '9/30/2016 14:00:00'
AND starttime < '10/30/2016 17:00:00'
ORDER BY starttime;
This would look at the start time and consider it character-by-character and if the 1st character is greater than '9' and also less than '1' then it will return a row (since this will never be true it will not return a row). Moreover, it will not consider that the 9 and the 10 represent the months and that when doing a string comparison '09/30/2016' < '09/31/1900' < '10/30/2016'.
Even if you store the value in a TIMESTAMP column, using string literals is a bad idea:
SELECT *
FROM dummy
WHERE starttime > '9/30/2016 14:00:00'
AND starttime < '9/30/2016 17:00:00'
ORDER BY starttime;
This works as Oracle will perform an implicit cast (using TO_TIMESTAMP( time, format_mask )) using the session parameter NLS_TIMESTAMP_FORMAT as the format mask.
So your query would (assuming a TIMESTAMP data type) effectively be (although Oracle will implement it in a more efficient fashion):
SELECT *
FROM dummy
CROSS JOIN
( SELECT value AS format_mask
FROM NLS_SESSION_PARAMETERS
WHERE PARAMETER = 'NLS_TIMESTAMP_FORMAT' ) nls
WHERE starttime > TO_TIMESTAMP( '9/30/2016 14:00:00', nls.format_mask )
AND starttime < TO_TIMESTAMP( '9/30/2016 17:00:00', nls.format_mask )
ORDER BY starttime;
The NLS_TIMESTAMP_FORMAT is a session parameter - this means that each user can set their own value for this parameter in their own session and if one user changes it to YYYY-MM-DD"T"HH24:MI:SS.ZZZ"Z" (i.e. an ISO8601 format) then your query will break for that user (and not for the other users who have not changed it) without any changes having been made to your query.
Rather than using a string literal and implicit conversion, it is better to either explicitly set the format mask you are expecting or to use an ANSI TIMESTAMP literal:
SELECT *
FROM dummy
WHERE TO_TIMESTAMP( starttime, 'MM-DD-YYYY HH24:MI:SS' ) > TIMESTAMP '2016-09-30 14:00:00'
AND TO_TIMESTAMP( starttime, 'MM-DD-YYYY HH24:MI:SS' ) < TIMESTAMP '2016-09-30 17:00:00'
ORDER BY TO_TIMESTAMP( starttime, 'MM-DD-YYYY HH24:MI:SS' );
You would then benefit from a function-based index on TO_TIMESTAMP( starttime, 'MM-DD-YYYY HH24:MI:SS' ).
Even better, would be to convert your column to the correct TIMESTAMP format then you do not need a function-based index and can just use TIMESTAMP literals for the bounds without any need for conversion functions.
Storing date as varchar is less than clever...
Your first method is fine, provided you don't need to cross the boundary of a year. The numbers are compared left to right (because text). Unless you store as 'YYYY-MM-DD HH24:MI:SS' you will run into problems.
2 options, change that storage to DATE, or use a to_date or to_timestamp on the WHERE clause (I recommend to_date)
It's a bad idea to store date/times as character strings or numbers. The optimizer has no idea of the domain and so when attempting to estimate the cardinality you are not giving the optimizer the best chance. For example, consider the following two dates
Dec 31st 2016
Jan 1st 2017
If you store these as a number, you might use
20170101 and 20161231
So what is the number of days between them? Using numbers, you get
20170101 - 20161231
= 8870
However, the true (date based) answer is one.
Although you can TO_DATE() or CAST your columns, you now run the risk of not being able to use certain optimizations, such as indexing, partition pruning, bloom filtering etc.
So I highly recommend using the correct data types.
On Oracle, you could use too:
SELECT * FROM table
WHERE field BETWEEN TRUNC(SYSDATE - 6) AND SYSDATE

Using TO_DATE() with AM/PM Formatting

I am trying to select some dates from a table where the format of the dates is like this:
14-APR-14 10.35.00.0000000000 AM
01-NOV-16 02.43.00.0000000000 PM
Note that the dates can be either AM or PM, but when I try to do a simple SELECT from the table such as:
SELECT * FROM MyTable
WHERE TO_DATE(MyDate, 'DD-MON-YYYY HH:MI:SS AM') > '31-DEC-2016 08:00:00 AM';
I get the error:
ORA-01855: AM/A.M. or PM/P.M. required
I've been trying to get this work for some time but with no luck. Any help here would be appreciated.
Several problems.
Your inputs are obviously strings, since they have ten decimal places and timestamps in Oracle have at most 9. Then, strings with fractions of a second can't be converted to a date with to_date - you need to use to_timestamp or else you need to remove all the fractional parts. In the solution below I only remove the last (the tenth) decimal, since you may have non-zero fractional parts in the table - although not in the sample you posted.
Then, your format mask has yyyy but your inputs have only two digits for the year (which probably means 93 means 1993 and not 2093, so the correct thing to use would be rr rather than yy). And you use : in the format mask where your inputs use .
Finally, don't even compare dates in string format: in string comparisons, 01-JAN-2015 is before 20-NOV-2013.
You probably want something like this:
select mydate
from (
select '14-APR-14 10.35.00.0000000000 AM' as mydate from dual
union all
select '01-NOV-16 02.43.00.0000000000 PM' from dual
) mytable
where to_timestamp(substr(mydate, 1, 28) || substr(mydate, -3), 'dd-MON-rr hh.mi.ss.ff AM')
> to_timestamp('31-DEC-2016 08:00:00 AM', 'dd-MON-yyyy hh:mi:ss AM');
This query compiles correctly, and it produces no rows in the output (for obvious reasons).
NOTE: In a comment you (the OP) say the mydate field is a timestamp(6) datatype. Hard to believe (you show ten decimal places), but if indeed it is a timestamp or date, then you don't need to wrap it within any to_timestamp or to_date function, it should stand alone in the left-hand side of the inequality.
From your comment:
It's actually a timestamp; not a string. Timestamp(6) to be precise
You can just use a TIMESTAMP literal:
SELECT *
FROM MyTable
WHERE MyDate > TIMESTAMP '2016-12-31 08:00:00';

Oracle Date Comparsion

I have a column in TableA which contains date as Varchar2 datatype for column Start_date.( '2011-09-17:09:46:13').
Now what i need to do is , compare the Start_date of TableA with the SYSDATE, and list out any values thts atmost 7days older than SYSDATE.
Can any body help me with this isssue.
You may perform the below to check the date:
select * from
TableA
where
to_date(start_date,'YYYY-MM-DD') between sysdate and sysdate-7;
something like
select * from tableA
where start_date between sysdate-7 and sysdate
I have a column in TableA which contains date as Varchar2 datatype for column Start_date.( '2011-09-17:09:46:13').
Then you have a flawed design. You must use appropriate data types for the data. A datetime should always be stored as DATE data type.
Now what i need to do is , compare the Start_date of TableA with the SYSDATE, and list out any values thts atmost 7days older than SYSDATE.
Since the data type of your column is VARCHAR2, you must use TO_DATE to explicitly convert the string into date.
where to_date(start_date,'YYYY-MM-DD HH24:MI:SS') between sysdate and sysdate-7;
Remember, a DATE has both date and time elements. IF you want to ignore the time portion, then you need to use TRUNC.
For example,
SQL> alter session set nls_date_format='YYYY-MM-DD HH24:MI:SS';
Session altered.
SQL> SELECT sysdate FROM DUAL;
SYSDATE
-------------------
2015-07-28 16:03:32
SQL> SELECT TRUNC(sysdate) FROM DUAL;
TRUNC(SYSDATE)
-------------------
2015-07-28 00:00:00
SQL>
So, BETWEEN sysdate AND sysdate -7 will consider the current datetime till past 7 days till that time portion. If you only want to consider the date portion, use TRUNC.
For example,
where to_date(start_date,'YYYY-MM-DD') between TRUNC(sysdate) and TRUNC(sysdate-7);
Although you ought to be storing your times as DATE data types, you are at least storing the dates in a format that allows greater-than/less-than comparisons.
So, while you ought to convert these columns to dates, or alternatively convert the values to dates for comparison with SYSDATE -7, you could also convert SYSDATE -7 to the same string format as you are storing.
For example:
start_date between to_char(sysdate -7, "YYYY-MM-DD HH24:MI:SS") and
to_char(sysdate , "YYYY-MM-DD HH24:MI:SS")
This would let you use an indexed search without needing a function-based index on the start date column.

Oracle DB select between dates

I would like to query number of records between night 12.01 AM to 11.59 PM but issue is, I would like to schedule this query so I cant specify any hard coded dates.
Query should pull number of records for query date between 12.01 AM to 11.59 PM.
Could someone please help me on this.
Query should pull number of records for query date between 12.01 AM to 11.59 PM.
You could do it as:
TRUNC gives you date element truncating the time portion
convert the SYSDATE into string using TO_CHAR
then concatenate the time element
finally convert everything back to DATE
SYSDATE returns the current date and time set for the operating system on which the database resides. The datatype of the returned value is DATE, and the format returned depends on the value of the NLS_DATE_FORMAT initialization parameter.
So, you don't have to hard-code any DATE value if you want to execute the query everyday.
Use the following in the filter predicate:
BETWEEN
TO_DATE(TO_CHAR(TRUNC(SYSDATE), 'MM/DD/YYYY') ||' 00:01', 'MM/DD/YYYY HH24:MI')
AND
TO_DATE(TO_CHAR(TRUNC(SYSDATE), 'MM/DD/YYYY') ||' 23:59', 'MM/DD/YYYY HH24:MI')
Demo
SQL> alter session set nls_date_format = 'MM/DD/YYYY HH24:MI:SS';
Session altered.
SQL> SELECT to_date(TO_CHAR(TRUNC(SYSDATE), 'MM/DD/YYYY')
2 ||' 00:01', 'MM/DD/YYYY HH24:MI') start_dt ,
3 to_date(TO_CHAR(TRUNC(SYSDATE), 'MM/DD/YYYY')
4 ||' 23:59', 'MM/DD/YYYY HH24:MI') end_date
5 FROM dual;
START_DT END_DATE
------------------- -------------------
05/06/2015 00:01:00 05/06/2015 23:59:00
SQL>
So, you don't have to put any hard-coded value for current date, the SYSDATE will take care of it. All you are doing is:
TRUNC gives the date element by truncating the time portion.
Then concatenating the required time element
Converting the entire string into DATE using TO_DATE
I would like to schedule this query so I cant specify any hardcord dates
To schedule the query to execute everyday, you could use DBMS_SCHEDULER.
I'm going to assume you want everything that happens during the date of interest. So you want everything from and including midnight of that day and before midnight of the next day.
declare
AsOf Date = date '2015-01-01 13:14:15';
select ...
from tablename
where tabledate >= trunc( AsOf )
and tabledate < trunc( AsOf ) + 1;
If you know the date doesn't have a time portion, just can eliminate the calls to trunc. But you may want to keep them just in case.