Tuesday, May 2, 2017

Oracle Datetime (2) - Examples

(1)-Concepts      (2)-Examples      (3)-Assignments      (4)-Comparisons      (5)-SQL Arithmetic      (6)-PLSQL Arithmetic


At first glance, Oracle Datetime is obvious, but in reality it is a vulnerable part of SQL and PLSQL.

Oracle Datetime most frequently cited master Document:
    MOS: 340512.1 Timestamps & time zones – Frequently Asked Questions
contains a total of 34 Items, of which 2 have non obvious deficiencies.

We will take 4 examples to demonstrate the deep implication to applications. The first 2 examples are those 2 Items of MOS: 340512.1, the other 2 examples reveal the cryptic behaviors of Datetime.


1. Example-1: Datetime Conversion


In MOS: 340512.1, Item 22) wrote:

22) How can I compute the difference between two timestamp values?

You can simply subtract 2 timestamps from each other to get a interval, for example this calculates 
how long it is since/before lunch on Christmas day:

declare
  duration interval day(6) to second(6);
  v_start  timestamp := to_timestamp('25-DEC-2003 14:00:00.000000', 'DD-MON-YYYY HH24:MI:SSxFF');
  v_end    timestamp(6) := sysTimestamp;
begin
  duration := v_end - v_start;
  dbms_output.put_line ('Now: '||to_char(v_end,'DD-MON-YYYY HH24:MI:SSxFF'));
  dbms_output.put_line ('Difference: '|| to_char(duration));
end;
/

And it works the same in sql:

create table temp(start_TS Timestamp(6), duration interval day(6) to second(6)  );
Insert into temp values
  (to_timestamp('25-DEC-2003 14:00:00.000000', 'DD-MON-YYYY HH24:MI:SSxFF'),
  (sysTimeStamp - to_timestamp('25-DEC-2003 14:00:00.000000', 'DD-MON-YYYY HH24:MI:SSxFF')));

If we run above code with session time_zone = '+00:00' on a Server with time_zone '+02:00', the output looks like:

SQL> alter session set time_zone = '+00:00';

SQL> select SYSTIMESTAMP, SESSIONTIMEZONE from dual;

  SYSTIMESTAMP                        SESSIONTIMEZONE
  ----------------------------------- ---------------
  2017-MAY-02 08:15:14 +02:00         +00:00    

SQL> run Item 22) PLSQL anonymous Block

  Now:         02-MAY-2017 08:19:19.662437000
  Difference: +004876 18:19:19.662437

SQL> select * from temp;

  START_TS              DURATION
  --------------------- -----------------------
  2003*DEC*25 14:00:00  +004876 16:19:28.701151

The above output shows that "Difference" is about 2 hours more than "DURATION", but Item 22) claims:
        And it works the same in sql

Let's look what caused such a discrepancy. In PLSQL anonymous Block, when
    v_end timestamp(6) := sysTimestamp;
only datetime part of sysTimestamp is verbatim copied to v_end by ignoring Offset TZ.

So the real computation is like:

v_end   = '02-MAY-2017 08:19:19'
v_start = '25-DEC-2003 14:00:00'

Difference = v_end - v_start
           = select to_timestamp('02-MAY-2017 08:19:19', 'DD-MON-YYYY HH24:MI:SS') 
                  - to_timestamp('25-DEC-2003 14:00:00', 'DD-MON-YYYY HH24:MI:SS')
               from dual;
           = '+004876 18:19:19'

Whereas in SQL, when
    (sysTimeStamp - to_timestamp('25-DEC-2003 14:00:00.000000', 'DD-MON-YYYY HH24:MI:SSxFF'))
the second part, which has no Offset TZ, is accomplished with default SESSIONTIMEZONE = '+00:00'.

So the effective computation is like:

DURATION = '02-MAY-2017 08:19:28 +02:00' - '25-DEC-2003 14:00:00 +00:00'
         = '02-MAY-2017 08:19:28 +02:00' - '25-DEC-2003 16:00:00 +02:00'
         = select to_timestamp_tz('02-MAY-2017 08:19:28 +02:00', 'DD-MON-YYYY HH24:MI:SS TZH:TZM') 
                - to_timestamp_tz('25-DEC-2003 16:00:00 +02:00', 'DD-MON-YYYY HH24:MI:SS TZH:TZM')
             from dual;
         = '+4876 16:19:28' 

Because Server TZ is 2 hours earlier than Session TZ, "Difference" is about 2 hours more than "DURATION".


2. Example-2: Datetime Arithmetic Data Type


In MOS: 340512.1, Item 28) said:

28) Why does using Datetime Arithmetic on datatypes having timezone information seams to give incorrect result ?
    It's often not known that using Datetime Arithmetic in most cases returns a DATE dataype (Typo datatype).

It shows the Test Output of SQL, but no Test Output of PLSQL.

Now we add the similar dump in PLSQL code, and make a test:

--------------- Test Code --------------- 

ALTER SESSION SET NLS_DATE_FORMAT         ='DD*MM*YYYY HH24:MI:SS';
ALTER SESSION SET NLS_TIMESTAMP_FORMAT    ='DD*MM*YYYY HH24:MI:SS'
ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT ='DD/MM/YYYY HH24:MI:SS TZR TZD' 

ALTER SESSION SET TIME_ZONE = '+02:00'; 

set serveroutput on 

declare 
  v_t1 timestamp with time zone; 
  v_t2 timestamp with time zone;
  v_date date := sysdate; 
  l_dump varchar2(50); 
begin 
  v_t1 := from_tz(cast(to_date('2013-11-04', 'YYYY-MM-DD') as timestamp), '-06:00');
  dbms_output.put_line(v_t1); 
  select (dump(v_t1, 1016)) into l_dump from dual; 
  dbms_output.put_line('v_t1 DUMP          : '||l_dump); 
  
  v_t2 := v_t1 + 35/1440; 
  dbms_output.put_line(v_t2); 
  select (dump(v_t2, 1016)) into l_dump from dual; 
  dbms_output.put_line('v_t2 Number DUMP   : '||l_dump); 
  
  v_t2 := v_t1 + NUMTODSINTERVAL(35, 'MINUTE'); 
  dbms_output.put_line(v_t2); 
  select (dump(v_t2, 1016)) into l_dump from dual; 
  dbms_output.put_line('v_t2 Interval DUMP : '||l_dump);
  
  dbms_output.put_line(v_date); 
  select (dump(v_date, 1016)) into l_dump from dual; 
  dbms_output.put_line('v_date DUMP        : '||l_dump);  
end; 
/ 

--------------- Test Output ---------------

  2013-NOV-04 00:00:00 -06:00
  v_t1 DUMP          : Typ=181 Len=13: 78,71,b,4,7,1,1,0,0,0,0,e,3c
  
  2013-NOV-04 00:35:00 +02:00
  v_t2 Number DUMP   : Typ=181 Len=13: 78,71,b,3,17,24,1,0,0,0,0,16,3c
  
  2013-NOV-04 00:35:00 -06:00
  v_t2 Interval DUMP : Typ=181 Len=13: 78,71,b,4,7,24,1,0,0,0,0,e,3c
  
  03*05*2017 07:10:29
  v_date DUMP        : Typ=12 Len=7: 78,75,5,3,8,b,1e

The above Test Output shows that the Data type is always 181 (SQLT_TIME_TZ), not 12 (SQLT_DAT). So the claim in 28):
    It's often not known that using Datetime Arithmetic in most cases returns a DATE dataype.
probably only holds for SQL, not applicable for PLSQL.


3. Example-3: Datetime Comparisons


Note 2017-03-26 [02:00 03:00) 'Europe/Zurich' is DST transit time.

--------------- Test Code ---------------

alter session set time_zone = 'Europe/London';

declare
  l_timestp_tz      TIMESTAMP WITH TIME ZONE;
  l_timestp_ltz     TIMESTAMP WITH LOCAL TIME ZONE;
  l_timestp_ltz_15  TIMESTAMP WITH LOCAL TIME ZONE;
  l_timestp_ltz_75  TIMESTAMP WITH LOCAL TIME ZONE;
  l_diff            INTERVAL DAY TO SECOND;
  l_dump            varchar2(100);
begin
  l_timestp_tz     := to_timestamp_tz('2017-03-26 01:52:00 Europe/Zurich CET', 'yyyy-mm-dd hh24:mi:ss tzr tzd');
  l_timestp_ltz    := l_timestp_tz; 
  l_timestp_ltz_15 := l_timestp_ltz + numtodsinterval(15,  'Minute');  -- after 15 minutes
  l_timestp_ltz_75 := l_timestp_ltz + numtodsinterval(75,  'Minute');  -- after 75 minutes
  
  dbms_output.put_line('l_timestp_tz          = '||l_timestp_tz);
  dbms_output.put_line('l_timestp_ltz         = '||l_timestp_ltz);
  dbms_output.put_line('l_timestp_ltz_15      = '||l_timestp_ltz_15);
  dbms_output.put_line('l_timestp_ltz_75      = '||l_timestp_ltz_75);
  
  select (dump(l_timestp_ltz_15, 1016)) into l_dump from dual;
  dbms_output.put_line('l_timestp_ltz_15 Dump = '||l_dump);     -- after 15 minutes
  select (dump(l_timestp_ltz_75, 1016)) into l_dump from dual;
  dbms_output.put_line('l_timestp_ltz_75 Dump = '||l_dump);     -- after 75 minutes
  
  l_diff := l_timestp_ltz_15 - l_timestp_tz;
  dbms_output.put_line('Diff                  = '  ||l_diff);
  
  -- Diff works
  if l_diff > numtodsinterval(0,  'Minute') and (extract(minute from l_diff)) > 0 then
    dbms_output.put_line('Diff Compare          >  0');
  else
    dbms_output.put_line('Diff Compare          <= 0');
  end if;
  
  -- Comparison does NOT works
  if l_timestp_ltz_15 <= l_timestp_tz then               -- line 35
    dbms_output.put_line('l_timestp_ltz_15 Lost');
  else
    dbms_output.put_line('l_timestp_ltz_15 Win');
  end if;
end;
/

--------------- Test Output ---------------
  l_timestp_tz          = 2017-MAR-26 01:52:00 EUROPE/ZURICH CET
  l_timestp_ltz         = 2017*MAR*26 00:52:00
  l_timestp_ltz_15      = 2017*MAR*26 02:07:00
  l_timestp_ltz_75      = 2017*MAR*26 02:07:00
  l_timestp_ltz_15 Dump = Typ=231 Len=7: 78,75,3,1a,3,8,1
  l_timestp_ltz_75 Dump = Typ=231 Len=7: 78,75,3,1a,3,8,1
  Diff                  = +00 00:15:00.000000
  Diff Compare          >  0
  
  ORA-01878: specified field not found in datetime or interval
  ORA-06512: at line 35

In the above test, we take Europe/Zurich DST transit day of 2017-03-26. The output shows that the time INTERVAL arithmetic follows the natural rule, 15 minutes after "2017-03-26 01:52:00" is same as that of 75 minutes, so that one day is always 24 hours. It looks like an expected behavior.

When we perform "l_timestp_ltz_15 <= l_timestp_tz", we get ORA-01878. Because both operands have different Data Types, Oracle implicitly promotes "TIMESTAMP WITH LOCAL TIME ZONE" to "TIMESTAMP WITH TIME ZONE". This is documented in:

Oracle Datetime Data Types and Time Zone Support - Datetime Comparisons wrote:
   When you compare date and timestamp values, Oracle Database converts the data to the more precise data type before doing the comparison. The order of precedence for converting date and timestamp data is as follows:
  1. DATE
  2. TIMESTAMP
  3. TIMESTAMP WITH LOCAL TIME ZONE
  4. TIMESTAMP WITH TIME ZONE
It seems that Oracle takes value of l_timestp_ltz_15, which is "2017*MAR*26 02:07:00", and converts it to the data type of second operand l_timestp_tz, which is "TIMESTAMP WITH TIME ZONE" and more precise than "TIMESTAMP WITH LOCAL TIME ZONE". Since there does not exist "2017*MAR*26 02:07:00 EUROPE/ZURICH", it throws ORA-01878. It is very strange that the converting neglects the Time Zone info of l_timestp_ltz_15, and only literally picks its date and time. Probably internally l_timestp_ltz_15 is implemented as a datatype TIMESTAMP.

