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
Related
I need to filter a table for specific, accurate to the second, timeperiods.
Datatype is "TIMESTAMP (6)"
select *
from table
where trunc(load_date)<=to_date ('10.08.15 15:10:58', 'dd.mm.yy hh24:mi:ss')
and trunc(load_date)>=to_date ('10.08.15 15:11:08', 'dd.mm.yy hh24:mi:ss');
This is what if come to. But i seem to miss smt.
select *
from b_bis_donexa_delta_2
where trunc(load_date)=to_date ('10.08.15', 'dd.mm.yy');
This works perfectly fine. But scolling through the results isn't very efficient.
atm i get no results, but no error message.
but there IS at least one result. i even tried to swap those < = > randomly because i thought i lost my mind.
Doing trunc(load_date) truncates the time to midnight on that value's day (and also converts to a date, rather than a timestamp):
select systimestamp, trunc(systimestamp) from dual;
SYSTIMESTAMP TRUNC(SYSTIMESTAMP)
------------------------------------ -------------------
2019-09-10 12:53:54.400453000 +01:00 2019-09-10 00:00:00
Once you've truncated your load_date, that midnight time is not within your target range. In your second version you are comparing the truncated value with a time which is also at midnight, hence it now finding a match - but it may or may not be in your 10-second window (there's no way to tell), and also may prevent an index on that column being used - which is probably why it's slow.
Don't truncate; and I'd compare against the same data type:
select *
from table
where load_date >= to_timestamp ('10.08.15 15:10:58', 'dd.mm.yy hh24:mi:ss')
and load_date <= to_timestamp ('10.08.15 15:11:08', 'dd.mm.yy hh24:mi:ss');
or preferably using 4-digit years:
select *
from table
where load_date >= to_timestamp ('10.08.2015 15:10:58', 'dd.mm.yyyy hh24:mi:ss')
and load_date <= to_timestamp ('10.08.2015 15:11:08', 'dd.mm.yyyy hh24:mi:ss');
or even timestamp literals if these are fixed values:
select *
from table
where load_date >= timestamp '2015-08-10 15:10:58'
and load_date <= timestamp '2015-08-10 15:11:08';
Also check that you do really want both >= and <=; if you are getting multiple 10-second ranges then you may actually want >= and < to avoid the exact time appearing in two ranges.
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.
I'm trying to get data between two dates that should include seconds
I'm currently using the below code, which works fine; however now it needs to be more precise to include the time.
where h.creation_time >= date '2017-01-01'
and h.CLOSED_TIME >= date '2018-12-16' and h.CLOSED_TIME <= date '2018-12-17'
Here is what I tried changing the code to:
where (h.creation_time >= date '2017-01-01')
and (h.CLOSED_TIME between '2018-12-16 18:19:00' and '2018-12-16 18:20:00')
I should expect the results of everything between 6:19pm and 6:20pm (one minute time frame). Instead it is spitting out the error:
ORA-01861: literal does not match format string
Try to convert them explicitly with to_date().
WHERE h.creation_time >= to_date('2017-01-01', 'YYYY-MM-DD')
AND h.closed_time BETWEEN to_date('2018-12-16 18:19:00', 'YYYY-MM-DD HH24:MI:SS')
AND to_date('2018-12-16 18:20:00', 'YYYY-MM-DD HH24:MI:SS')
A timestamp literal starts with the keyword TIMESTAMP (just as a date literal starts with DATE):
where h.creation_time >= date '2017-01-01'
and h.closed_time between timestamp '2018-12-16 18:19:00' and timestamp '2018-12-16 18:20:00'
In order to take fractions of seconds into account, you shouldn't use BETWEEN however, but:
where h.creation_time >= date '2017-01-01'
and h.closed_time >= timestamp '2018-12-16 18:19:00'
and h.closed_time < timestamp '2018-12-16 18:20:00'
everything between 6:19pm and 6:20pm (one minute time frame)
The problem presented here is not only technical but lingual. The word "between" can have several subtle differences such as driving between the gateposts (i..e hopefully that car does not hit either side and so no gatepost is included) or flying between London and Paris where both cities are included otherwise you have fallen short of the destination.
SQL standards use the latter approach for between, it is defined as inclusive of both endpoints. This is a potential problem for date/time ranges as it can lead to double-counting of endpoints. As #mathguy indicates in his excellent comment under the question the stricter approach is to avoid using between and explicitly define the range this way:
WHERE h.creation_time >= to_date('2017-01-01', 'YYYY-MM-DD')
AND h.closed_time >= to_date('2018-12-16 18:19:00', 'YYYY-MM-DD HH24:MI:SS')
AND h.closed_time < to_date('2018-12-16 18:20:00', 'YYYY-MM-DD HH24:MI:SS')
IF the closed_time column is a date data type use to_date()
IF the closed_time column is a timestamp data type use to_timestamp() instead.
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';
What is the difference between TIMESTAMP , DATE AND TIMESTAMP with TIMEZONE?
E.g if I wanted to search for all entries between 01-JAN-1990 and 01-JAN-2000 , how would I do so in each format?
I have been searching for timestamp as:
SELECT COUNT(*) FROM TABLE_NAME WHERE DATE BETWEEN '01-JAN-1990' AND '01-JAN-2000;
But I am not sure what format to use to search for DATE or TIMESTAMP WITH TIMEZONE.
The data types and differences between them are in the documentation. The short version is:
DATE has precision down to a second with no time zone support;
TIMESTAMP has precision down to fractions of a second (up to nine decimal places, but your operating system affects that too), still with no time zone support;
TIMESTAMP WITH TIME ZONE has the same precision as TIMESTAMP but also has time zone support, as the name suggests;
TIMESTAMP WITH LOCAL TIME ZONE adjusts the stored value to and from the creating/querying session's local time zone.
You might find this article interesting too.
Whenever you are comparing datetime values stored in your database you should use values of the same datatype to compare against. You don't want to have to convert every value in the column for comparison, especially if the column is indexed. If you have a DATE column then compare with a DATE - don't compare as a string, and don't rely on implicit conversion of a string. When you do:
WHERE date_col BETWEEN '01-JAN-1990' AND '01-JAN-2000'
you are relying on your NLS_DATE_FORMAT being DD-MON-YYYY and your NLS_DATE_LANGUAGE being English. If someone else runs the same query in another session their settings may cause the query to fail (or in some cases, give wrong results, which can be worse). To avoid the language issue it's better to use month numbers rather than names. If you have a string variable to compare against you should use TO_DATE() to convert the string to a DATE using a fixed known format mask - don't rely on NLS. If you have a fixed value you can do the same, or you can use a date literal, which is shorter and unambiguous.
With the format you used you are also including any rows which have a the column set to midnight on January 1st 2000, but not any later on that day. That may be what you want, but make sure you understand how BETWEEN works. If you're actually looking for dates within that decade, including at any time on December 31st 1999, you can use:
WHERE date_col >= DATE '1990-01-01' AND date_col < DATE '2000-01-01'
For timestamps you can either use TO_TIMESTAMP() or a timestamp literal:
WHERE ts_col >= TIMESTAMP '1990-01-01 00:00:00'
AND ts_col < TIMESTAMP '2000-01-01 00:00:00'
For timestamps with time zones you can either use TO_TIMESTAMP_TZ() or a timestamp literal, with a names time zone region:
WHERE tstz_col >= TIMESTAMP '1990-01-01 00:00:00 America/New_York'
AND tstz_col < TIMESTAMP '2000-01-01 00:00:00 America/New_York'
Don't compare dates with strings. It can work if your session's nls_date_format happens to match the format of the string that you're using. But then your query will immediately fail for someone who has a different configuration. Compare dates with dates, timestamps with timestamps, etc.
For dates, you can use either ANSI date literals
SELECT COUNT(*)
FROM your_table
WHERE date_column BETWEEN date '1900-01-01' AND date '2000-01-01'
or you can use a to_date with an explicit format mask
SELECT COUNT(*)
FROM your_table
WHERE date_column BETWEEN to_date('1900-01-01', 'YYYY-MM-DD')
AND to_date('2000-01-01', 'YYYY-MM-DD')
Note that a date in Oracle always has a day and a time component. If you don't specify a time in your to_date, it will default to midnight. If you use an explicit to_date, you can use a string in any format just so long as it matches the format mask you pass in as the second parameter.
For timestamps, you can either use an ANSI timestamp literal
SELECT COUNT(*)
FROM your_table
WHERE timestamp_column BETWEEN timestamp '1900-01-01 00:00:00.000'
AND timestamp '2000-01-01 00:00:00.000'
or you can use a to_timestamp with an explicit format mask
SELECT COUNT(*)
FROM your_table
WHERE timestamp_column BETWEEN to_timestamp('1900-01-01 00:00:00.000', 'YYYY-MM-DD HH24:MI:SS.FFF')
AND to_timestamp('2000-01-01 00:00:00.000', 'YYYY-MM-DD HH24:MI:SS.FFF')
If you use an explicit to_timestamp, you can use a string in any format just so long as it matches the format mask you pass in as the second parameter.
For timestamps with time zone, as you may have guessed, you can either use an ANSI timestamp literal
SELECT COUNT(*)
FROM your_table
WHERE timestamp_column BETWEEN timestamp '1900-01-01 00:00:00.000 -05:00'
AND timestamp '2000-01-01 00:00:00.000 -05:00'
or you can use the to_timestamp_tz function with an explicit format mask
SELECT COUNT(*)
FROM your_table
WHERE timestamp_column BETWEEN to_timestamp('1900-01-01 00:00:00.000 -05:00', 'YYYY-MM-DD HH24:MI:SS.FFF TZH:TZM')
AND to_timestamp('2000-01-01 00:00:00.000 -05:00', 'YYYY-MM-DD HH24:MI:SS.FFF TZH:TZM')
If you use an explicit to_timestamp_tz, you can use a string in any format just so long as it matches the format mask you pass in as the second parameter.