Strange behavior of systimestamp and sysdate - oracle

Today I encountered a situation which I can not explain and I hope you can.
It boils down to this:
Let say function sleep() return number; loops for 10 seconds and then returns 1.
A sql query like
SELECT systimestamp s1, sleep(), systimestamp s2 from dual;
Will result in two identical values for s1 and s2. So this is somewhat optimized.
A PLSQL-Block with
a_timestamp := systimestamp + 5sec;
IF systimestamp < a_timestamp and sleep() = 1 and systimestamp > a_timestamp THEN
[...]
END IF;
will evaluate to true, because the expression get evaluated from left to right and because of the sleep() the second systimestamp is 10sec greater than the first and a_timestamp lies between both. The +5sec syntax is pseudo code, but bear with me. So here the systimestamp is not optimized.
But now it gets funky:
IF systimestamp between systimestamp and systimestamp THEN [...]
Is always false, but
IF systimestamp + 0 between systimestamp + 0 and systimestamp + 0 THEN [...]
Is always true.
Why? I am confused...
This happens with sysdate as well

When you do:
if systimestamp between systimestamp and systimestamp then
the non-deterministic systimestamp call is just being evaluated three times, and getting very slightly different results each time.
You can see the same effect with
if systimestamp >= systimestamp then
which also always returns false.
Except, it's not quite always. If the server is fast enough and/or the platform it's on has low-enough precision for timestamp fractional seconds (i.e. on Windows, which I believe still limits the precision to milliseconds) then all of those calls could still get the same value some or most of the time.
Things are a bit different in SQL; as an equivalent:
select *
from dual
where systimestamp between systimestamp and systimestamp;
will always return a row, so the condition is always true. That is explicitly mentioned in the documentation:
All of the datetime functions that return current system datetime information, such as SYSDATE, SYSTIMESTAMP, CURRENT_TIMESTAMP, and so forth, are evaluated once for each SQL statement, regardless how many times they are referenced in that statement.
There is no such restriction/optimisation (depending on how you look at it) in PL/SQL. When you use between it does say:
The value of the expression x BETWEEN a AND b is defined to be the same as the value of the expression (x>=a) AND (x<=b) . The expression x will only be evaluated once.
and the SQL reference also mentions that:
In SQL, it is possible that expr1 will be evaluated more than once. If the BETWEEN expression appears in PL/SQL, expr1 is guaranteed to be evaluated only once.
but it says nothing about skipping evaluation of a and b (or expr2 or expr3) even for system datetime functions in PL/SQL.
So all three expressions in your between will be evaluated, it will make three separate calls to systimestamp, and they will all (usually) get slightly different results. You effectively end up with:
if initial_time between initial_time + 1 microsecond and initial_time + 2 microseconds then
or to put it another way
if (initial_time >= initial_time + 1 microsecond) and (initial_time <= initial_time + 2 microseconds) then
While (initial_time <= initial_time + 2 microseconds) is always going to be true, (initial_time >= initial_time + 1 microsecond) has to be false - unless the interval between the first and third evaluations is actually zero for that platform/server/invocation. When it is zero the condition evaluates to true; the rest of the time, when there is any measurable delay, it will evaluate to false.
Your other examples all manipulate the timestamp in way that removes the fractional seconds, by turning some or all of the results into dates, as #Connor showed (and I alluded to in comments). Those aren't really relevant to your core question of why if systimestamp between systimestamp and systimestamp then is (usually) false.

If you add a number to a timestamp, you do not get a timestamp...you get a date. Thus you have lopped off the fractional seconds part, and thus comparisons that look like they should be equal might not be
SQL> select systimestamp, systimestamp+0 from dual;
SYSTIMESTAMP SYSTIMESTAMP+0
--------------------------------------------------------------------------- -------------------
18-JUN-20 11.57.28.350000 AM +08:00 18/06/2020 11:57:28
SQL> select * from dual where systimestamp > sysdate;
D
-
X
If you want to add days etc to a timestamp, use an INTERVAL data type not a NUMBER.

Related

Oracle Time Range Query

I need to write a query which will do a date lookup by the most completed quarter hour.
So, if the current time is 5:35, then the criteria would be 5:15 - 5:30. If the time is 5:46, then 5:30 - 5:45, if the time is 6:02, then 5:45 - 6:00.
Not sure how to easily do this.
Something like
with cqh (dt) as (
select trunc(sysdate, 'hh') +
trunc(extract(minute from systimestamp) / 15) * interval '15' minute
from dual
)
select [_data_] from [_your_table_] cross join cqh
where [_date_column_] >= cqh.dt - interval '15' minute
and [_date_column_] < cqh.dt
;
The subquery calculates the most recently completed quarter-hour. It first truncates SYSDATE to the beginning of the current hour. Then we add a multiple of 15 minutes - the multiplier is 0, 1, 2 or 3, depending on the minute component of SYSDATE. Alas, EXTRACT(MINUTE FROM ...) only works on timestamps, so I had to use SYSTIMESTAMP there instead of SYSDATE, but other than that, the computation should be pretty obvious.
Then cross-join whatever else gives you "the data" to this small helper view, to use the DT value calculated in it.