If we use another equivalence of relational comparison, at first compute:
    l_diff := l_timestp_ltz_15 - l_timestp_tz;
and then compare, the diff value is correctly and comparison works properly.

It is not clear where such a behavior comes from.


Progressive Convertion


As discussed, when comparing different data type of datatime, lower type has to be promoted to higher type. It seems that this convertion is strictly stepwise progressive.

During these Progressive Convertions, it can also throws "ORA-01878". In the following example, we want to compare TIMESTAMP with TIMESTAMP WITH TIME ZONE:
    first step is to convert TIMESTAMP "2017*MAR*26 02:07:00" to TIMESTAMP WITH LOCAL TIME ZONE;
    second step is to TIMESTAMP WITH TIME ZONE.

However in the first step, TIMESTAMP WITH LOCAL TIME ZONE "2017*MAR*26 02:07:00" is not existed in TZ "Europe/Zurich". If we run the same code in TZ "Europe/London", it is OK because "2017*MAR*26 02:07:00" exists in TZ "Europe/London".

--------------- Test Code ---------------

declare
  l_timestp_tz      TIMESTAMP WITH TIME ZONE; 
  l_timestp_start   TIMESTAMP;
  l_timestp_next    TIMESTAMP;
  l_timestp_next_ltz TIMESTAMP WITH LOCAL TIME ZONE;
  l_timestp_next_tz  TIMESTAMP WITH TIME ZONE; 
begin    
  l_timestp_tz  := to_timestamp_tz('2017-03-26 01:52:00 +01:00', 'yyyy-mm-dd hh24:mi:ss tzh:tzm');
  
  l_timestp_start    := l_timestp_tz;
  l_timestp_next := l_timestp_start + numtodsinterval(15,  'Minute');
  
  dbms_output.put_line('l_timestp_tz    = ' || l_timestp_tz);
  dbms_output.put_line('l_timestp_start = ' || l_timestp_start);
  dbms_output.put_line('l_timestp_next  = ' || l_timestp_next);
  
  -- Code to see strictly stepwise progressive Convertion.
  begin
    l_timestp_next_ltz := l_timestp_next;
    dbms_output.put_line('l_timestp_next_ltz = ' || l_timestp_next_ltz);
    l_timestp_next_tz  := l_timestp_next_ltz;
    dbms_output.put_line('l_timestp_next_tz  = ' || l_timestp_next_tz);
  exception when others then dbms_output.put_line('--- ORA-01878 in Convertion ---');
  end;
  
  -- Compare TIMESTAMP with TIMESTAMP WITH TIME ZONE
  
  if l_timestp_next <= l_timestp_tz then
    dbms_output.put_line('l_timestp_next <= l_timestp_tz: True');
  else
   dbms_output.put_line('l_timestp_next <= l_timestp_tz: False');
  end if;
end;
/

--------------- Test Output: Europe/Zurich ---------------

alter session set time_zone = 'Europe/Zurich';

  l_timestp_tz    = 2017-MAR-26 01:52:00 +01:00
  l_timestp_start = 2017*MAR*26 01:52:00
  l_timestp_next  = 2017*MAR*26 02:07:00
  --- ORA-01878 in Convertion ---
  ORA-01878: specified field not found in datetime or interval
  ORA-06512: at line 28

--------------- Test Output: Europe/London ---------------

alter session set time_zone = 'Europe/London';

  l_timestp_tz    = 2017-MAR-26 01:52:00 +01:00
  l_timestp_start = 2017*MAR*26 01:52:00
  l_timestp_next  = 2017*MAR*26 02:07:00
  l_timestp_next_ltz = 2017*MAR*26 02:07:00
  l_timestp_next_tz  = 2017-MAR-26 02:07:00 EUROPE/LONDON BST
  l_timestp_next <= l_timestp_tz: False


4. Example-4: No Index, No ORA-01878


In the following test code, if table is empty, "TABLE ACCESS FULL" has no error because where Clause is used as filter predicate on an empty result set; whereas "INDEX RANGE SCAN" hits ORA-01878 because where Clause is used as access predicate.

If table is not empty, both executions have ORA-01878.

The error is not obvious if test cases are not fully covered.

Datatime constant is constructed by TIMESTAMP WITH TIME ZONE minus a number. (See Blog: Oracle Datetime (5) - SQL Arithmetic Section: Number and Interval Arithmetic)

--------------- Test Code ---------------

drop table ltz_test_tab;

create table ltz_test_tab (loc_ltz TIMESTAMP WITH LOCAL TIME ZONE);

create index ltz_test_tab_idx on ltz_test_tab(loc_ltz);

insert into ltz_test_tab values (localtimestamp);

commit;

truncate table ltz_test_tab;

alter session set time_zone = 'Europe/Paris';

select /*+ full(t) */ count(*) from ltz_test_tab t
where t.loc_ltz  < to_timestamp_tz('2017-03-27 02:52:00 +02:00', 'yyyy-mm-dd hh24:mi:ss tzh:tzm') -1;

select /*+ index(t ltz_test_tab_idx) */ count(*) from ltz_test_tab t
where t.loc_ltz  < to_timestamp_tz('2017-03-27 02:52:00 +02:00', 'yyyy-mm-dd hh24:mi:ss tzh:tzm') -1;

--------------- Test Output --------------- 

SQL > select /*+ full(t) */ count(*) from ltz_test_tab t
      where t.loc_ltz  < to_timestamp_tz('2017-03-27 02:52:00 +02:00', 'yyyy-mm-dd hh24:mi:ss tzh:tzm') -1;

        COUNT(*)
      ----------
               0

SQL > select /*+ index(t ltz_test_tab_idx) */ count(*) from ltz_test_tab t
      where t.loc_ltz  < to_timestamp_tz('2017-03-27 02:52:00 +02:00', 'yyyy-mm-dd hh24:mi:ss tzh:tzm') -1;

      ORA-01878: specified field not found in datetime or interval


5. Example-5: Datetime Interval Irregularity


Note 2017-03-26 [02:00 03:00) 'Europe/Zurich' is DST transit time.

Run code below and look its output:

--------------- Test Code ---------------

alter session set time_zone = 'Europe/Zurich';

declare
  l_a1              varchar2(100) := '2017-03-26 01:52:00';
  l_a2              varchar2(100) := '2017-03-26 03:55:00';
  l_timestp_a1      TIMESTAMP;   
  l_timestp_a2      TIMESTAMP;   
  l_timestp_ltz_a1  TIMESTAMP WITH LOCAL TIME ZONE;
  l_timestp_ltz_a2  TIMESTAMP WITH LOCAL TIME ZONE;
  l_timestp_tz_a1   TIMESTAMP WITH TIME ZONE;
  l_timestp_tz_a2   TIMESTAMP WITH TIME ZONE;
  
  l_timestp_ltz_b1  TIMESTAMP WITH LOCAL TIME ZONE;
  l_timestp_ltz_b2  TIMESTAMP WITH LOCAL TIME ZONE;
  l_interval        INTERVAL DAY TO SECOND;
  l_interval_b      INTERVAL DAY TO SECOND;
  l_dump            varchar2(100);
begin
  l_timestp_a1  := to_timestamp(l_a1, 'yyyy-mm-dd hh24:mi:ss');
  l_timestp_a2  := to_timestamp(l_a2, 'yyyy-mm-dd hh24:mi:ss');
  
  l_timestp_ltz_a1  := l_timestp_a1;
  l_timestp_ltz_a2  := l_timestp_a2;
  
  l_timestp_tz_a1 := to_timestamp_tz(l_a1||' Europe/Zurich CET', 'yyyy-mm-dd hh24:mi:ss TZR TZD');
  l_timestp_tz_a2 := to_timestamp_tz(l_a2||' Europe/Zurich CEST', 'yyyy-mm-dd hh24:mi:ss TZR TZD');
  l_timestp_ltz_b1  := l_timestp_tz_a1;
  l_timestp_ltz_b2  := l_timestp_tz_a2;
  
  -- TIMESTAMP Interval
  l_interval := l_timestp_a2 - l_timestp_a1;
  dbms_output.put_line('TIMESTAMP Interval     = '||l_interval); 
  
  dbms_output.put_line('l_timestp_ltz_a1       = '||to_char(l_timestp_ltz_a1, 'DD*MON*YYYY HH24:MI:SS TZR TZD'));
  dbms_output.put_line('l_timestp_ltz_b1       = '||to_char(l_timestp_ltz_b1, 'DD*MON*YYYY HH24:MI:SS TZR TZD'));
  select (dump(l_timestp_ltz_a1, 1016)) into l_dump from dual;
  dbms_output.put_line('l_timestp_ltz_a1  DUMP = '||l_dump);
  select (dump(l_timestp_ltz_b1, 1016)) into l_dump from dual;
  dbms_output.put_line('l_timestp_ltz_b1  DUMP = '||l_dump); 
  
  dbms_output.put_line('l_timestp_ltz_a2       = '||to_char(l_timestp_ltz_a2, 'DD*MON*YYYY HH24:MI:SS TZR TZD'));
  dbms_output.put_line('l_timestp_ltz_b2       = '||to_char(l_timestp_ltz_b2, 'DD*MON*YYYY HH24:MI:SS TZR TZD'));
  select (dump(l_timestp_ltz_a2, 1016)) into l_dump from dual;
  dbms_output.put_line('l_timestp_ltz_a2  DUMP = '||l_dump);
  select (dump(l_timestp_ltz_b2, 1016)) into l_dump from dual;
  dbms_output.put_line('l_timestp_ltz_b2  DUMP = '||l_dump); 
  
  -- LOCAL TIME ZONE Interval 
  l_interval := l_timestp_ltz_a2 - l_timestp_ltz_a1;
  dbms_output.put_line('LOCAL TZ Interval_a    = '||l_interval);
   
  -- LOCAL TIME ZONE Interval_X 
  l_interval_b := l_timestp_ltz_b2 - l_timestp_ltz_b1;
  dbms_output.put_line('LOCAL TZ Interval_b    = '||l_interval_b);
end;
/

--------------- Test Output ---------------

  TIMESTAMP Interval     = +00 02:03:00.000000
  l_timestp_ltz_a1       = 26*MAR*2017 01:52:00 EUROPE/ZURICH CET
  l_timestp_ltz_b1       = 26*MAR*2017 01:52:00 EUROPE/ZURICH CET
  l_timestp_ltz_a1  DUMP = Typ=231 Len=7: 78,75,3,1a,2,35,1
  l_timestp_ltz_b1  DUMP = Typ=231 Len=7: 78,75,3,1a,2,35,1
  l_timestp_ltz_a2       = 26*MAR*2017 03:55:00 EUROPE/ZURICH CEST
  l_timestp_ltz_b2       = 26*MAR*2017 03:55:00 EUROPE/ZURICH CEST
  l_timestp_ltz_a2  DUMP = Typ=231 Len=7: 78,75,3,1a,3,38,1
  l_timestp_ltz_b2  DUMP = Typ=231 Len=7: 78,75,3,1a,3,38,1
  LOCAL TZ Interval_a    = +00 02:03:00.000000
  LOCAL TZ Interval_b    = +00 01:03:00.000000

In this example, all 4 variables: l_timestp_ltz_a1, l_timestp_ltz_a2, l_timestp_ltz_b1, l_timestp_ltz_b2 are defined as same Data Type.

Their formatted output and dump attest:
  l_timestp_ltz_a1 = l_timestp_ltz_b1 
  l_timestp_ltz_a2 = l_timestp_ltz_b2
But
  l_timestp_ltz_a2 - l_timestp_ltz_a1 = 02:03
  l_timestp_ltz_b2 - l_timestp_ltz_b1 = 01:03

There is no obvious clue where the cryptic disparity comes from.

In math, if:
  a1 = b1
  a2 = b2
then
  a2 - a1 = b2 - b1
But this PLSQL code seems not able to comply to the basic math law.

Oracle Datetime (1) - Concepts

(1)-Concepts      (2)-Examples      (3)-Assignments      (4)-Comparisons      (5)-SQL Arithmetic      (6)-PLSQL Arithmetic


This small Oracle Datetime Cookbook is made of a series of 6 Blogs:
Oracle Datetime (1) - Concepts
Oracle Datetime (2) - Examples
Oracle Datetime (3) - Assignments
Oracle Datetime (4) - Comparisons
Oracle Datetime (5) - SQL Arithmetic
Oracle Datetime (6) - PLSQL Arithmetic