When is systimestamp generated by Oracle

When is the return value of systimestamp function generated in an Oracle sql query? Returned value concerns the moment in which the query is submitted or is ended? And if the function is used in a subquery?
thanks!
NOTE: The discussion below is for SQL only. The behavior of sysdate and systimestamp is different in PL/SQL, the procedural programming language associated with Oracle SQL. Please see Jon Heller's comment below this Answer, and the link he provides in the comment. END OF NOTE
systimestamp is calculated once, at the BEGINNING of the execution of a query. It will have the same value within the query and all its subqueries, no matter how deep and no matter how many times systimestamp is referenced in the query.
For example, the following (intentionally convoluted) query will always return zero:
select sum (abs( date '2000-01-01'
+ ( systimestamp - (select systimestamp from dual) )
- date '2000-01-01'
)
)
from dual
connect by level <= 300000;
NOTE: In a comment below this Answer, the OP points out that - while systimestamp - systimestamp always returns zero, as it should, systimestamp - (systimestamp - 1) returns something like +01 00:00:00.762611. Which is unexpected. So let's explain that.
Date arithmetic with numbers (like 1 to represent one day) is for expressions of date data type - not for timestamps. So for the operation systimestamp - 1, the timestamp is truncated first - only whole seconds are kept, and the fraction of second after the decimal point is discarded.
Then, for an operation of type [timestamp] - [date], Oracle will first cast the date as a timestamp, and then take the difference and return an interval. Which is what the OP saw. The fraction of a second is due to truncation for the systimestamp - 1 calculation, while the first systimestamp is not truncated.
To get the same calculation "done right" (and to see that indeed the result will be exactly +01 00:00:00.000000), we must use the proper data type when we subtract one day from systimestamp. Namely, just like numbers are "what we should use" for date arithmetic, for timestamp arithmetic we must use the interval data type.
select systimestamp - (systimestamp - interval '1' day) from dual
will always return exactly +01 00:00:00.000000.

Confusion on using variables & constants in PL/SQL

I've new to PL/SQL (and it's been a while since I've used vanilla SQL). I've got a query that I inherited that I'm trying to schedule in TOAD. In order for that to work I have to change hard coded date references to be calculated at run time.
To that end I added a Declare statement to the front of the query, added the necessary constants, setting them at declaration, and then had the query use them.
When I try to execute an error gets thrown saying a Select Into. To my understanding, SELECT Into is used to set a variable based on a value in the db (based on Constants in Oracle SQL query), whereas I'm looking to define the value independent of any value in the db (in this case the date on the server). The full error follows:
ORA-06550: line 6, column 5:
PLS-00428: an INTO clause is expected in the Select statement
So I'm looking for a little guidance on where my understanding of variables/constants in PL/SQL is off, and also help with getting the following to execute:
DECLARE OLD CONSTANT char(11):= to_Char(SYSDATE - 6, 'DD-MON-YYYY');
NEW CONSTANT char(11):= to_char(SYSDATE, 'DD-MON-YYYY');
BEGIN
SELECT CASE
WHEN (userhost LIKE 'a%'
AND userid IN ('s',
'sub')) THEN 'BATCH'
WHEN userid LIKE 'N%' THEN 'N'
WHEN ((userhost LIKE 'b%'
OR userhost LIKE 'c%')
AND userid IN ('s',
'sub')) THEN 'Forms'
WHEN ((userid LIKE '%_IU%'
OR userid LIKE 'RPT%'
OR userid IN ('q',
'r',
'p'))
AND userhost <> 'n%') THEN 'Interface'
ELSE 'Other'
END app_type , round(sum(sessioncpu/100), 1) cpu_seconds , (sum(sessioncpu/100) / (119*1*60*60) * 100) pct_of_cpu,
trunc(ntimestamp#,'MI')
FROM PERFSTAT.AUD$_ARCHIVE
WHERE ntimestamp# BETWEEN to_timestamp(OLD || ' 23:59','DD-MON-YYYY HH24:MI') AND to_timestamp(NEW || ' 00:00','DD-MON-YYYY HH24:MI')
AND logoff$time < to_date(NEW || ' 00:00','DD-MON-YYYY HH24:MI')
GROUP BY CASE
WHEN (userhost LIKE 'a%'
AND userid IN ('s',
'sub')) THEN 'BATCH'
WHEN userid LIKE 'N%' THEN 'N'
WHEN ((userhost LIKE 'b%'
OR userhost LIKE 'c%')
AND userid IN ('s',
'sub')) THEN 'Forms'
WHEN ((userid LIKE '%_IU%'
OR userid LIKE 'RPT%'
OR userid IN ('q',
'r',
'p'))
AND userhost <> 'n%') THEN 'Interface'
ELSE 'Other'
END app_type,
trunc(ntimestamp#,'MI')
ORDER BY trunc(ntimestamp#,'MI'),
1;
END;
You have two issues here. The first is trying to use the CHAR datatype and then not giving it a length. This defaults to a CHAR(1), i.e. a single character. For memory concerns, you might also consider VARCHAR2 instead. https://docs.oracle.com/cd/E17952_01/refman-5.1-en/char.html
The second issue has to do with the INTO clause as mentioned in your question. When you run a SELECT statement in PL/SQL (not associated to DML), you have to give Oracle something to return the result set into. You can then use those variables, whether printing them, storing them, or doing processing with them.
I'd have to see the error, but I think that it may want you to set a length to your char. So, something like char(30).
Also, I'm a big fan of varchar2. Only uses as much space in the DB as the characters in the variable. So, it it's varchar2(500) and has 8 characters, it only uses 8 chars worth of memory.
Your query has an inherent flaw, in that anything that occurs between 23:59 and 0:00 will satisfy conditions at both ends of the range (e.g. something that happens at 23:59:30). If this query were my responsibility, I'd get rid of the variables and the text conversions altogether:
WHERE ntimestamp# >= TRUNC (SYSDATE) - 6
AND ntimestamp# < TRUNC (SYSDATE)
AND logoff$time < TRUNC (SYSDATE)
Using >= and < for dates where you want to avoid an overlap tends to be safer that using between.
Taking a closer look, I'm not sure what the point of your query using one minute before midnight on the lower bound is. That kind of thing is more typically done on the upper bound. Assuming that you're actually doing that for a reason, you can still get around transforming to a string by using either of the following:
WHERE ntimestamp# BETWEEN TRUNC (SYSDATE) - 6 - (1 / 24 / 60)
AND TRUNC (SYSDATE)
AND logoff$time < TRUNC (SYSDATE)
WHERE ntimestamp# BETWEEN TRUNC (SYSDATE)
- NUMTODSINTERVAL (6, 'DAY')
- NUMTODSINTERVAL (1, 'MINUTE')
AND TRUNC (SYSDATE)
AND logoff$time < TRUNC (SYSDATE)
All of that is really just an aside to your main problem though: you need to tell the interpreter what to do with the result of the query. That means that you need to provide a variable to put the result in, then (presumably) do something with the result. One way to do this is to use a cursor loop:
DECLARE
CURSOR cur_query IS
[your query goes here];
BEGIN
FOR r_query IN cur_query LOOP
DBMS_OUTPUT.put_line (r_query.app_type);
DBMS_OUTPUT.put_line (r_query.cpu_seconds);
DBMS_OUTPUT.put_line (r_query.pct_of_cpu);
END LOOP;
END;
Of course, the alternative is just to run your query as SQL, rather than PL/SQL. With the variables eliminated, that will be easier.
Comment Response
PL/SQL blocks are not intended to return query results, like you get if you run straight SQL in Toad. There are ways to fake it via functions that return user-defined types or pipelined functions, but you're better off writing SQL if you are able to (and, in this case, you should be able to).
I'm not sure what you mean by "the variables are supposed to dynamically set the date range to look at". The code provided is returning data relative to sysdate, not getting outside data. You can do that in the query as easily as you can in a PL/SQL block.

ORA-01873: the leading precision of the interval is too small

When I try to query differences between 2 timestamps in Oracle, the result returns the interval normally.
select NVL2(ERROR_OUT_TS, ERROR_OUT_TS-ERROR_IN_TS, null) from table
or
select interval '8 00:00:10' day to second from dual
But when I try to select rows with greater than some interval, Oracle give me this error.
where ERROR_OUT_TS - ERROR_IN_TS <= '00 00:02:00'
or
where ERROR_OUT_TS - ERROR_IN_TS >= interval '0 00:00:10' day to second
It keeps saying that "the leading precision is too small".
I am trying to return the interval like 0 00:00:00:000
It is working fine for other customers. Only few customers are experiencing it.
How to choose the correct precision?
Try:
select interval '8 00:00:10' day(4) to second(4) from dual;
What it does is that 'day' and 'second' are default 2 digits, this expands them to accept 4. You probably just need 3 though.

using date function to query

I have the following table
Alarm (AlarmID INT, InstalledDate Date)
Given that the alarm need to be replace every 5 years, how do i display all the alarms that is due for replacement in the next 6 months?
I tried the following and there was no result:
SELECT AlarmID
FROM Alarm
WHERE Add_months(InstalledDate, 60)
BETWEEN SYSDATE AND Add_months(SYSDATE, 6);
"I tried the following and there was no result:"
The query you propose looks correct, so perhaps you don't have any ALARMS which are five years old?
"it seems like there is a difference using BETWEEN SYSDATE AND
Add_months(SYSDATE, 6) compare to BETWEEN Add_months(SYSDATE, 6) AND
SYSDATE;"
The BETWEEN operator demands that we pass the two values in a specific order, lower bound then upper bound. So this filter is true:
where date '2012-03-01' between date '2012-01-01' and date '2012-06-01'
whereas this is false:
where date '2012-03-01' between date '2012-06-01' and date '2012-01-01'
Perhaps this seems unfair, but the Oracle documentation makes it clearer by translating the BETWEEN operator into lt and gt statements:
where date '2012-03-01' >= date '2012-01-01'
and date '2012-03-01' <= date '2012-06-01'
If you swap the values of the second and third expressions you'll see why the reversed order returns false.

Resources