1. Data Types


Oracle Datetime consists of 4 basic Data Types:
DATE                             stores as a literal constant, no TimeZone. 
TIMESTAMP                        stores as a literal constant, no TimeZone. 
TIMESTAMP WITH TIME ZONE         stores with explicit TimeZone.
TIMESTAMP WITH LOCAL TIME ZONE   stores with implicit sessiontimezone as default TimeZone.
The difference between DATE and TIMESTAMP is precision (fractional part of the SECOND), and the difference between TIMESTAMP WITH TIME ZONE and TIMESTAMP WITH LOCAL TIME ZONE is the later one with a default TimeZone, but the common usage is same.

Therefore, there are two essential Data Types: TIMESTAMP and TIMESTAMP WITH TIME ZONE. Even TIMESTAMP can be considered as a Subtype of TIMESTAMP WITH TIME ZONE without time zone.


2. Oracle built-in Functions

SYSDATE            returns current Datetime in the server (OS) time zone in datatype DATE.
SYSTIMESTAMP       returns current Datetime in the server (OS) time zone (Unix TZ variable) 
                   in datatype TIMESTAMP WITH TIME ZONE.

CURRENT_DATE       returns current Datetime in the session time zone in datatype DATE.
CURRENT_TIMESTAMP  returns current Datetime in the session time zone in datatype 
                   TIMESTAMP WITH TIME ZONE.

LOCALTIMESTAMP     returns the current Datetime in the session time zone in datatype TIMESTAMP.
The first two are from OS Server point of view, irrelevant to Oracle, just like date command.
SYSDATE is SYSTIMESTAMP by dropping Time Zone info.

The next two are the counterparts from Oracle session point of view, depending on each Oracle session setting.
CURRENT_DATE and LOCALTIMESTAMP are CURRENT_TIMESTAMP by removing Time Zone info.

The last one is an Oracle special mixed variant (not in SQL-92), API similar to TIMESTAMP WITH TIME ZONE, internal storage as TIMESTAMP relative to DBTIMEZONE (which is invented only for TIMESTAMP WITH LOCAL TIME ZONE). This automatic conversion probably implicates certain performance difference, as tested, it is about 30% (1000,000 calls takes less than 1 second).

In fact, LOCALTIMESTAMP is a cast of CURRENT_TIMESTAMP as data type timestamp_unconstrained (see Oracle package SYS.STANDARD spec and body), whereas internally timestamp_unconstrained is defined as Typ=180.

type TIMESTAMP is new DATE_BASE;
SUBTYPE TIMESTAMP_UNCONSTRAINED IS TIMESTAMP(9);
SUBTYPE TIMESTAMP_TZ_UNCONSTRAINED IS TIMESTAMP(9) WITH TIME ZONE;

FUNCTION localtimestamp RETURN timestamp_unconstrained
IS t timestamp_tz_unconstrained := current_timestamp;
BEGIN
 RETURN (cast(t AS timestamp_unconstrained));
END;
  
declare
  l_tz_unconstrained  timestamp_unconstrained := (cast(current_timestamp AS timestamp_unconstrained));
  l_dump              varchar2(100);
begin
  select (dump(l_tz_unconstrained, 1016)) into l_dump from dual;
  dbms_output.put_line('timestamp_unconstrained DUMP: '||l_dump);
end;
/

timestamp_unconstrained DUMP: Typ=180 Len=11: 78,78,b,8,b,23,20,5,2a,38,48

For performance discussion on datetime indexing, see Blog: Tony’s Tirade against TIMESTAMP WITH TIME ZONE

Here is a basic test and its output:

col dbtimezone        format a15
col sessiontimezone   format a15
col sysdate           format a25
col systimestamp      format a35
col current_date      format a25
col current_timestamp format a50
col localtimestamp    format a35

ALTER SESSION SET NLS_DATE_FORMAT         ='YYYY*MON*DD HH24:MI:SS';    
ALTER SESSION SET NLS_TIMESTAMP_FORMAT    ='YYYY*MON*DD HH24:MI:SS.FF9';
ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT ='YYYY-MON-DD HH24:MI:SS.FF9 TZR TZD';

-- ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT ='SYYYY-MON-DD HH24:MI:SS.ff9 TZR TZD'; 
-- ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT ='YYYY-MON-DD HH24:MI:SS.ff9 TZR TZD AD'; 

alter session set time_zone = 'Europe/Paris';

select dbtimezone, sessiontimezone,
    sysdate, systimestamp,
    current_date, current_timestamp, localtimestamp
from dual;

DBTIMEZONE          : +01:00
SESSIONTIMEZONE     : Europe/Paris
SYSDATE             : 2017*MAY*02 07:52:50
SYSTIMESTAMP        : 2017-MAY-02 07:52:50.123456000 +02:00
CURRENT_DATE        : 2017*MAY*02 07:52:50
CURRENT_TIMESTAMP   : 2017-MAY-02 07:52:50.123458000 EUROPE/PARIS CEST
LOCALTIMESTAMP      : 2017*MAY*02 07:52:50.123458000

Oracle MOS Note*: The Priority of NLS Parameters Explained (Where To Define NLS Parameters) (Doc ID 241047.1)
  NLS_TIME_FORMAT and NLS_TIME_TZ_FORMAT, are currently used for internal purposes only. 
  We strongly suggest to NOT define them. If they are visible in the NLS_INSTANCE_PARAMETERS 
  then please DO remove them and bounce the database. If set they may cause errors 
  like ORA-1821: date format not recognized, ORA-6512: at "SYS.DBMS_SCHEDULER" 
  when submitting / running DBMS_SCHEDULER jobs.
    ALTER SESSION SET NLS_TIME_FORMAT         ='HH.MI.SSXFF AM';
    ALTER SESSION SET NLS_TIME_TZ_FORMAT      ='HH.MI.SSXFF AM TZR';

3. Time Zone and Time Zone Abbreviation


(a). Time Zone can be represented in either Named TZ (TZR), or Offset TZ ( TZH:TZM) format
for example:

alter session set time_zone='Europe/Paris';
alter session set time_zone='+01:00';

Named TZ is DST aware; If the Named region is DST sensitive, it is varied between Standard and DST. Otherwise it is static with a constant offset. Offset TZ is DST unaware with a constant offset.

(b). Time Zone Abbreviation is DST unaware, defined with a constant offset
It is used to distinguish ambiguous overlap timestamp during transit from DST to Standard when ERROR_ON_OVERLAP_TIME is enabled. If ERROR_ON_OVERLAP_TIME is disabled, it takes Standard as the default in case of ambiguity.

Here is an example:

alter session set ERROR_ON_OVERLAP_TIME=FALSE;
select TIMESTAMP '2017-10-29 02:52:00 Europe/Paris' from dual;
  2017-OCT-29 02:52:00 EUROPE/PARIS CET 

alter session set ERROR_ON_OVERLAP_TIME=TRUE;
select TIMESTAMP '2017-10-29 02:52:00 Europe/Paris' from dual;
   ORA-01883: overlap was disabled during a region transition

select TIMESTAMP '2017-10-29 02:52:00 Europe/Paris CEST',
       TIMESTAMP '2017-10-29 02:52:00 Europe/Paris CET' from dual;
  2017-OCT-29 02:52:00 EUROPE/PARIS CEST                         
  2017-OCT-29 02:52:00 EUROPE/PARIS CET

CET is a special twofold shortcut. It denotes a Time Zone Region (TZR, such as Europe/Paris), and it also denotes a static Time Zone Abbreviation (TZD: Time Zone Designator, such as CEST). The difference is that TZR CET is DST aware, in Winter it is (UTC +1), in Summer, it is (UTC +2). Whereas TZD CET is static, it represents fixed Central European Time (UTC +1). So in Winter, TZR CET is TZD CET (UTC +1); whereas in Summer, TZR CET is TZD CEST (UTC +2). We can say that TZD is same as Offset TZ ( TZH:TZM), or TZD is a symbolic notation of TZH:TZM. For example,

select TIMESTAMP '2017-10-29 02:52:00 CET CET' from dual;
  2017-OCT-29 02:52:00 CET CET

select * from v$timezone_names where tzname = 'CET' and tzabbrev = 'CET';

TZNAME    TZABBREV 
--------- ---------
CET       CET 


select TIMESTAMP '2017-10-28 02:52:00 CET CEST',
       TIMESTAMP '2017-10-28 02:52:00 Europe/Paris CEST',
       TIMESTAMP '2017-10-30 02:52:00 CET CET',
       TIMESTAMP '2017-10-30 02:52:00 Europe/Paris CET' from dual;

2017-OCT-28 02:52:00 CET CEST       
2017-OCT-28 02:52:00 EUROPE/PARIS CEST
2017-OCT-30 02:52:00 CET CET  
2017-OCT-30 02:52:00 EUROPE/PARIS CET

4. Server Time Zone


Oracle does not have support to get Named TZ of Server. SYSTIMESTAMP output has an offset from UTC, defined not to include an actual named timezone, for example,


select extract(TIMEZONE_OFFSET from systimestamp), extract(TIMEZONE_REGION from systimestamp) from dual;
  +000000000 02:00:00.000000000  
   UNKNOWN

There exists an internal implementation in DBMS_SCHEDULER to get Named TZ (one DBMS_SCHEDULER attribute):
    select DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
    select * from dba_scheduler_global_attribute where attribute_name='DEFAULT_TIMEZONE';

It works in AIX, Solaris, Linux in most case, except HP.

UNIX environment variable TZ has two TZ format: POSIX and Olson.

It seems that Java is able to get a unified Name TZ with Class TimeZone (see JDK 8 "tzdb.dat"):

import java.time.Instant;
import java.util.TimeZone;

public class TimeZoneTest {
   public static void main(String args[]) {
      TimeZone tz = TimeZone.getDefault();
      // TimeZone tz = Calendar.getInstance().getTimeZone();
      System.out.println("ID=" + tz.getID());
      System.out.println("Name=" + tz.getDisplayName());
      final String clientTimeZoneOffset = tz.toZoneId().getRules().getOffset(Instant.now()).getId();
      System.out.println("TimeZoneOffset = " + clientTimeZoneOffset);
   }
}

Here one set of test results on different UNIX:
(Unix TZ environment format: AIX in POSIX. Linux, Solaris in Olson. HP in special format)

OS AIX Solaris Linux HP
echo $TZ CET-1CEST,M3.5.0,M10.5.0 Europe/Zurich Europe/Zurich MET-1METDST
dbms_scheduler.get_sys_time_zone_name Europe/Vienna Europe/Zurich Europe/Zurich
TimeZoneTest.java ID Europe/Paris Europe/Zurich Europe/Zurich Europe/Paris
TimeZoneTest.java Name Central European Time Central European Time Central European Time Central European Time

Offset TZ in SYSTIMESTAMP is determined by the shell TZ of UNIX process, from which the Oracle session is spawned. Therefore, we can have 3 different connection scenarios, each with its own shell TZ. If they are all configured differently, we may end up 3 different Offset TZ in SYSTIMESTAMP (although this is not a recommended practice):
(1). TZ of UNIX process which starts up DB. 
       This TZ is used by Oracle processes to get SYSTIMESTAMP and SYSDATE when recording logs and traces.
       Content in alert.log and trace files are stamped in SYSDATE.
       Datetime in alert/log.xml is enhanced and marked in SYSTIMESTAMP, i.e, with Time Zone info.
     
     DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME returns this shell TZ (in Named TZ).

     By the way, Oracle 12.2.0.1 introduced UNIFORM_LOG_TIMESTAMP_FORMAT 
     to specify a uniform timestamp format in trace (.trc) files and alert log. 
     
(2). TZ of UNIX process which starts a local connection via Bequeath Protocol (sqlplus / as sysdba)

(3). TZ of UNIX process which starts TNS Listener, and Oracle session is connected via Listener.


DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME Test


DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME is a call of subroutine "jsxsgetsystimezonename" in SYS.DBMS_ISCHED.GET_SYS_TIME_ZONE_NAME.
Oracle MOS "DBMS_SCHEDULER or DBMS_JOB And DST / Timezones Explained (Doc ID 467722.1)" wrote:

  SELECT DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME FROM DUAL; is not related to the DEFAULT_TIMEZONE.
  (select value from dba_scheduler_global_attribute where attribute_name='DEFAULT_TIMEZONE';)
  DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME provides the TimeZone set on the OS level trough the TZ variable, 
  it will return the named timezone only if the OS TZ setting is also known in Oracle.
  Only when no OS TZ variable is set it will report the DEFAULT_TIMEZONE.
We will make 4 different TZ tests and watch the output (The output contains more info and not all fits to above MOS Docu).
The test is performed in Linux - Oracle 19.17 with following time and DEFAULT_TIMEZONE settings:

$ timedatectl
            Local time: Sun 2023-06-11 13:57:12 GMT
        Universal time: Sun 2023-06-11 13:57:12 UTC
              RTC time: Sun 2023-06-11 13:57:12
             Time zone: Etc/GMT (GMT, +0000)
           NTP enabled: no
      NTP synchronized: yes
       RTC in local TZ: no
            DST active: n/a

SQL> select value from dba_scheduler_global_attribute where attribute_name='DEFAULT_TIMEZONE';
            Europe/Zurich


Case-1 unset TZ


GET_SYS_TIME_ZONE_NAME return depends on session time_zone setting.

--===================================== unset TZ ===============================================

$ unset TZ
$ export TZ

SQL> startup force

SQL> host echo $TZ       -- in case of PC Window Sqlplus, check Date&Time -> Time Zone setting
       -- no value return

SQL> select value from dba_scheduler_global_attribute where attribute_name='DEFAULT_TIMEZONE';
            Europe/Zurich

SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            +00:00           Etc/GMT
  
SQL> alter session set time_zone = 'Europe/Paris';
SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            Europe/Paris     -- no value for GET_SYS_TIME_ZONE_NAME
  
SQL> alter session set time_zone = 'Etc/GMT';
SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            +00:00           Etc/GMT

SQL> alter session set time_zone = '+02:00';
SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            +02:00           -- no value for GET_SYS_TIME_ZONE_NAME

SQL> alter session set time_zone = '+00:00';
SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            +00:00           Etc/GMT


Case-2 TZ=""


All GET_SYS_TIME_ZONE_NAME returns "UTC".

--=============== TZ="" (without: export TZ="", output can be different) ===================
$ set TZ=""
$ export TZ=""

SQL> startup force

SQL> host echo $TZ       -- in case of PC Window Sqlplus, check Date&Time -> Time Zone setting
       -- no value return

SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            +00:00           UTC
  
SQL> alter session set time_zone = 'Europe/Paris';
SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            +00:00           UTC
  
SQL> alter session set time_zone = 'Etc/GMT';
SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            +00:00           UTC

SQL> alter session set time_zone = '+02:00';
SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            +00:00           UTC

SQL> alter session set time_zone = '+00:00';
SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            +00:00           UTC

SQL> alter session set time_zone = 'UTC';
SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            UTC              UTC


Case-3 TZ="Europe/Paris"


All GET_SYS_TIME_ZONE_NAME returns "Europe/Paris".

--===================================== TZ="Europe/Paris" ====================================
$ set TZ="Europe/Paris"
$ export TZ="Europe/Paris"

SQL> startup force

SQL> host echo $TZ       -- in case of PC Window Sqlplus, check Date&Time -> Time Zone setting
       Europe/Paris

SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            +02:00           Europe/Paris
  
SQL> alter session set time_zone = 'Europe/Paris';
SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            Europe/Paris     Europe/Paris
  
SQL> alter session set time_zone = 'Etc/GMT';
SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
           Etc/GMT           Europe/Paris

SQL> alter session set time_zone = '+02:00';
SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            +02:00           Europe/Paris

SQL> alter session set time_zone = '+00:00';
SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            +00:00           Europe/Paris


Case-4 TZ="Europe/Pariz" (wrong name: Pariz)


There are no values for GET_SYS_TIME_ZONE_NAME.

--============================= TZ="Europe/Pariz" (wrong name: Pariz) ========================
$ set TZ="Europe/Pariz"
$ export TZ="Europe/Pariz"

SQL> startup force

SQL> host echo $TZ       -- in case of PC Window Sqlplus, check Date&Time -> Time Zone setting
       Europe/Pariz

SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            +00:00           -- no value for GET_SYS_TIME_ZONE_NAME
  
SQL> alter session set time_zone = 'Europe/Paris';
SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            Europe/Paris     -- no value for GET_SYS_TIME_ZONE_NAME
  
SQL> alter session set time_zone = 'Etc/GMT';
SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
           Etc/GMT           -- no value for GET_SYS_TIME_ZONE_NAME

SQL> alter session set time_zone = '+02:00';
SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            +02:00           -- no value for GET_SYS_TIME_ZONE_NAME

SQL> alter session set time_zone = '+00:00';
SQL> select SESSIONTIMEZONE, sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME from dual;
            +00:00           -- no value for GET_SYS_TIME_ZONE_NAME
When setting TZ with POSIX time zone format (in AIX or Linux), one should specify timezone names which are listed in V$TIMEZONE_NAMES, for example, following POSIX time zone format (Olson equivalent TZ=Europe/Zurich):

  export TZ=CET-1CEST,M3.5.0,M10.5.0
If using AIX specific format:

  export TZ=NFT-1DFT,M3.5.0,M10.5.0
The query does not return any row:

  select sys.DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME SYS_ZONE_NAME from dual; 
We can use AIX zdump to display the time zone information:

$ > zdump -v -c 2025,2027 Europe/Vienna

	Europe/Vienna  Fri Dec 13 20:45:52 1901 UT = Fri Dec 13 21:45:52 1901 CET isdst=0 gmtoff=3600
	Europe/Vienna  Sat Dec 14 20:45:52 1901 UT = Sat Dec 14 21:45:52 1901 CET isdst=0 gmtoff=3600
	Europe/Vienna  Sun Mar 30 00:59:59 2025 UT = Sun Mar 30 01:59:59 2025 CET isdst=0 gmtoff=3600
	Europe/Vienna  Sun Mar 30 01:00:00 2025 UT = Sun Mar 30 03:00:00 2025 CEST isdst=1 gmtoff=7200
	Europe/Vienna  Sun Oct 26 00:59:59 2025 UT = Sun Oct 26 02:59:59 2025 CEST isdst=1 gmtoff=7200
	Europe/Vienna  Sun Oct 26 01:00:00 2025 UT = Sun Oct 26 02:00:00 2025 CET isdst=0 gmtoff=3600
	Europe/Vienna  Sun Mar 29 00:59:59 2026 UT = Sun Mar 29 01:59:59 2026 CET isdst=0 gmtoff=3600
	Europe/Vienna  Sun Mar 29 01:00:00 2026 UT = Sun Mar 29 03:00:00 2026 CEST isdst=1 gmtoff=7200
	Europe/Vienna  Sun Oct 25 00:59:59 2026 UT = Sun Oct 25 02:59:59 2026 CEST isdst=1 gmtoff=7200
	Europe/Vienna  Sun Oct 25 01:00:00 2026 UT = Sun Oct 25 02:00:00 2026 CET isdst=0 gmtoff=3600
	Europe/Vienna  Mon Jan 18 03:14:07 2038 UT = Mon Jan 18 04:14:07 2038 CET isdst=0 gmtoff=3600
	Europe/Vienna  Tue Jan 19 03:14:07 2038 UT = Tue Jan 19 04:14:07 2038 CET isdst=0 gmtoff=3600

$ > zdump -v -c 2025,2027 CET

	CET  Fri Dec 13 20:45:52 1901 UT = Fri Dec 13 21:45:52 1901 CET isdst=0 gmtoff=3600
	CET  Sat Dec 14 20:45:52 1901 UT = Sat Dec 14 21:45:52 1901 CET isdst=0 gmtoff=3600
	CET  Sun Mar 30 00:59:59 2025 UT = Sun Mar 30 01:59:59 2025 CET isdst=0 gmtoff=3600
	CET  Sun Mar 30 01:00:00 2025 UT = Sun Mar 30 03:00:00 2025 CEST isdst=1 gmtoff=7200
	CET  Sun Oct 26 00:59:59 2025 UT = Sun Oct 26 02:59:59 2025 CEST isdst=1 gmtoff=7200
	CET  Sun Oct 26 01:00:00 2025 UT = Sun Oct 26 02:00:00 2025 CET isdst=0 gmtoff=3600
	CET  Sun Mar 29 00:59:59 2026 UT = Sun Mar 29 01:59:59 2026 CET isdst=0 gmtoff=3600
	CET  Sun Mar 29 01:00:00 2026 UT = Sun Mar 29 03:00:00 2026 CEST isdst=1 gmtoff=7200
	CET  Sun Oct 25 00:59:59 2026 UT = Sun Oct 25 02:59:59 2026 CEST isdst=1 gmtoff=7200
	CET  Sun Oct 25 01:00:00 2026 UT = Sun Oct 25 02:00:00 2026 CET isdst=0 gmtoff=3600
	CET  Mon Jan 18 03:14:07 2038 UT = Mon Jan 18 04:14:07 2038 CET isdst=0 gmtoff=3600
	CET  Tue Jan 19 03:14:07 2038 UT = Tue Jan 19 04:14:07 2038 CET isdst=0 gmtoff=3600

$ > zdump -v -c 2025,2027 CEST

	CEST  Fri Dec 13 20:45:52 1901 UT = Fri Dec 13 20:45:52 1901 CEST isdst=0 gmtoff=0
	CEST  Sat Dec 14 20:45:52 1901 UT = Sat Dec 14 20:45:52 1901 CEST isdst=0 gmtoff=0
	CEST  Mon Jan 18 03:14:07 2038 UT = Mon Jan 18 03:14:07 2038 CEST isdst=0 gmtoff=0
	CEST  Tue Jan 19 03:14:07 2038 UT = Tue Jan 19 03:14:07 2038 CEST isdst=0 gmtoff=0

$ > zdump -v -c 2025,2027 NFT

	NFT  Fri Dec 13 20:45:52 1901 UT = Fri Dec 13 20:45:52 1901 NFT isdst=0 gmtoff=0
	NFT  Sat Dec 14 20:45:52 1901 UT = Sat Dec 14 20:45:52 1901 NFT isdst=0 gmtoff=0
	NFT  Mon Jan 18 03:14:07 2038 UT = Mon Jan 18 03:14:07 2038 NFT isdst=0 gmtoff=0
	NFT  Tue Jan 19 03:14:07 2038 UT = Tue Jan 19 03:14:07 2038 NFT isdst=0 gmtoff=0

$ > zdump -v -c 2025,2027 DFT

	DFT  Fri Dec 13 20:45:52 1901 UT = Fri Dec 13 20:45:52 1901 DFT isdst=0 gmtoff=0
	DFT  Sat Dec 14 20:45:52 1901 UT = Sat Dec 14 20:45:52 1901 DFT isdst=0 gmtoff=0
	DFT  Mon Jan 18 03:14:07 2038 UT = Mon Jan 18 03:14:07 2038 DFT isdst=0 gmtoff=0
	DFT  Tue Jan 19 03:14:07 2038 UT = Tue Jan 19 03:14:07 2038 DFT isdst=0 gmtoff=0


gdb debug


For Case-1 unset TZ, if we gdb the Sqlplus session, in case of DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME returning "Etc/GMT", we can see following output.
(no Breakpoint stopped in case of "no value return")

break *OCIStringAssignText
break *jsxsGetSysTimeZoneName+938
display /s $rbx
display /s $rsp

Breakpoint 2, 0x0000000004b50f00 in OCIStringAssignText ()
2: x/s $rsp  0x7ffc9e55bd28:    "\372Yt\f"
1: x/s $rbx  0xa5dc026d:        "Etc/GMT"
(gdb) c
Continuing.

Breakpoint 1, 0x000000000c7459fa in jsxsGetSysTimeZoneName ()
2: x/s $rsp  0x7ffc9e55bd30:    "GMT"
1: x/s $rbx  0xa5dc026d:        "Etc/GMT"
The call stack and part of jsxsGetSysTimeZoneName code lines are as follows:

Breakpoint 1, 0x0000000004313c70 in LdiDateComparei ()
(gdb) bt
#0  0x0000000004313c70 in LdiDateComparei ()
#1  0x000000000434768a in ltzGetIndex ()
#2  0x0000000004347560 in ltzGet ()
#3  0x0000000004327884 in sLdiGetLazyDt_int ()
#4  0x00000000043274ec in sLdiGetDate ()
#5  0x000000000c745750 in jsxsGetSysTimeZoneName ()     
                  0x000000000c745716 <+198>:	 callq  0x43274a0 
                  ...
                  0x000000000c74585c <+524>:	 callq  0x1032ad40 
                  ...
                  0x000000000c74599c <+844>:	callq  0x1032ad40 
                  
                  0x000000000c7459d4 <+900>:   jne    0xc7459e1 
                  0x000000000c7459d6 <+902>:   mov    %rdx,%rax
                  0x000000000c7459d9 <+905>:   add    %rcx,%rdx
                  0x000000000c7459dc <+908>:   callq  0x6fcac60 <__intel_sse2_strlen>
                  0x000000000c7459e1 <+913>:   mov    %rbx,%rdx
                  0x000000000c7459e4 <+916>:   mov    %eax,%ecx
                  0x000000000c7459e6 <+918>:   mov    -0x78(%rbp),%rdi
                  0x000000000c7459ea <+922>:   lea    -0x98(%rbp),%r8
                  0x000000000c7459f1 <+929>:   mov    -0x38(%rbp),%rsi
                  0x000000000c7459f5 <+933>:   callq  0x4b50f00 
               => 0x000000000c7459fa <+938>:   xor    %eax,%eax
                  0x000000000c7459fc <+940>:   mov    %ax,(%r15)
                  0x000000000c745a00 <+944>:   lea    -0x28(%rbp),%rsp
                  0x000000000c745a04 <+948>:   pop    %rbx
                  0x000000000c745a05 <+949>:   pop    %r15
                  0x000000000c745a07 <+951>:   pop    %r14
                  0x000000000c745a09 <+953>:   pop    %r13
                  0x000000000c745a0b <+955>:   pop    %r12
                  0x000000000c745a0d <+957>:   pop    %rbp
                  0x000000000c745a0e <+958>:   retq
#6  0x0000000005833a3d in spefcmpa ()
#7  0x000000000580df7b in spefmccallstd ()
#8  0x00000000057aed2b in peftrusted ()
#9  0x000000000426b69d in psdexsp ()
#10 0x0000000012de0e84 in rpiswu2 ()
#11 0x000000000384646a in kxe_push_env_internal_pp_ ()
#12 0x00000000038b9675 in kkx_push_env_for_ICD_for_new_session ()
#13 0x000000000426b083 in psdextp ()
#14 0x00000000057a8bb7 in pefccal ()
#15 0x00000000057a846f in pefcal ()
#16 0x000000000566b34b in pevm_FCAL ()
#17 0x000000000564c8ae in pfrinstr_FCAL ()
#18 0x00000000130ea88c in pfrrun_no_tool ()
#19 0x00000000130e91f6 in pfrrun ()
#20 0x00000000130f4dbb in plsql_run ()

DBMS_SCHEDULER.SET_SCHEDULER_ATTRIBUTE Test (on Linux)


-- $echo $TZ
--    Europe/Zurich

exec DBMS_SCHEDULER.SET_SCHEDULER_ATTRIBUTE ('DEFAULT_TIMEZONE', 'Europe/Zurich');
select DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME value from dual;
  -- 'Europe/Zurich'
select value from dba_scheduler_global_attribute where attribute_name='DEFAULT_TIMEZONE';
  -- 'Europe/Zurich'

exec DBMS_SCHEDULER.SET_SCHEDULER_ATTRIBUTE ('DEFAULT_TIMEZONE', 'Europe/London');
select DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME value from dual;
  -- 'Europe/Zurich'
select value from dba_scheduler_global_attribute where attribute_name='DEFAULT_TIMEZONE';
  -- 'Europe/London'

exec DBMS_SCHEDULER.SET_SCHEDULER_ATTRIBUTE ('DEFAULT_TIMEZONE', NULL);
select DBMS_SCHEDULER.GET_SYS_TIME_ZONE_NAME value from dual;
  -- 'Europe/Zurich'
select value from dba_scheduler_global_attribute where attribute_name='DEFAULT_TIMEZONE';
  -- null return


5. ORA-01878: specified field not found in datetime or interval


ORA-01878 recurs due to illegal datetime obtained in DST aware Named TZ. Often datetime is initiated by Oracle built-in functions (sysdate, systimestamp, current_date, current_timestamp, localtimestamp), and then manipulated by arithmetic operators. All of year application runs without problem except once or twice it gets runtime ORA-01878 during Standard/DST switches, which is hard to reproduce, and hence held off by wait and hesitate to next switch.

(a). Time Zone Appending
when assigning from TIMESTAMP (or DATE) to TIMESTAMP WITH LOCAL TIME ZONE, default sessiontimezone is appended, and results in a non-exist datetime. For example,

alter session set time_zone = 'Europe/Paris';
declare
  l_date         DATE := to_date('2017-03-26 01:52:00', 'yyyy-mm-dd hh24:mi:ss'); 
  l_timestp_ltz  TIMESTAMP WITH LOCAL TIME ZONE;
begin
  l_timestp_ltz := l_date + 8/1440;
end;
/

ORA-01878: specified field not found in datetime or interval
ORA-06512: at line 5


Note: Clock Changes in Paris, ÃŽle-de-France, France 2017
  26 Mar 2017 - Daylight Saving Time Started
     When local standard time was about to reach
     Sunday, 26 March 2017, 02:00:00 clocks were turned forward 1 hour to 
     Sunday, 26 March 2017, 03:00:00 local daylight time instead.

  29 Oct 2017 - Daylight Saving Time Ended
     When local daylight time was about to reach
     Sunday, 29 October 2017, 03:00:00 clocks were turned backward 1 hour to 
     Sunday, 29 October 2017, 02:00:00 local standard time instead.

Following test code shows that localtimestamp evolution in expression (on the fly) is session time_zone irrelevant. However, when it refers to a SQL or PLSQL defined localtimestamp column or variable, it is appended with session time_zone, i.e, landed on that time_zone, hence has to be validated againt that time_zone.

create table ltz_tab(loc_ltz TIMESTAMP WITH LOCAL TIME ZONE);
insert into ltz_tab values(localtimestamp);
commit;     

alter session set time_zone = 'Europe/Paris';

declare
  l_intv      interval day (6) to second (0) :=  localtimestamp - timestamp'2017-03-26 02:17:39'; 
  l_ltz_new   timestamp with local time zone;
  l_val       number;
  l_boolean   boolean;
  l_ltz_init  timestamp with local time zone;
begin
  -- NO ORA-01878 in expression (on the fly)
  dbms_output.put_line('ltz calc = '||(localtimestamp - l_intv));  
  
  -- ORA-01878 when SQL compare
  begin
    select 1 into l_val from ltz_tab where loc_ltz > (localtimestamp - l_intv);  
  exception when others then dbms_output.put_line('ltz SQL compare: '||SQLERRM);
  end;
  
  -- ORA-01878 in SQL assigment
  begin
    select (localtimestamp - l_intv) into l_ltz_new from dual;       
  exception when others then dbms_output.put_line('ltz SQL assigment: '||SQLERRM);
  end;
  
  -- ORA-01878 in PL/SQL compare
  begin
    l_boolean := l_ltz_new  > (localtimestamp - l_intv);                         
  exception when others then dbms_output.put_line('ltz PL/SQL compare: '||SQLERRM);
  end;
  
  -- ORA-01878 in PL/SQL assigment
  begin
    l_ltz_new  := (localtimestamp - l_intv);                         
  exception when others then dbms_output.put_line('ltz PL/SQL assigment: '||SQLERRM);
  end;
  
  -- workaround-1, using time zone aware variable, '2017-03-26 02:17:39' mapped to '2017-03-26 03:17:39'
  l_ltz_init := localtimestamp;
  l_ltz_new  := l_ltz_init - l_intv; 
  dbms_output.put_line('ltz (workaround-1) = '||l_ltz_new); 
  
  -- workaround-2, using time zone aware function. current_timestamp is localtimestamp with session time zone
  l_ltz_new  := current_timestamp - l_intv; 
  dbms_output.put_line('ltz (workaround-2) = '||l_ltz_new); 
end;
/

---- Output ----
  ltz calc = 2017-03-26 02:17:39
  ltz SQL compare: ORA-01878: specified field not found in datetime or interval
  ltz SQL assigment: ORA-01878: specified field not found in datetime or interval
  ltz PL/SQL compare: ORA-01878: specified field not found in datetime or interval
  ltz PL/SQL assigment: ORA-01878: specified field not found in datetime or interval
  ltz (workaround-1) = 2017-03-26 03:17:39
  ltz (workaround-2) = 2017-03-26 03:17:39
If we make the following 5 tests with time_zone = 'Asia/Singapore', only the first and last have no errors, all other 3 hit: ORA-01878 since there does not exist Singapore time in interval ['1982-JAN-01 00:00:00', '1982-JAN-01 00:30:00'). (see Singapore Standard Time)

alter session set time_zone = 'Asia/Singapore';

select to_timestamp_tz('1982-DEC-31 23:59:59', 'YYYY-MON-DD hh24:mi:ss') from dual;
select to_timestamp_tz('1982-JAN-01 00:00:00', 'YYYY-MON-DD hh24:mi:ss') from dual;
select to_timestamp_tz('1982-JAN-01 00:00:01', 'YYYY-MON-DD hh24:mi:ss') from dual;
select to_timestamp_tz('1982-JAN-01 00:29:59', 'YYYY-MON-DD hh24:mi:ss') from dual;
select to_timestamp_tz('1982-JAN-01 00:30:00', 'YYYY-MON-DD hh24:mi:ss') from dual;
In 16 February 1942 (till 11 September 1945), Singapore Time offset was changed from GMT+07:30 to GMT+09:00 (east shift 1 and half hour) (see https://en.wikipedia.org/wiki/Singapore_Time).

Here some tests:

alter session set time_zone = 'ASIA/Singapore';
--OK
select '-1 second OK', to_timestamp_tz(to_char(date'1942-02-16' - interval '1' second, 'DD.MM.YYYY HH24:MI:SS'), 'DD.MM.YYYY HH24:MI:SS') from dual;
--ERR ORA-01878
select '+0 second ERR', to_timestamp_tz(to_char(date'1942-02-16', 'DD.MM.YYYY HH24:MI:SS'), 'DD.MM.YYYY HH24:MI:SS.FF') from dual;
select '+1 hour ERR', to_timestamp_tz(to_char(date'1942-02-16' + interval '1' hour, 'DD.MM.YYYY HH24:MI:SS'), 'DD.MM.YYYY HH24:MI:SS') from dual;
select '+89:59 minute to second ERR', to_timestamp_tz(to_char(date'1942-02-16' + interval '89:59' minute to second, 'DD.MM.YYYY HH24:MI:SS'), 'DD.MM.YYYY HH24:MI:SS') from dual;
--OK
select '+90 minute OK', to_timestamp_tz(to_char(date'1942-02-16' + interval '90' minute, 'DD.MM.YYYY HH24:MI:SS'), 'DD.MM.YYYY HH24:MI:SS') from dual;

--No problem if TZNAME are Hong_Kong or Paris
  alter session set time_zone = 'Asia/Hong_Kong';
  alter session set time_zone = 'Europe/Paris';
We can also look the time difference:

alter session set time_zone = 'ASIA/Singapore';

-- Delta = 07:30:00 in 1942-FEB-15
select 
  from_tz(timestamp '1942-02-15 10:00:00', 'GMT')             London_Time, 
  from_tz(timestamp '1942-02-15 10:00:00', 'Asia/Singapore')  Singapore_Time,
  from_tz(timestamp '1942-02-15 10:00:00', 'GMT') - from_tz(timestamp '1942-02-15 10:00:00', 'Asia/Singapore') Delta
from dual;

-- Delta = 09:00:00 in 1942-FEB-16
select 
  from_tz(timestamp '1942-02-16 10:00:00', 'GMT')             London_Time, 
  from_tz(timestamp '1942-02-16 10:00:00', 'Asia/Singapore')  Singapore_Time,
  from_tz(timestamp '1942-02-16 10:00:00', 'GMT') - from_tz(timestamp '1942-02-16 10:00:00', 'Asia/Singapore') Delta
from dual;

-- TS_GMT_TZ = 1942-FEB-15 02:30:00 GMT GMT  in 1942-FEB-15
select cast(timestamp'1942-02-15 10:00:00' as timestamp with time zone) ts,
       cast(timestamp'1942-02-15 10:00:00 Asia/Singapore' at time zone 'GMT' as timestamp) ts_gmt,
       cast(timestamp'1942-02-15 10:00:00 Asia/Singapore' at time zone 'GMT' as timestamp with time zone) ts_gmt_tz from dual;

–-TS_GMT_TZ = 1942-FEB-16 01:00:00 GMT GMT in 1942-FEB-16
select cast(timestamp'1942-02-16 10:00:00' as timestamp with time zone) ts,
       cast(timestamp'1942-02-16 10:00:00 Asia/Singapore' at time zone 'GMT' as timestamp) ts_gmt,
       cast(timestamp'1942-02-16 10:00:00 Asia/Singapore' at time zone 'GMT' as timestamp with time zone) ts_gmt_tz from dual;
(b). Time Zone Converting
When performing TIMESTAMP WITH TIME ZONE arithmetic, implicit timezone conversion is involved. For example,

alter session set time_zone = 'Europe/London';
declare
  l_timestp_tz    TIMESTAMP WITH TIME ZONE;
  l_timestp_tz2   TIMESTAMP WITH TIME ZONE;
begin
  l_timestp_tz   := to_timestamp_tz('2017-03-26 01:52:00 Europe/Paris', 'yyyy-mm-dd hh24:mi:ss tzr');
  dbms_output.put_line('l_timestp_tz  = '||l_timestp_tz);
  
  l_timestp_tz2  := l_timestp_tz + numtodsinterval(8,  'Minute');     -- OK
  dbms_output.put_line('l_timestp_tz + interval = '||l_timestp_tz2);
  l_timestp_tz2  := l_timestp_tz + 8/1440;                            -- ORA-01878
  dbms_output.put_line('l_timestp_tz + number   = '||l_timestp_tz2);
end;
/
  
l_timestp_tz  = 2017-MAR-26 01:52:00 EUROPE/PARIS CET
l_timestp_tz + interval = 2017-MAR-26 03:00:00 EUROPE/PARIS CEST
l_timestp_tz + number   = 2017-MAR-26 02:00:00 EUROPE/LONDON BST

Run the same code in 'Europe/Paris', hit ORA-01878:

alter session set time_zone = 'Europe/Paris';
declare
  l_timestp_tz   TIMESTAMP WITH TIME ZONE;
  l_timestp_tz2  TIMESTAMP WITH TIME ZONE;
begin
  l_timestp_tz   := to_timestamp_tz('2017-03-26 01:52:00 Europe/Paris', 'yyyy-mm-dd hh24:mi:ss tzr');
  dbms_output.put_line('l_timestp_tz  = '||l_timestp_tz);
  
  l_timestp_tz2  := l_timestp_tz + numtodsinterval(8,  'Minute');     -- OK
  dbms_output.put_line('l_timestp_tz + interval = '||l_timestp_tz2);
  l_timestp_tz2  := l_timestp_tz + 8/1440;                            -- ORA-01878, line 10
  dbms_output.put_line('l_timestp_tz + number   = '||l_timestp_tz2);
end;
/                                     

l_timestp_tz  = 2017-MAR-26 01:52:00 EUROPE/PARIS CET
l_timestp_tz + interval = 2017-MAR-26 03:00:00 EUROPE/PARIS CEST

ORA-01878: specified field not found in datetime or interval
ORA-06512: at line 10

It looks like that interval arithmetic is performed in natural sense, whereas number arithmetic is a pure math computation.

With Oracle function from_tz, we can construct a TIMESTAMP WITH TIME ZONE value by appending a time zone to a TIMESTAMP value. Following three tests showed different behaviour of Plsql vs Sql (Note that outer from_tz is a wrong usage since inner from_tz returns datatype TIMESTAMP WITH TIME ZONE, not datatype TIMESTAMP).

alter session set time_zone = 'Europe/Paris';
 
select sessiontimezone, dbtimezone from dual;
 
  SESSIONTIMEZONE  DBTIME
  ---------------  ------
  Europe/Paris     +01:00
 
 
begin
  dbms_output.put_line(
       from_tz(from_tz(timestamp'2021-03-28 02:00:00', 'UTC'), sessiontimezone)
  );
end;
/
 
  ORA-01878: specified field not found in datetime or interval
  ORA-06512: at line 2
 
 
select
    from_tz(from_tz(timestamp'2021-03-28 02:00:00', 'UTC'), sessiontimezone)
  from dual;
 
  ERROR at line 2:
  ORA-00932: inconsistent datatypes: expected TIMESTAMP got TIMESTAMP WITH TIME ZONE
 
 
with function func_ret (p_ts timestamp) return timestamp with time zone as
     begin
       return from_tz(from_tz(p_ts, 'UTC'), sessiontimezone);
     end;
select func_ret(timestamp'2021-03-28 02:00:00')
  from dual
/
 
  ORA-01878: specified field not found in datetime or interval
  ORA-06512: at line 5
By the way, tzname 'CET' and 'UTC' are different in respect of DST. In 'CET', Daylight Saving Time (DST) is in effect (DST aware); but in 'UTC', Daylight Saving Time has never been used (Not DST aware), so 'UTC' is the same as Offset TZ (00:00).

select tzname, tzabbrev from v$timezone_names where tzname in ('CET', 'UTC');
 
  TZNAME     TZABBREV
  ---------- ----------
  CET        LMT
  CET        CEST
  CET        CET
  UTC        GMT
Time Zone "Z" (Zulu Time Zone, Offset: TZ 00:00) is the same as "UTC" (DST unaware), but it is an nvalid timezone region in Oracle.

SQL > select to_timestamp_tz('2017-03-26 01:52:00 Z', 'YYYY-MM-DD HH24:MI:SS TZR') from dual;
        26-MAR-2017 01:52:00 +00:00

SQL > alter session set time_zone = 'Z';
        ERROR:
        ORA-01882: timezone region not found
In the following test, we can see Plsql CASE statement raising ORA-01830 special behaviour when returning mixed data types (date and varchar2).

-- Test Code
declare
  l_date date;
  l_ret  varchar2(32000);
begin
  l_date := date'1982-11-22';
  dbms_output.put_line('l_date = '||to_char(l_date, 'DD-MON-YYYY HH24:MI:SS'));
  
  -- CASE Statement Breaking down: OK
  l_ret := l_date;
  l_ret := to_char(l_date, 'DD-MON-YYYY HH24:MI:SS');

  -- CASE 1: OK
  l_ret :=  case when 1 = 1
                   then l_date
                   else to_char(l_date, 'DD-MON-YYYY HH24:MI:SS')
            end;
  dbms_output.put_line('Case 1: l_date = '||to_char(l_date, 'DD-MON-YYYY HH24:MI:SS'));
  
  -- CASE 2: OK       
  l_ret :=  case when 1 = 2
                   then to_char(l_date, 'DD-MON-YYYY HH24:MI:SS')
                   else l_date
            end;
  dbms_output.put_line('Case 2: l_date = '||to_char(l_date, 'DD-MON-YYYY HH24:MI:SS'));
  
  -- CASE 3: NOK              
  begin
    l_ret :=  case when 1 = 1
                     then to_char(l_date, 'DD-MON-YYYY HH24:MI:SS')
                     else l_date
              end;
    exception when others then dbms_output.put_line('Case 3: '||SQLERRM);
  end;

  -- CASE 4: NOK 
  begin
    l_ret :=  case when 1 = 2
                     then l_date
                     else to_char(l_date, 'DD-MON-YYYY HH24:MI:SS')
              end;
    exception when others then dbms_output.put_line('Case 4: '||SQLERRM);
  end;
end;
/

-- Output 
l_date = 22-NOV-1982 00:00:00
Case 1: l_date = 22-NOV-1982 00:00:00
Case 2: l_date = 22-NOV-1982 00:00:00
Case 3: ORA-01830: date format picture ends before converting entire input string
Case 4: ORA-01830: date format picture ends before converting entire input string
ORA-01878 in LOCALTIMESTAMP Calculations (Updated 2026-09-03)

     
--Europe/Paris Year 2026: Start of DST (Spring Forward): Sunday, March 29, 2026, at 02:00 AM local time. 
--Clocks moved forward 1 hour to 03:00 AM (shifting from UTC+1 to UTC+2).

-- Set time_zone as Named TZ (TZR) and use current_timestamp instead of localtimestamp, also fix ORA-01878

 
drop table test_ltz;
 
create table test_ltz (id number, timestamp timestamp(6) with local time zone);
 
insert into test_ltz values (1, localtimestamp);
 
commit;
 
declare
  l_dst_ts             timestamp := timestamp'2026-03-29 03:01:14 Europe/Paris';   
  l_interval_dst       INTERVAL DAY(4) TO SECOND(3);
  l_interval_dst_15m   INTERVAL DAY(4) TO SECOND(3);
  l_ltz_current        timestamp with local time zone;
  l_ltz_calc_indirect  timestamp with local time zone;
  l_ltz_calc_direct    timestamp with local time zone;
begin
  l_ltz_current       := localtimestamp;
  l_interval_dst      := l_ltz_current - l_dst_ts;
  dbms_output.put_line('l_interval_from_dst = '||l_interval_dst);
 
  l_interval_dst_15m := l_interval_dst + numtodsinterval(15, 'Minute');
  dbms_output.put_line('l_interval_dst_15m = '||l_interval_dst_15m);
 
  -- l_ltz_current is with timezone info, calculation with timezone info
  -- No ORA-01878, possible fix
  l_ltz_calc_indirect := l_ltz_current  - l_interval_dst_15m;
  dbms_output.put_line('l_ltz_calc_indirect 1 = '||l_ltz_calc_indirect);
 
  l_ltz_calc_indirect := l_ltz_current  - l_interval_dst_15m - interval '1' Hour;
  dbms_output.put_line('l_ltz_calc_indirect 2 = '||l_ltz_calc_indirect);
 
  -- No ORA-01878 if 1 hour earlier
  l_ltz_calc_direct := localtimestamp - l_interval_dst_15m - interval '1' Hour;
  dbms_output.put_line('l_ltz_calc_direct 1 = '||l_ltz_calc_direct);
 
  -- LOCALTIMESTAMP is datatype TIMESTAMP (without timezone)
  -- At first, Calculation without timezone info, then convert as TIMESTAMP WITH LOCAL TIME ZONE (with implicit sessiontimezone as default TimeZone)
  -- Hit ORA-01878: specified field not found in datetime or interval
  l_ltz_calc_direct := localtimestamp - l_interval_dst_15m;
  dbms_output.put_line('l_ltz_calc_direct 2 = '||l_ltz_calc_direct);
end;
/
 
---- Output----
 
l_interval_from_dst = +0158 11:20:31.794
l_interval_dst_15m = +0158 11:35:31.794
l_ltz_calc_indirect 1 = 29-MAR-2026 03:46:13
l_ltz_calc_indirect 2 = 29-MAR-2026 01:46:13
l_ltz_calc_direct 1 = 29-MAR-2026 01:46:14
declare
*
ERROR at line 1:
ORA-01878: specified field not found in datetime or interval
ORA-06512: at line 31


declare
  l_dst_ts             timestamp := timestamp'2017-03-26 03:00:06 Europe/Paris';   
  l_interval_dst       INTERVAL DAY(4) TO SECOND(3);
  l_interval_dst_10s   INTERVAL DAY(4) TO SECOND(3);
  l_ltz_current        timestamp with local time zone;
  l_ltz_calc_indirect  timestamp with local time zone;
  l_ltz_calc_direct    timestamp with local time zone;
begin
  l_ltz_current       := localtimestamp;
  l_interval_dst      := l_ltz_current - l_dst_ts;
  dbms_output.put_line('l_interval_from_dst = '||l_interval_dst);
 
  l_interval_dst_10s := l_interval_dst + numtodsinterval(10, 'Second');
  dbms_output.put_line('l_interval_dst_10s = '||l_interval_dst_10s);
 
  -- l_ltz_current is with timezone info, calculation with timezone info
  -- No ORA-01878
  for c in (select * from test_ltz where timestamp < l_ltz_current - l_interval_dst_10s)
  loop
    dbms_output.put_line('timestamp = '||c.timestamp);
  end loop;
 
  -- LOCALTIMESTAMP is datatype TIMESTAMP (without timezone)
  -- At first, Calculation without timezone info, then convert as TIMESTAMP WITH LOCAL TIME ZONE (with implicit sessiontimezone as default TimeZone)
  -- Hit ORA-01878: specified field not found in datetime or interval
  for c in (select * from test_ltz where timestamp < localtimestamp - l_interval_dst_10s)
  loop
    dbms_output.put_line('timestamp = '||c.timestamp);
  end loop;
end;
/
 
---- Output----
 
l_interval_from_dst = +3448 11:22:09.292
l_interval_dst_10s = +3448 11:22:19.292
declare
*
ERROR at line 1:
ORA-01878: specified field not found in datetime or interval
ORA-06512: at line 26
ORA-06512: at line 26

Monday, March 27, 2017

Oracle 12c PL/SQL Function in the WITH Clause: wrong result

In order to make PL/SQL Function run faster in SQL, Oracle 12c introduced a new feature to integrate PL/SQL Function in the SQL WITH Clause to eliminate SQL-PLSQL context switching.

This Blog is trying to demonstrate that the outcome of this new feature is varied with "ORDER BY" clause.

Note: Tested in Oracle 12.1.0.2.0 on AIX, Solaris, Linux.


1. Build Test



drop table base_tab cascade constraints;

create table base_tab
(
  id    number(12) not null,
  seq   number(9)                    
);

create index base_tab#ix_1 on base_tab (seq, id);

create index base_tab#ix_2 on base_tab (id);

drop table date_tab cascade constraints;

create table date_tab
(
  id          number(12),
  base_id     number(12),
  come_date   date      ,
  constraint date_tab#pk primary key (base_id, come_date) enable validate
) organization index;

insert into base_tab (id, seq) values (1, 11111);
insert into base_tab (id, seq) values (2, 22222);
insert into base_tab (id, seq) values (3, 33333);
insert into base_tab (id, seq) values (4, 4444);

insert into date_tab (id, base_id, come_date) values (11, 1, to_date('2010-01-11', 'yyyy-mm-dd'));
insert into date_tab (id, base_id, come_date) values (33, 3, to_date('2030-03-13', 'yyyy-mm-dd'));
insert into date_tab (id, base_id, come_date) values (44, 4, to_date('2040-04-14', 'yyyy-mm-dd'));

commit;


2. Run Test



--*** test 1: returns 2 rows without order by *** --

with 
  function get_max_date_sqlwith (p_id in number) return date is
    l_date date;
  begin
    select come_date into l_date from (
      select come_date from date_tab where base_id = p_id
       order by come_date desc
       ) where rownum = 1;
    return l_date;
  end;
select b.id
     ,(select get_max_date_sqlwith(b.id) from dual) cdate
  from base_tab b
 where rownum <= 2
--  order by b.id
/

 ID CDATE
--- -----------
  4 14-APR-2040
  1 11-JAN-2010


--*** test 2: returns only 1 row with order by *** --

with 
  function get_max_date_sqlwith (p_id in number) return date is
    l_date date;
  begin
    select come_date into l_date from (
      select come_date from date_tab where base_id = p_id
       order by come_date desc
       ) where rownum = 1;
    return l_date;
  end;
select b.id
     ,(select get_max_date_sqlwith(b.id) from dual) cdate
  from base_tab b
 where rownum <= 2
 order by b.id
/

 ID CDATE
--- -----------
  1 11-JAN-2010

--*** test 3 with_plsql hint: returns 2 rows without order by *** --

select /*+ with_plsql */ * from 
(with 
  function get_max_date_sqlwith (p_id in number) return date is
    l_date date;
  begin
     select come_date into l_date from (
      select come_date from date_tab t where t.base_id = p_id
       order by come_date desc
       ) where rownum = 1;
    return l_date;
  end;
select b.id, get_max_date_sqlwith(b.id) cdate 
  from base_tab b) x
 where rownum <= 2
--  order by x.id
/

 ID CDATE
--- -----------
  4 14-APR-2040
  1 11-JAN-2010

--*** test 4 with_plsql hint: returns only 1 row with order by *** --

select /*+ with_plsql */ * from 
(with 
  function get_max_date_sqlwith (p_id in number) return date is
    l_date date;
  begin
     select come_date into l_date from (
      select come_date from date_tab t where t.base_id = p_id
       order by come_date desc
       ) where rownum = 1;
    return l_date;
  end;
select b.id, get_max_date_sqlwith(b.id) cdate 
  from base_tab b) x
 where rownum <= 2
 order by x.id
/

 ID CDATE
--- -----------
  1 11-JAN-2010


3. Standalone PL/SQL function



--*** test 5: returns 2 rows without order by *** --

create or replace function get_max_date (p_id in number) return date is
  l_date date;
begin
  select come_date into l_date from (
    select come_date from date_tab where base_id = p_id
     order by come_date desc
     ) where rownum = 1;
  return l_date;
end;
/

select b.id
     ,(select get_max_date(b.id) from dual) cdate
  from base_tab b
 where rownum <= 2
--  order by b.id
/

 ID CDATE
--- -----------
  4 14-APR-2040
  1 11-JAN-2010

--*** test 6: returns 2 rows with order by *** --

select b.id
     ,(select get_max_date(b.id) from dual) cdate
  from base_tab b
 where rownum <= 2
 order by b.id
/

 ID CDATE
--- -----------
  1 11-JAN-2010
  2


4. 12c UDF Pragma PL/SQL function



--*** test 7: returns 2 rows without order by *** --

create or replace function get_max_date_UDF (p_id in number) return date is
  l_date date;
  PRAGMA UDF;
begin
  select come_date into l_date from (
    select come_date from date_tab where base_id = p_id
     order by come_date desc
     ) where rownum = 1;
  return l_date;
end;
/

select b.id
     ,(select get_max_date_UDF(b.id) from dual) cdate
  from base_tab b
 where rownum <= 2
--  order by b.id
/

 ID CDATE
--- -----------
  4 14-APR-2040
  1 11-JAN-2010
  
--*** test 8: returns 2 rows with order by *** --

select b.id
     ,(select get_max_date_UDF(b.id) from dual) cdate
  from base_tab b
 where rownum <= 2
 order by b.id
/

 ID CDATE
--- -----------
  1 11-JAN-2010
  2

Monday, March 20, 2017

Oracle abstract_lob Memory Leak

Oracle MOS:
    Bug 14521799 : XMLTYPE.GETCLOBVAL IN AN ANONYMOUS BLOCK VIA EXECUTE IMMEDIATE AND USING LEAKS
reveals a CLOB memory leak.

This Blog will try to demonstrate that it is an abstract_lob Memory Leak.

In Oracle 10g, "What's New in Large Objects?" wrote:
    A new column named 'ABSTRACT_LOBS' has been added to the V$TEMPORARY_LOBS table.
    This column displays the number of abstract LOBs accumulated in the current session.
    Abstract LOBs are temporary lobs returned from queries involving XMLType columns.

Note: all tests are done in Oracle 12.1.0.2.0. See appended Test Code. Case C1 is from MOS Bug 14521799.


1. Run following 3 Tests, each time in a new SQL Session.



SQL (111) > exec test_run_all(1024, 2);

PGA_MEM(MB):Used=7, Alloc=8, Max=11--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=0
------ TestCase ------ C1
PGA_MEM(MB):Used=15,Alloc=16,Max=16--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=1024
------ TestCase ------ C2
PGA_MEM(MB):Used=24,Alloc=25,Max=25--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=2048
------ TestCase ------ C3
PGA_MEM(MB):Used=24,Alloc=25,Max=25--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=2049
------ TestCase ------ C4
PGA_MEM(MB):Used=24,Alloc=26,Max=26--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=2049
------ TestCase ------ C5
PGA_MEM(MB) Used=24,Alloc=26,Max=26--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=2049


SQL (222) > exec test_run_all(1024, 1024);

PGA_MEM(MB): Used=7, Alloc=8, Max=11--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=0
------ TestCase ------ C1
PGA_MEM(MB): Used=15,Alloc=16,Max=16--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=1024
------ TestCase ------ C2
PGA_MEM(MB): Used=24,Alloc=25,Max=25--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=2048
------ TestCase ------ C3
PGA_MEM(MB): Used=24,Alloc=26,Max=26--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=2049
------ TestCase ------ C4
PGA_MEM(MB): Used=24,Alloc=26,Max=26--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=2049
------ TestCase ------ C5
PGA_MEM(MB): Used=24,Alloc=26,Max=26--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=2049


SQL (333) > exec test_run_all(1024, 1024*16);

PGA_MEM(MB): Used=7,Alloc=8,  Max=11--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=0
------ TestCase ------ C1
PGA_MEM(MB): Used=49,Alloc=50,Max=50--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=1024
------ TestCase ------ C2
PGA_MEM(MB): Used=92,Alloc=93,Max=93--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=2048
------ TestCase ------ C3
PGA_MEM(MB): Used=92,Alloc=93,Max=93--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=2049
------ TestCase ------ C4
PGA_MEM(MB): Used=92,Alloc=93,Max=93--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=2049
------ TestCase ------ C5
PGA_MEM(MB): Used=92,Alloc=93,Max=93--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=2049

In Case C1 and C2, PGA_MEM is inflated, hence PGA memory leak; Augmented ABSTRACT_LOBS indicates that the leak is located in abstract_lobs, whereas in Case C3, C4 and C5, PGA_MEM and ABSTRACT_LOBS are constant.

Furthermore, LOB length 2 and 1024 consumes the same amount of memory, so there is certain minimum size for each ABSTRACT_LOB.

Oracle9i Docu: Temporary LOB Performance Guidelines in Oracle9i Application Developer's Guide - Large Objects (LOBs) has a Note:

Temporary LOBs created using a session locator are not cleaned up automatically at the end of function or procedure calls. The temporary LOB should be explicitly freed by calling DBMS_LOB.FREETEMPORARY().

which talks about "session locator", and there is nowhere mentioned "abstract_lob". It is not clear how both are related.

But since Oracle 10g, we can't find this Note any more.

Here is the new link of Oracle 12.2: Temporary LOB Performance Guidelines SecureFiles and Large Objects Developer's Guide.

It contains details about Temporary LOB usage and shows how to get LOB Access Statistics in dynamic performance views.


2. Open a new SQL Session, Run Case C1:



prompt -------- 1st Block --------

begin
 test_run(1, 2, 'C1');
 test_run(100, 2, 'C1');
 test_run(10000, 2, 'C1');
end;
/

prompt -------- 2nd Block --------

begin
 test_run(1, 2, 'C1');
 test_run(100, 2, 'C1');
 test_run(10000, 2, 'C1');
end;
/

-------- 1st Block --------

PGA_MEM(MB): Used=7, Alloc=8, Max=11--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=1
PGA_MEM(MB): Used=7, Alloc=8, Max=11--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=101
PGA_MEM(MB): Used=95,Alloc=96,Max=96--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=10101

-------- 2nd Block --------

PGA_MEM(MB): Used=95,Alloc=96,Max=96--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=1
PGA_MEM(MB): Used=95,Alloc=96,Max=96--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=101
PGA_MEM(MB): Used=95,Alloc=96,Max=96--TEMPORARY_LOBS:CACHE_LOBS=0,NOCACHE_LOBS=0,ABSTRACT_LOBS=10101

The output shows that the PGA_MEM is not released (leak) between two Blocks, however ABSTRACT_LOBS count is reset.That means once the call if terminated, abstract_lobs in v$temporary_lobs is reset to 0, none abstract_lobs is exposed any more in this view, however their PGA memory is not released.


3. Test till ORA-04030


This test is trying to allocate more than 32GB PGA, and it will take about half hour till hitting ORA-04030 ERROR.

During the test, open another new SQL session, sample PGA memory by:

SQL(555) > exec pga_sampling(777, 3600);

See Blog: dbms_session.get_package_memory_utilization and limitations

Open one more new SQL session, and watch sampling result by (only partial result are shown):

SQL(666) > select * from process_memory_detail_v order by timestamp desc, bytes desc; 

CATEGORY  NAME                  HEAP_NAME         BYTES            ALLOCATION_COUNT 
-------   ---------------       ---------------   --------------   ---------------- 
                                                  
Other     permanent memory      kokltcr: creat    31,680,073,912       8,579,890
Other     free memory           kokltcr: creat       648,951,096       6,239,915
Other     free memory           session heap         616,868,032         445,136
Other     kokltcr: create clob  koh dur heap d       299,515,712       1,559,980

Open a new SQL Session, Run Case C1 and it will raise ORA-04030:
        
SQL(777) > exec test_run(1024*1024*2, 1024*16, 'C1');
------ TestCase ------ C1
BEGIN test_run(1024*1024*2, 1024*16, 'C1'); END;

*
ERROR at line 1:
ORA-04030: out of process memory when trying to allocate 4040 bytes (kokltcr: creat,kghsseg: kolaslCreateCtx)
ORA-06512: at "S.CRE_CLOB", line 10

The incident file looks like:

ORA-04030: out of process memory when trying to allocate 169040 bytes (pga heap,kgh stack)
ORA-04030: out of process memory when trying to allocate 4040 bytes (kokltcr: creat,kghsseg: kolaslCreateCtx)

========= Dump for incident 22642 (ORA 4030) ========
----- Beginning of Customized Incident Dump(s) -----
=======================================
TOP 10 MEMORY USES FOR THIS PROCESS
---------------------------------------

*** 2017-03-17 22:06:16.101
95%   30 GB, 8600973 chunks: "permanent memory          "  
         kokltcr: creat  ds=fffffd77ec09d628  dsprt=fffffd7ffbebb900
 2%  620 MB, 6255235 chunks: "free memory               "  
         kokltcr: creat  ds=fffffd77ec09d628  dsprt=fffffd7ffbebb900
 2%  590 MB, 446319 chunks: "free memory               "  
         session heap    ds=fffffd7ffc02d728  dsprt=fffffd7ffc358350
 1%  286 MB, 1563814 chunks: "kokltcr: create clob      "  
         koh dur heap d  ds=fffffd7ffbebb900  dsprt=fffffd7ffc02d728
 0%   62 MB, 781909 chunks: "kolraloc-1                "  
         kolr heap ds i  ds=fffffd7ffc048488  dsprt=fffffd7ffc02d728
 0%   61 MB, 3850 chunks: "kolrde_alloc              "  
         koh-kghu sessi  ds=fffffd7ffc05edd8  dsprt=fffffd7ffc02d728
 0%   48 MB, 781907 chunks: "kolrarfc:lobloc_kolrhte   "  
         kolr heap ds i  ds=fffffd7ffc048488  dsprt=fffffd7ffc02d728
 0%   27 MB, 195483 chunks: "free memory               "  
         koh dur heap d  ds=fffffd7ffbebb900  dsprt=fffffd7ffc02d728
 0%  828 KB, 17329 chunks: "free memory               "  
         kolr heap ds i  ds=fffffd7ffc048488  dsprt=fffffd7ffc02d728
 0%  505 KB,  34 chunks: "permanent memory          "  
         pga heap        ds=fffffd7ffc345640  dsprt=0

We can see that "30 GB, 8600973 chunks" are allocated as "permanent memory", that probably explains why it is not reclaimable and hence a memory leak.


4. Temporary LOBs: CACHE_LOBS, NOCACHE_LOBS, ABSTRACT_LOBS


Here a small test of Temporary LOBs: CACHE_LOBS, NOCACHE_LOBS, ABSTRACT_LOBS, and their space usage (tested in Oracle 12cR1 and 12cR2).

------------------------------ SetUp -------------------------------
create or replace package lob_cache_test_pkg as
 g_CACHE_LOBS    clob;
 g_NOCACHE_LOBS  clob;
 g_ABSTRACT_LOBS clob;
end;
/

create or replace procedure lob_cache_test_CACHE_LOBS (p_cnt number) as 
 l_txt varchar2(10) := '0123456789';
begin
  for i in 1..p_cnt loop
    dbms_lob.createtemporary(
      lob_loc => lob_cache_test_pkg.g_CACHE_LOBS, cache => true, dur => dbms_lob.call);
    -- without assignment, or with "dbms_lob.writeappend" create CACHE_LOBS. 
    dbms_lob.writeappend(lob_loc => lob_cache_test_pkg.g_CACHE_LOBS, amount => 10, buffer => l_txt);
  end loop;
end;
/

create or replace procedure lob_cache_test_NOCACHE_LOBS (p_cnt number) as 
 l_txt varchar2(10) := '0123456789';
begin
  for i in 1..p_cnt loop
    dbms_lob.createtemporary(
      lob_loc => lob_cache_test_pkg.g_NOCACHE_LOBS, cache => false, dur => dbms_lob.call);
    -- without assignment, or with "dbms_lob.writeappend" create NOCACHE_LOBS. 
    dbms_lob.writeappend(lob_loc => lob_cache_test_pkg.g_NOCACHE_LOBS, amount => 10, buffer => l_txt);
  end loop;
end;
/

-- 12cR1 not reported space (BLOCKs) usage of ABSTRACT_LOBS in v$tempseg_usage. 
-- But 12cR2 Reported, and put them into CACHE_LOBS.
create or replace procedure lob_cache_test_ABSTRACT_LOBS (p_cnt number) as 
 l_txt varchar2(10) := '0123456789';
begin
  for i in 1..p_cnt loop
    dbms_lob.createtemporary(
      lob_loc => lob_cache_test_pkg.g_ABSTRACT_LOBS, cache => true, dur => dbms_lob.call);
    -- with direct text assignment create ABSTRACT_LOBS. 
    lob_cache_test_pkg.g_ABSTRACT_LOBS := l_txt;
  end loop;
end;
/
Run test and show the output:

--------------------------- Test on 12cR2 ---------------------------
-- It takes hours with "dbms_session.reset_package" to free created temporary LOBs 
-- if their number is high (more than 1,000,000). 
-- LOB Subroutine Callstack: kdlt_freetemp -> kdl_destroy -> kdlclose -> memcmp
--
-- ALTER SYSTEM KILL SESSION 'sid,serial#' releases memory immediately.

exec dbms_session.reset_package; 

select l.*, t.blocks  --t.* 
from v$session s, v$temporary_lobs l, v$tempseg_usage t
where s.sid = l.sid and s.saddr = t.session_addr;

exec lob_cache_test_CACHE_LOBS(1122);

select l.*, t.blocks  --t.* 
from v$session s, v$temporary_lobs l, v$tempseg_usage t
where s.sid = l.sid and s.saddr = t.session_addr;

exec lob_cache_test_NOCACHE_LOBS(1133);

select l.*, t.blocks  --t.* 
from v$session s, v$temporary_lobs l, v$tempseg_usage t
where s.sid = l.sid and s.saddr = t.session_addr;

exec lob_cache_test_ABSTRACT_LOBS(1144);

select l.*, t.blocks  --t.* 
from v$session s, v$temporary_lobs l, v$tempseg_usage t
where s.sid = l.sid and s.saddr = t.session_addr;

--------------------------- Test Result on 12cR2 ---------------------------
   SID CACHE_LOBS NOCACHE_LOBS ABSTRACT_LOBS     CON_ID     BLOCKS
  ---- ---------- ------------ ------------- ---------- ----------
   738          0            0             0          0          0

SQL > exec lob_cache_test_CACHE_LOBS(1122);

   SID CACHE_LOBS NOCACHE_LOBS ABSTRACT_LOBS     CON_ID     BLOCKS
  ---- ---------- ------------ ------------- ---------- ----------
   738       1122            0             0          0       1280
      
SQL > exec lob_cache_test_NOCACHE_LOBS(1133);

   SID CACHE_LOBS NOCACHE_LOBS ABSTRACT_LOBS     CON_ID     BLOCKS
  ---- ---------- ------------ ------------- ---------- ----------
   738       1122         1133             0          0       2304

SQL > exec lob_cache_test_ABSTRACT_LOBS(1144);

   SID CACHE_LOBS NOCACHE_LOBS ABSTRACT_LOBS     CON_ID     BLOCKS
  ---- ---------- ------------ ------------- ---------- ----------
   738       2266         1133          1144          0       3584


5. Test Code



create or replace function cre_clob(p_clob_len number) return clob as
  --l_text varchar2(1024) := lpad('a', 1024, 'b');
  l_clob clob;
  --l_blob blob;                    -- BLOB has similar behaviour
  --l_raw  raw(100) := '4b53554e';
  l_text  varchar2(32767);
begin
  --x := 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
  --dbms_lob.createtemporary(l_clob, true, dbms_lob.session);
  --for i in 1..1024 loop
  -- dbms_lob.writeappend(l_clob, length(l_text) , l_text);
  --end loop;
  --l_blob := l_raw;
  --return l_blob;
  
  l_text := lpad('a', p_clob_len, 'b');
  
  l_clob := l_text;
  return l_clob;
end;
/

create or replace type t_clob as object(c clob);
/

create or replace type t_clob_tab as table of clob;
/

create or replace procedure print_lob_and_mem as
 l_ret varchar2(400); 
 l_sid number := sys.dbms_support.mysid;
 l_mb  number := 1024*1024;
begin
  select 'PGA_MEM(MB): '||'Used='||round(p.pga_used_mem/l_mb)||', Alloc='||round(p.pga_alloc_mem/l_mb)||', Max='||round(p.pga_max_mem/l_mb)||
        ' --- TEMPORARY_LOBS: '||'CACHE_LOBS='||cache_lobs||', NOCACHE_LOBS='||nocache_lobs||', ABSTRACT_LOBS='||abstract_lobs
   into l_ret
   from v$process p, v$session s, v$temporary_lobs l
 where p.addr=s.paddr and s.sid = l.sid and s.sid = l_sid;
 dbms_output.put_line(l_ret);
end;
/

--exec print_lob_and_mem;

create or replace procedure test_run(p_cnt number, p_clob_len number, p_case varchar2) as
  l_stmt_var_c1 varchar2(100); 
  l_stmt_var_c2 varchar2(100); 
  l_stmt_var_c3 varchar2(100);
  l_stmt_var_c4 varchar2(100);
  l_clob        clob; 
  l_clob_t      t_clob     := t_clob(null);
  l_clob_tab    t_clob_tab := t_clob_tab();
begin 
  l_stmt_var_c1 := 'begin select cre_clob('||p_clob_len||') into :c1 from dual; end;';
  l_stmt_var_c2 := 'begin select cre_clob('||p_clob_len||') into :c1 from dual; end;'; 
  l_stmt_var_c3 := 'begin select t_clob(cre_clob('||p_clob_len||')) into :c1 from dual; end;';  
  l_stmt_var_c4 := 'begin select cre_clob('||p_clob_len||') bulk collect into :c1 from dual connect by level <= 1; end;'; 
  
  dbms_output.put_line('------ TestCase ------ '||p_case);
  
  for i in 1..p_cnt loop 
   case p_case
    when 'C1' then
      execute immediate l_stmt_var_c1 using out l_clob;          -- abstrace_lob increasing
        --dbms_lob.freetemporary(l_clob);           -- no help
        --dbms_session.free_unused_user_memory();   -- no help
      when 'C2' then
        execute immediate l_stmt_var_c2 using out l_clob_t.c;    -- abstrace_lob increasing
        l_clob := l_clob_t.c;
      when 'C3' then
        execute immediate l_stmt_var_c3 using out l_clob_t;      -- abstrace_lob constant
        l_clob := l_clob_t.c;
      when 'C4' then
        execute immediate l_stmt_var_c4 using out l_clob_tab;    -- abstrace_lob constant
        l_clob := l_clob_tab(1);
      when 'C5' then
         l_clob := cre_clob(p_clob_len);                         -- abstrace_lob constant
    end case;
  end loop; 
  
  print_lob_and_mem;
end; 
/

--exec test_run(100, 1024, 'C1');

create or replace procedure test_run_all(p_cnt number, p_clob_len number) as
begin
 print_lob_and_mem;
 test_run(p_cnt, p_clob_len, 'C1');
 test_run(p_cnt, p_clob_len, 'C2');
 test_run(p_cnt, p_clob_len, 'C3');
 test_run(p_cnt, p_clob_len, 'C4');
 test_run(p_cnt, p_clob_len, 'C5');
end;
/