Skip to Main Content
  • Questions
  • Convert date to string and compare, or convert string to date and compare?

Breadcrumb

Question and Answer

Connor McDonald

Thanks for the question, Antonio.

Asked: May 07, 2012 - 9:54 am UTC

Last updated: November 11, 2019 - 1:31 am UTC

Version: 11.1.0 - 64bits

Viewed 100K+ times! This question is

You Asked

Tom,

please consider this:

I have a database/table with a DATE column that has no index and has the format "DD-MM-YYYY".

I would like to use a standard format in my PL/SQL code that does not lead to implicit database conversions between date formats in the table's date columns and user date/string parameters, and at the same time be portable/reusable if the date formats change. I typically chose 'YYYY-MM-DD'.

which method should I use (or other if you have a better one):

1)
where dateCol = to_date(:param, 'YYYY-MM-DD')

2)
where to_date(dateCol, 'YYYY-MM-DD') = to_date(:param, 'YYYY-MM-DD')

3)
where to_char(dateCol, 'YYYY-MM-DD') = :param

4)
where to_char(dateCol, 'YYYY-MM-DD') = to_char(:param, 'YYYY-MM-DD')

thank you,
Antonio

and Tom said...

I have a database/table with a DATE column that has no index and has the format "DD-MM-YYYY".


No, a date doesn't have a format! A date is 7 bytes of binary information - a byte each for the century, year, month, day, hour, minute, and second. It gets formatted when fetched as a string by a client or when implicitly converted in SQL or PLSQL.

Your database might have a NLS_DATE_FORMAT set to dd-mm-yyyy - but your date doesn't!


You should always explicitly convert the STRING into a DATE to compare. ALWAYS.


1) that is the correct way to do that.

2) that is horrible - NEVER do that. that is really:

where to_date( TO_CHAR(dateCol), 'YYYY-MM-DD' ) = to_date(:param,'YYYY-MM-DD')

first - there is that implicit to_char taking place! And if your default date mask it DD-MM-YYYY - it wouldn't even work!!

Never allow implicit conversions
Convert the less specific type (string) into the more specific type (date)

3) that would be a non-performant approach. do you want to convert EVERY ROW from a date to string to compare?

Or do you want to convert a string into a date ONCE and compare.

Also, using to_char() on the database date column would typically obviate the use of any indexes

4) see #2



#1 all the way

Rating

  (18 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

calling from JCAPS

Antonio Lopes, May 15, 2012 - 11:47 am UTC

Tom, thank you for your explanation. However this is a bit more tricky.

I have a Java client invoking a Stored Procedure (SP) using JCAPS. The SP receives a DATE parameter.

My NLS_DATE_FORMAT is RR.MM.DD

this SP in turns calls another SP that receives again a DATE parameter and does the following:

l_sql := to_char(p_date, 'YYYY-MM-DD');

when a client calls the main SP it gets the error:
ORA-01861: literal does not match format string

i confirmed that the error is hit when making the to_char() in the second SP.

the client is passing a java.sql.Date '1920-01-01'

i could only make the call work by putting this at the start of the:

execute immediate 'alter session set NLS_DATE_FORMAT=''RRRR-MM-DD''';

I can't find the logic behind this since the DATE type does not store any format like you said.

How come a DATE value passed by a Java client get into trouble when making a to_char ??

it is blowing my mind :|


1)
select to_char( to_date('01-01-1920', 'DD-MM-YYYY') , 'YYYY-MM-DD') from dual;

1920-01-01

2)
select to_char( to_date('01-01-1920', 'DD-MM-YYYY') , 'RRRR-MM-DD') from dual;

1920-01-01

3)
select to_char( to_date('01-01-1920', 'DD-MM-YYYY') , 'DD-MM-YYYY') from dual;

01-01-1920

4)
select to_char( to_date('1920-01-01', 'YYYY-MM-DD') , 'DD-MM-YYYY') from dual;

01-01-1920

5)
select to_char( to_date('1920-01-01', 'YYYY-MM-DD') , 'DD-MM-RRRR') from dual;

01-01-1920


but if i start making to_date(to_date())

1)
select to_date( to_date('1920-01-01', 'YYYY-MM-DD') , 'DD-MM-RRRR') from dual;

20-01-2001

2)
select to_date( to_date('1920-01-01', 'YYYY-MM-DD') , 'DD-MM-YYYY') from dual;

20-01-0001

what's the logic behind this? i can't seem to get it
Tom Kyte
November 30, 2012 - 7:51 am UTC

No clue what JCAPS is or does or why it is even relevant.


I care not what the NLS_DATE_FORMAT is - anyone that relies on it being set to a specific value is in for a world of hurt. You should use explicit formats in your to_date and to_char calls.



I'll need to see code and something that is reproducible. You are doing something wrong - there is an implicit conversion happening somewhere that is messing up your data.

I need code - I don't follow your flow, I don't see any specs, nothing.


How come a DATE value passed by a Java client get into trouble when making a
to_char ??


show us explicitly how this java program is binding and passing said "date"




to_date(to_date())


to_date takes a CHARACTER STRING to to_date( to_date() ) is really:

to_date( TO_CHAR( to_date() ) )

you are throwing in an implicit conversion from date to string in the middle of everything - mucking it up entirely.


Antonio Lopes, May 16, 2012 - 9:18 am UTC

Hi Tom,

the reason I am making to_date(to_date()) is to try to simulate the passing of a date type to the SP from the client.

this is because there is no problem whatsoever with my code in the SPs. if I make my tests in Sql*Plus it works as expected with no ORA-01861. i tried different date formats as input values.

but the client gets this error.

It is not the first time that this happens. another team has met a similar issue when receiving DATE arguments from a client in JAVA+JCAPS. the solution was to make in the SP a

to_date(date_param, 'YYYY-MM-DD')

where
date_param IN DATE

as strange as it sounds it worked.

in my case I am getting a somewhat different error because I get the error when doing

to_char(date_param, 'YYYY-MM-DD');

where
date_param IN DATE


the code is simply something like this:

create procedure sp2( p_date IN DATE )
is
l_sql varchar2(2000)
begin

(...)
l_sql := l_sql || to_char(p_date, 'YYYY-MM-DD');
(...)

end;

create procedure sp1( p_date IN DATE )
is
begin
(...)
sp2(p_date);
(...)

end;

i can only make this work with the client call if I apply this solution:

a)
at the beginning of SP1:
execute immediate 'alter session set NLS_DATE_FORMAT=''RRRR-MM-DD''';


the bogus thing is if I put this call to SP2:

sp2(to_date(p_date,'YYYY-MM-DD'));

the client gets:
ORA-01847: day of month must be between 1 and last day of month

given the submitted date '1920-01-01' I guess that in someway the date is being "read" in Oracle as DD-MM-YYYY.

if i call SP2 in the normal/initial form:

sp2(p_date);

the client gets:
ORA-01861: literal does not match format string

which is hit in SP2 with the instruction:
l_sql := l_sql || to_char(p_date, 'YYYY-MM-DD');


SO, i can't make any sense out of this. the only conclusion is that JCAPS has a bug in the way he sends DATEs to Oracle, but even then I can't see how Oracle gets confused with this parameter considering the tests I made above.

Tom, my apologies for this somewhat out-of-topic question but maybe it can help someone else that may be using JCAPS with Oracle.

I understood however your teachings:
1) a date does not have any format
2) doing to_date(to_date()) leads to implicit to_char() which can lead to some unexpected behaviors.
3) for best performance, in a query keep your date column intact and only mess with your input parameter:
where my_date_column = to_date(my_date_param)

Tom Kyte
May 17, 2012 - 2:36 am UTC

the reason I am making to_date(to_date()) is to try to simulate the passing of
a date type to the SP from the client.


then that is your problem - don't you get it?

to_date( to_date() )

is to_date( TO_CHAR( to_date() ) )


client should bind a date - then there is no to_date, there is no to_char, there is no conversion.




I keep saying:

... i tried
different date formats as input values. ...

date format, who cares - you have dates, dates are 7 bytes of binary information. A format is used to

a) pretty print a date using to_char
b) describe how to_date will convert a string into 7 bytes of binary information.


to_date(date_param, 'YYYY-MM-DD')

where
date_param IN DATE

as strange as it sounds it worked.


that does not sound "strange", that sounds like:

a) date_param was NOT A DATE, date_param was a string
b) date_param was a string that held something like '2011-01-02'
c) the stored procedure wanted a 7 byte binary date field - to_date of the string "date_param" using that format was the right way to supply it.


doesn't sound STRANGE at all. In fact, it sounds like what I've been saying: avoid implicit conversions, avoid default nls_date_format.

It sounds like you explicitly converted a string to a date using a well formed, known date format to me.

Sounds totally normal.
Sounds like what I've been saying



create procedure sp2( p_date IN DATE )
is
  l_sql varchar2(2000)
begin 

  (...)
  l_sql := l_sql || to_char(p_date, 'YYYY-MM-DD');
  (...)



your kidding right? Please tell me you don't do that, please tell me you are really using bind variables - pretty please? Please tell me you are not really concatenating the date into a sql statement?

You do realize what happens with that right? You concatenate the date into the string - AS A STRING - so then when you execute the SQL you have to IMPLICITLY CONVERT THE STRING BACK INTO A DATE USING THE DEFAULT NLS_DATE_FORMAT.

Pretty please tell me it ain't so????



a)
at the beginning of SP1:
execute immediate 'alter session set NLS_DATE_FORMAT=''RRRR-MM-DD''';


oh, come on. Why would you do that. Why wouldn't you just

a) bind a date - no formats ANYWHERE - best solution

b) bind a string - using EXPLICIT FORMATS TO CONVERT IT IN THE SQL STATEMENT

c) there is no C



but even then I can't see how Oracle
gets confused with this parameter considering the tests I made above.


trust me, Oracle isn't confused here, not at all.


Antonio Lopes, May 17, 2012 - 6:11 am UTC

Tom, yes i know now the error I was making in trying to simulate the passing of a DATA parameter to my SP by making to_date(to_date()) and similar.

the client binds a DATE, or so it is supposed to when using JCAPS+Java. the SP receives a date. there are no to_char or to_date in between.

the date has no format, i get it. but the fact is this is not working. when I make the to_char(p_date) i get an ORA-01861. How can i get this error from a date parameter that has no format then?

yes, i know the to_char() will lead to a to_date() in execution time. in this case the situation is more complex than what i have shown and there is no time and human power to change this bit of the (bigger) query to use bind variables. trust me in this case.

the issue is i need to do a to_char() from a DATE variable and I can't without explicitly setting the DATE format on the session.

my intuition says that the problem is somehow related to the database date format being RR.MM.DD and the session format established by the client to be DD.MON.RR or something like that. this is why if i make a alter session the call runs fine. WHY this has a problem when using a DATE i don't know. it should be obvious but i don't get it.




Tom Kyte
May 18, 2012 - 2:14 am UTC

show the entire set of code we need to see to reproduce this - from the java snippet that sets a date, binds it, calls the procedure to the procedure (which is tiny - it'll only need about one line of code) failing.

short of that - we are not going to be able to debug your code since we don't actually have your code.


the issue is i need to do a to_char() from a DATE variable and I can't without
explicitly setting the DATE format on the session.,


seriously - after all that has been said?????


to_char( date_field, 'dd-mon-yyyy hh23:mi:ss' )


the date format should be supplied in the to_char call, it should be supplied in the to_date call - it should NOT rely on the NLS_DATE_FORMAT



My experiences tell me relying on defaults, relying on implicit conversions is a horribly bad idea. Be explicit.

to_date default value?

Ravi B, August 07, 2012 - 3:21 pm UTC

Hi Tom,

Could you please let me know why does the following query default to 01-AUG-1998 00:00:00? Why August?

select  to_date( '1998', 'YYYY' ) from dual;


Tom Kyte
August 17, 2012 - 12:41 pm UTC

it only does that this month, next month it will be september 1st and so on.

default year   = current year
default month  = current month
default day    = 1
default hour   = 0
default minute = 0
default second = 0


ops$tkyte%ORA10GR2> select sysdate, to_date( ' ', ' ' ) from dual;

SYSDATE              TO_DATE('','')
-------------------- --------------------
17-aug-2012 13:41:06 01-aug-2012 00:00:00






to_date default value?

Ravi B, August 07, 2012 - 3:36 pm UTC

Tom,

Never mind my earlier question. It would default to 1st of the current month.

Thanks.

Jess, November 28, 2012 - 7:51 am UTC

Hi Tom,

Being on-board with converting the parameter once and comparing it to the real dates, I'm curious about the following scenario.

I've run autotrace for each of the following queries:

select case when my_date = to_date('99991231','YYYYMMDD') then 'A' else 'B' end from my_table;
and
select case when to_char(my_date,'YYYYMMDD') = '99991231' then 'A' else 'B' end from my_table;

But it returns identical output:
7 recursive calls
52 db block gets
59 physical reads
139 redo size
351 bytes sent via SQL*Net to client
352 bytes received via SQL*Net from client
357 sorts (memory)
358 sorts (disk)

I would've expected to see a difference given the difference in where to_date() is. Or does it not matter because it will still do the conversion per row, regardless of whether it's the date or the constant?

Thank you as always.

Tom Kyte
November 30, 2012 - 3:17 am UTC

well, why would any of those change - the queries are basically identical "IO wise" - they read the same number of blocks and so on. They would different in CPU potentially...

ops$tkyte%ORA11GR2> declare
  2      l_date date := to_date('99991231','YYYYMMDD');
  3      l_str  varchar2(30) := '99991231';
  4  begin
  5      for x in (select case when created = l_date then 'A' else 'B' end
  6                  from big_table.big_table)
  7      loop
  8          null;
  9      end loop;
 10  
 11      for x in (select case when to_char(created,'YYYYMMDD') = l_str then 'A' else 'B' end
 12                  from big_table.big_table)
 13      loop
 14          null;
 15      end loop;
 16  end;
 17  /

PL/SQL procedure successfully completed.



SELECT CASE WHEN CREATED = :B1 THEN 'A' ELSE 'B' END FROM BIG_TABLE.BIG_TABLE

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch    10001      1.16       1.14      14529      24377          0     1000000
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total    10003      1.16       1.14      14529      24377          0     1000000
********************************************************************************
SELECT CASE WHEN TO_CHAR(CREATED,'YYYYMMDD') = :B1 THEN 'A' ELSE 'B' END FROM BIG_TABLE.BIG_TABLE

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch    10001      1.38       1.36      14529      24377          0     1000000
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total    10003      1.38       1.36      14529      24377          0     1000000


I would expect those two queries to do the same number of IO's and everything - but potentially use different amounts of CPU...

Jess, November 29, 2012 - 6:34 am UTC

Hi Tom,

I've also run into some code that looks like this:

select ... from ... where my_date between
to_date(to_char(some_date,'YYYYMMDD') || '00:00:00','YYYYMMDD HH24:MI:SS') and
to_date(to_char(some_date,'YYYYMMDD') || '23:59:59','YYYYMMDD HH24:MI:SS')

Is this not an implicit conversion here? I would've thought it'd be better to do
where my_date >= some_date and my_date < some_date +1
(because, clearly, some_date doesn't have a time component), but the predicate information on explain plan seems to be the same in both cases. Am I missing something here?

Thanks as always.


Tom Kyte
November 30, 2012 - 3:48 am UTC

assuming that my_date is a DATE type - then there is no implicit conversion here - rather - this is all *explicit* conversions (from a string to a date) to compare to a date - using a date format.

In other words - this code is perfectly OK.

some_date DOES have a time component???? why do you say it does not?? all dates have a time component. that time component might be midnight - but it is there.


they could have coded:

where my_date >= trunc(some_date,'d') and my_date < trunc(some_date+1,'d')

to be slightly better than

my_date between
to_date(to_char(some_date,'YYYYMMDD') || '00:00:00','YYYYMMDD HH24:MI:SS') and
to_date(to_char(some_date,'YYYYMMDD') || '23:59:59','YYYYMMDD HH24:MI:SS')




Jess, November 30, 2012 - 7:42 am UTC

Thanks Tom.
Didn't realise (for the first question) that it's CPU that would matter between the queries.

For the second one, when I said the dates had no time component, I meant that the data coming in does not provide a logical time (i.e., everything will be midnight in the dataase).

When you say the code could've done
"where my_date >= trunc(some_date,'d') and my_date < trunc(some_date+1,'d')"
would this not have a negative effect on partition pruning because of the truncate in front of the date?




Tom Kyte
November 30, 2012 - 7:49 am UTC

You'd have KEY KEY on the range partitions, assuming my_date is a date that is the partition key.

It would be the same (KEY KEY) regardless, you'd have to use LITERALS to get fixed partitions.

but the trunc would have no effect on partition pruning


manas, January 10, 2013 - 6:21 am UTC

Hi,

There is a certain code that we are using to convert a date from one timezone to another -

<u>Code Snippet 1-</u>
SELECT 
TO_DATE (
                TO_CHAR (
                         FROM_TZ (
                                  CAST (last_upd AS TIMESTAMP)
                                , 'PST'
                                 )
                            AT TIME ZONE ('EST')
                       , 'MM/DD/YYYY HH:MI:SS AM'
                        )
              , 'MM/DD/YYYY HH:MI:SS AM'
               )
          last_upd
  FROM <<table_name>> a
 WHERE <<conditions>>;

Now I tested the performance of the same code as-

<u> Code Snippet 2-</u>
SELECT  CAST(
                         FROM_TZ (
                                  CAST (last_upd AS TIMESTAMP)
                                , 'PST'
                                 )
                            AT TIME ZONE ('EST') AS DATE)
          last_upd
  FROM <<table_name>> a
 WHERE <<conditions>>;

The Code Snippet 2 seems to be working faster as compared to Code Snippet 1 almost always ( I did flush the Buffer_cache as well as Shared_Pool just to ensure that there are no other factors affecting the Query Performance).

Here is the AUtotrace output of the two Queries-
 
When Using CAST -->
Query -
SQL> insert into testp
  2  SELECT  CAST(
  3                           FROM_TZ (
  4                                    CAST (last_upd AS TIMESTAMP)
  5                                  , 'PST'
  6                                   )
  7                              AT TIME ZONE ('EST') AS DATE)
  8            last_upd
          FROM <<table_name>> a
         WHERE <<conditions>>;

2000135 rows created.

Elapsed: 00:01:14.54

Statistics
----------------------------------------------------------
       3187  recursive calls
      58417  db block gets
      15731  consistent gets
         54  physical reads
   38151784  redo size
        717  bytes sent via SQL*Net to client
       1080  bytes received via SQL*Net from client
          4  SQL*Net roundtrips to/from client
         21  sorts (memory)
          0  sorts (disk)
    2000135  rows processed


When Using TO_DATE(TO_CHAR -->
Query-
SQL> insert into testp
  2  SELECT
  3  TO_DATE (
  4                  TO_CHAR (
  5                           FROM_TZ (
  6                                    CAST (last_upd AS TIMESTAMP)
  7                                  , 'PST'
  8                                   )
  9                              AT TIME ZONE ('EST')
 10                         , 'MM/DD/YYYY HH:MI:SS AM'
 11                          )
 12                , 'MM/DD/YYYY HH:MI:SS AM'
 13                 )
 14            last_upd
          FROM <<table_name>> a
         WHERE <<conditions>>;

2000135 rows created.

Elapsed: 00:01:22.25
Statistics
----------------------------------------------------------
       3106  recursive calls
      58448  db block gets
      15560  consistent gets
          2  physical reads
   38156380  redo size
        716  bytes sent via SQL*Net to client
       1235  bytes received via SQL*Net from client
          4  SQL*Net roundtrips to/from client
         11  sorts (memory)
          0  sorts (disk)
    2000135  rows processed

Table is created simply as-
create table testp ( last date);


Database Version -
Oracle Database 11g Enterprise Edition Release 11.2.0.2.0 - Production
PL/SQL Release 11.2.0.2.0 - Production
CORE 11.2.0.2.0 Production
TNS for Linux: Version 11.2.0.2.0 - Production
NLSRTL Version 11.2.0.2.0 - Production


Does this anyway denotes that in such a scenario CAST is performance wise better that "TO_DATE(TO_CHAR" or there is a better way to do this.

Tom Kyte
January 14, 2013 - 1:15 pm UTC

I did flush the Buffer_cache as well as Shared_Pool just to
ensure that there are no other factors affecting the Query Performance


you do know that that gives you even more skewed results than any other approach. the buffer cache is never empty. the shared pool is never empty.


You don't see me flushing anything....



use tkprof to see what time is spent in the database doing things, autotrace isn't good for that - it can show you the amount of IOs and elapsed times - but you are interested in the CPU time here.



but yes, CAST is (obviously!?) going to be better than using a string formatting function with TO_CHAR and then having to reinterpret that string using TO_DATE again. (it seems very intuitive doesn't it? less steps, simple operation, less code)

CAST ....

Manas, January 28, 2013 - 12:38 pm UTC

Thanks Tom!! I'll avoid flushing but while going through your article in Oracle Magazine I have a quick question - Do we have both the Shared pool as well as the Database Buffer cache cached by the OS?

Also we are facing an issue with the functions in SQL statements. When we use FROM_TZ (CAST (my_date AS TIMESTAMP), SOURCE_TIMEZONE)
AT TIME ZONE (TARGET_TIMEZONE) AS DATE)
directly in the Query it works almost twice as fast when called from a function. I understand that there will be a time lag but I was not expecting such a huge difference.

Code for same -

CREATE TABLE my_table_1
(
column_2 NUMBER NOT NULL,
column_1 VARCHAR2(250),
my_date_1 DATE,
my_date_2 DATE,
my_date_3 DATE,
my_date_4 DATE,
my_date_5 DATE,
my_date_6 DATE,
my_date_7 DATE NOT NULL);

ALTER TABLE my_table_1 ADD ( CONSTRAINT PK_MY_TABLE_1 PRIMARY KEY (COLUMN_2) USING INDEX PK_MY_TABLE_1);

CREATE TABLE my_table_2
(
column_2 NUMBER NOT NULL,
column_1 VARCHAR2(250),
my_date_1 DATE,
my_date_2 DATE,
my_date_3 DATE,
my_date_4 DATE,
my_date_5 DATE,
my_date_6 DATE,
my_date_7 DATE NOT NULL);

ALTER TABLE my_table_2 ADD ( CONSTRAINT PK_MY_TABLE_2 PRIMARY KEY (COLUMN_2) USING INDEX PK_MY_TABLE_2);

DECLARE
v_sys_dt DATE := TO_DATE('06-NOV-2012 00:00:00', 'DD-MON-YYYY HH24:MI:SS');
BEGIN

for i in 1..2000135
LOOP
insert into my_table_1
(column_1
,column_2
,my_date_1
,my_date_2
,my_date_3
,my_date_4
,my_date_5
,my_date_6
,my_date_7
)
VALUES
(i
,'LOOP'
,v_sys_dt + (1*i/86400)
,v_sys_dt + (2*i/86400)
,v_sys_dt + (3*i/86400)
,v_sys_dt + (1*i/86400)
,v_sys_dt + (2*i/86400)
,v_sys_dt + (3*i/86400)
,v_sys_dt + (1*i/86400)
);

--DBMS_OUTPUT.PUT_LINE(i);
END LOOP;
COMMIT;

END;
/

Function used:
CREATE OR REPLACE FUNCTION fn_timezone_conversion
(
p_date DATE,
p_source_timezone VARCHAR2,
p_target_timezone VARCHAR2
)
RETURN DATE
AS
v_date DATE := p_date;
v_new_date TIMESTAMP(0);
v_source_timezone VARCHAR2(100):= p_source_timezone;
v_target_timezone VARCHAR2(100):= p_target_timezone;
BEGIN
v_new_date := CAST(FROM_TZ(CAST(v_date AS TIMESTAMP),
v_source_timezone) AT TIME ZONE (v_target_timezone) AS DATE);
RETURN v_new_date;
END;
/

Function case 1:
CREATE OR REPLACE PROCEDURE my_procedure_case1 (
p_my_fetch_limit IN NUMBER DEFAULT 1000)
IS
TYPE column_1 IS TABLE OF my_table_1.column_1%TYPE;
TYPE column_2 IS TABLE OF my_table_1.column_2%TYPE;
TYPE my_date_1 IS TABLE OF my_table_1.my_date_1%TYPE;
TYPE my_date_2 IS TABLE OF my_table_1.my_date_2%TYPE;
TYPE my_date_3 IS TABLE OF my_table_1.my_date_3%TYPE;
TYPE my_date_4 IS TABLE OF my_table_1.my_date_4%TYPE;
TYPE my_date_5 IS TABLE OF my_table_1.my_date_5%TYPE;
TYPE my_date_6 IS TABLE OF my_table_1.my_date_6%TYPE;
TYPE my_date_7 IS TABLE OF my_table_1.my_date_7%TYPE;

v_column_1_array column_1;
v_column_2_array column_2;
v_my_date_1_array my_date_1;
v_my_date_2_array my_date_2;
v_my_date_3_array my_date_3;
v_my_date_4_array my_date_4;
v_my_date_5_array my_date_5;
v_my_date_6_array my_date_6;
v_my_date_7_array my_date_7;

CURSOR c_my_cursor
IS
SELECT column_1,
column_2,
CAST (FROM_TZ (CAST (my_date_1 AS TIMESTAMP), 'PST') AT TIME ZONE ('EST') AS DATE),
CAST (FROM_TZ (CAST (my_date_2 AS TIMESTAMP), 'PST') AT TIME ZONE ('EST') AS DATE),
CAST (FROM_TZ (CAST (my_date_3 AS TIMESTAMP), 'PST') AT TIME ZONE ('EST') AS DATE),
CAST (FROM_TZ (CAST (my_date_4 AS TIMESTAMP), 'PST') AT TIME ZONE ('EST') AS DATE),
CAST (FROM_TZ (CAST (my_date_5 AS TIMESTAMP), 'PST') AT TIME ZONE ('EST') AS DATE),
CAST (FROM_TZ (CAST (my_date_6 AS TIMESTAMP), 'PST') AT TIME ZONE ('EST') AS DATE),
CAST (FROM_TZ (CAST (my_date_7 AS TIMESTAMP), 'PST') AT TIME ZONE ('EST') AS DATE)
FROM my_table_1;
BEGIN
OPEN c_my_cursor;

LOOP
FETCH c_my_cursor
BULK COLLECT INTO v_column_1_array,
v_column_2_array,
v_my_date_1_array,
v_my_date_2_array,
v_my_date_3_array,
v_my_date_4_array,
v_my_date_5_array,
v_my_date_6_array,
v_my_date_7_array
LIMIT p_my_fetch_limit;

FORALL i IN 1 .. v_column_1_array.COUNT
INSERT INTO my_table_2 (column_1,
column_2,
my_date_1,
my_date_2,
my_date_3,
my_date_4,
my_date_5,
my_date_6,
my_date_7)
VALUES (v_column_1_array (i),
v_column_2_array (i),
v_my_date_1_array (i),
v_my_date_2_array (i),
v_my_date_3_array (i),
v_my_date_4_array (i),
v_my_date_5_array (i),
v_my_date_6_array (i),
v_my_date_7_array (i));

COMMIT;
EXIT WHEN c_my_cursor%NOTFOUND;
END LOOP;

COMMIT;

EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.PUT_LINE('SQLERRM: ' || SQLERRM);

END my_procedure_case1;
/

CREATE OR REPLACE PROCEDURE my_procedure (
p_my_fetch_limit IN NUMBER DEFAULT 1000)
IS
TYPE column_1 IS TABLE OF my_table_1.column_1%TYPE;
TYPE column_2 IS TABLE OF my_table_1.column_2%TYPE;
TYPE my_date_1 IS TABLE OF my_table_1.my_date_1%TYPE;
TYPE my_date_2 IS TABLE OF my_table_1.my_date_2%TYPE;
TYPE my_date_3 IS TABLE OF my_table_1.my_date_3%TYPE;
TYPE my_date_4 IS TABLE OF my_table_1.my_date_4%TYPE;
TYPE my_date_5 IS TABLE OF my_table_1.my_date_5%TYPE;
TYPE my_date_6 IS TABLE OF my_table_1.my_date_6%TYPE;
TYPE my_date_7 IS TABLE OF my_table_1.my_date_7%TYPE;

v_column_1_array column_1;
v_column_2_array column_2;
v_my_date_1_array my_date_1;
v_my_date_2_array my_date_2;
v_my_date_3_array my_date_3;
v_my_date_4_array my_date_4;
v_my_date_5_array my_date_5;
v_my_date_6_array my_date_6;
v_my_date_7_array my_date_7;

CURSOR c_my_cursor
IS
SELECT column_1,
column_2,
fn_timezone_conversion (my_date_1),
fn_timezone_conversion (my_date_2),
fn_timezone_conversion (my_date_3),
fn_timezone_conversion (my_date_4),
fn_timezone_conversion (my_date_5),
fn_timezone_conversion (my_date_6),
fn_timezone_conversion (my_date_7)
FROM my_table_1
WHERE column_1 = 'DUMMY';
BEGIN
OPEN c_my_cursor;

LOOP
FETCH c_my_cursor
BULK COLLECT INTO v_column_1_array,
v_column_2_array,
v_my_date_1_array,
v_my_date_2_array,
v_my_date_3_array,
v_my_date_4_array,
v_my_date_5_array,
v_my_date_6_array,
v_my_date_7_array
LIMIT p_my_fetch_limit;

FORALL i IN 1 .. v_column_1_array.COUNT
INSERT INTO my_table_2 (column_1,
column_2,
my_date_1,
my_date_2,
my_date_3,
my_date_4,
my_date_5,
my_date_6,
my_date_7)
VALUES (v_column_1_array (i),
v_column_2_array (i),
v_my_date_1_array (i),
v_my_date_2_array (i),
v_my_date_3_array (i),
v_my_date_4_array (i),
v_my_date_5_array (i),
v_my_date_6_array (i),
v_my_date_7_array (i));

COMMIT;
EXIT WHEN c_my_cursor%NOTFOUND;
END LOOP;

COMMIT;
END my_procedure;
/

exec my_procedure;
exec my_procedure_case1;

Please can you help me understand why it is so above.
Tom Kyte
January 31, 2013 - 9:04 am UTC

... Do we have both the Shared pool as well as
the Database Buffer cache cached by the OS? ...


neither is cached by the OS? I don't know what you mean. The OS file system cache (assuming cooked file system) will cache data read from disk much like the buffer cache does - but it isn't caching the buffer cache, it is caching physical reads.

the shared pool is just a memory structure, the OS in no way would cache it.


Also we are facing an issue with the functions in SQL statements. When we use
FROM_TZ (CAST (my_date AS TIMESTAMP), SOURCE_TIMEZONE)
AT TIME ZONE (TARGET_TIMEZONE) AS DATE)
directly in the Query it works almost twice as fast when called from a
function. I understand that there will be a time lag but I was not expecting
such a huge difference.


why not?

think about how much code you would go through if you had to write a library that could support date/timezones/times - from 4713bc to 9999ad - in the gregorian, arabic, julian, etc calendars, with leap years, knowledge of the missing days in October 1582

ops$tkyte%ORA11GR2> select to_date( '&1', 'mon-yyyy')+level-1
  2    from dual
  3  connect by level <= 15;

TO_DATE('OCT-1582','
--------------------
01-oct-1582 00:00:00
02-oct-1582 00:00:00
03-oct-1582 00:00:00
04-oct-1582 00:00:00
15-oct-1582 00:00:00
16-oct-1582 00:00:00
17-oct-1582 00:00:00
18-oct-1582 00:00:00
19-oct-1582 00:00:00
20-oct-1582 00:00:00
21-oct-1582 00:00:00
22-oct-1582 00:00:00
23-oct-1582 00:00:00
24-oct-1582 00:00:00
25-oct-1582 00:00:00

15 rows selected.


and so on- the date stuff is horrifically complex if you want to do it right. that is a lot of code. only use it when you actually need it is the rule.





let me turn this around on you. If someone presents you with an approach they say "does a lot less work - a lot less work" - why do you think a very nice speedup wouldn't be the result?


also, if you want your code to be *really* efficient, you'll delete it all.

replace it with

insert /*+ append */ into my_table_2 (...)
SELECT column_1,
column_2,
CAST (FROM_TZ (CAST (my_date_1 AS TIMESTAMP), 'PST') AT TIME ZONE ('EST') AS
DATE),
CAST (FROM_TZ (CAST (my_date_2 AS TIMESTAMP), 'PST') AT TIME ZONE ('EST') AS
DATE),
CAST (FROM_TZ (CAST (my_date_3 AS TIMESTAMP), 'PST') AT TIME ZONE ('EST') AS
DATE),
CAST (FROM_TZ (CAST (my_date_4 AS TIMESTAMP), 'PST') AT TIME ZONE ('EST') AS
DATE),
CAST (FROM_TZ (CAST (my_date_5 AS TIMESTAMP), 'PST') AT TIME ZONE ('EST') AS
DATE),
CAST (FROM_TZ (CAST (my_date_6 AS TIMESTAMP), 'PST') AT TIME ZONE ('EST') AS
DATE),
CAST (FROM_TZ (CAST (my_date_7 AS TIMESTAMP), 'PST') AT TIME ZONE ('EST') AS
DATE)
FROM my_table_1;


what is your "logic" (logic in quotes because it is anti-logical to do what you are doing) behind doing this procedurally???????

Bulk Collect ....

manas, February 04, 2013 - 12:20 pm UTC

Thanks Tom!! I now understand - "the shared pool is just a memory structure, the OS in no way would cache it."

Regarding the second part -
1)We cannot use a direct Insert (actually DML error logging) because our architects believe that in certain environments rollback segment may not be large enough to accommodate all the data (we have a migration project and the code may run on Servers which may be inaccessible to us).
2)The code mentioned above is temporary - just to replicate the issue. The actual code has a certain amount of logic in between the fetch and the Inserts. While tuning - we found out that if we remove only the function call with the actual statement (CAST...) the time taken by the procedure is almost 25% (reduced by 75% ) of the original time. The existing logic is as follows -

DECLARE
Cursor to fetch rows ...
BEGIN
<<Initialize>>
LOOP
Bulk Fetch ……
Insert Into Main Table <<forall>>
Insert into Log table<<forall>>
If rowcount = 100K; Commit;
End loop;
End;


3)We have almost 7 date columns (I’m excluding the other columns) which we fetch through a DB Link; we are migrating the columns from one timezone to another using the function.
4)We are doing a commit every 500 records as of now. I plan to change that to 100K. I assume the consistency (in case the procedure fails mid-way and we have to rerun) is maintained using the log table. We simultaneously insert into a log table as we need the Primary keys of the existing & migrated records and in case of a failure, during rerun we check whether the record exists in the log table before trying to insert the record into the final table.
5)We are using a function and not the direct CAST(..) to avoid redundancies as the same conversion is used in almost all subprograms.

Here are the few problems I am facing–
a) Since we are fetching across DBLinks, the Optimizer is not able to guess the no. of rows correctly. At places the number of rows need to be 2Million and the Optimizer assumes them to be 5K. Is it due to the DB Link or I'm missing something?
b)I am also facing issues with Scalar Subquery Unnesting & Nesting. Even after all the tables being analyzed (using DBMS_STATS) there were cases wherein I had to add a Hint (e.g no_unnest) and the Query time was reduced from 10 mins to 45 secs; Or we had to use an Inline view so that a Scalar Subquery could be joined to the main tables.
c)I tried Append_Values hint but I suppose it does not works with Exception handling (Probably for the same reason Append does not works with constraints)

Tom Kyte
February 06, 2013 - 7:47 am UTC

what happens when you hit an ora-1555 on your select, or your insert runs out of space. what is your restart process. You do understand your code is missing LOTS of code to make it actually work, to be restartable?????


and you do know that insert /+* append */ into t select .... will generate NO undo.



I assume the consistency

please don't assume, verify, test it - kill the process in testing 50% of the way through and make sure it all works.


5) use the direct cast, inline this stuff in straight sql, the overhead of the function will kill you if you call it millions of times. This is a one time conversion program right - just repeat it.


a) could be - feel free to use the driving_site hint for queries that query all remote data.

b) i don't understand what you mean by "had to use an inline view so that a scalar subquery could be joined to the main table"




I would

a) start with empty tables, no constraints
b) copy the data over using direct path
c) add constraints, using exceptions into to find "bad rows"
d) cleanse data
e) add constraints that failed in c)


no undo, no redo necessary, big bulk operations, no procedural code.

Tuning FROM_TZ function for Timezone conversion

Ankit, April 25, 2013 - 4:37 pm UTC

Hi Tom,

In continuation to the discussion of converting timezone using the FROM_TZ function, the time taken by the SQL with and without the timezone conversion for 87 Million rows is of the order of 38 minutes and 6 minutes respectively.

Since timezone conversion is a must for us, can you suggest if there is a workaround to bring down the processing time with timezone conversion.

Thanks,
Ankit
Tom Kyte
April 25, 2013 - 7:53 pm UTC

show us what you are doing, explain what you are doing. sure, there might be a shortcut based on your data, on your needs...

Alternates to FROM_TZ function for Timezone Conversion

Ankit, April 26, 2013 - 7:32 pm UTC

Thanks Tom for the prompt response.

We have a requirement to migrate billions of rows from tables RANGE partitioned on a date column and HASH subpartitioned on an ID column.
The source table has multiple(upto 8) date columns which need to be converted into a timezone different from source table timezone.
The source and target timezones can differ from table to table (but would remain the same for a table).

Each subpartition of the source table is expected to have approximately 200 Million rows.
Our current design (still under discussion) is as follows:
1. Disable constraints on the target table
2. Extract the Subpartition information of the source table and store it in a table
OR
2. Create chunks of the data from source table on basis of ROWID using DBMS_EXECUTE_PARALLEL
3. Loop through each subpartition / chunk and migrate data for each subpartition / chunk in source using "INSERT AS SELECT" with PARALLEL hint in SELECT statement.
4. Enable the constraints and resolve the discrepancies

In the test environment, we have ~87 Million rows in the source table with 108 Subpartitions.
In Step# 3, using Subpartition Approach, following are the Elapsed time results:
A> When timezone conversion is carried out on 5 columns in source table using: "CAST(FROM_TZ (CAST (date_column_1 AS TIMESTAMP), 'GMT') AT TIME ZONE ('EST') AS TIMESTAMP(0))", time taken by the process is: 38 minutes
B> When no timezone conversion is carried out, then the time taken by the process is: 6 minutes.

Each of the date columns can have different values. The FROM_TZ function that is getting executed 5 times for each row(in test table) seems to pose a huge overhead in our case.
Unfortunately, timezone conversion is a must meet requirement.
Tom Kyte
April 30, 2013 - 1:57 pm UTC

since you are simply going from GMT to EST, could you not just subtract a fixed number of hours - doing a simple check for daylight savings if necessary - so for each of the five columns, it becomes a simple bit of math? instead of a big NLS driven function. since each row in a given column is going to have the same amount added/taken away from it - you could just put a constant there perhaps?

Alternates to FROM_TZ function for Timezone Conversion

Ankit, April 30, 2013 - 6:59 pm UTC

Data in each of the date columns in a row can have different values. This would mean that I would not be able to subtract/add a fixed number of hours to each date column of a row.
Also, as mentioned earlier, I have to do the timezone conversion for multiple tables, and source and target timezones can differ from table to table.
Tom Kyte
May 06, 2013 - 1:20 pm UTC

so each row has a timtestamp associated with it and each timestamp can be different... the example you showed had literals - in real life there are no literals, it is a dynamic field, another column in that row actually has the timezone right?

is that what you are saying?

Alternates to FROM_TZ function for Timezone Conversion

Ankit, May 07, 2013 - 10:12 am UTC

Right ... each row has few timestamp fields and each timestamp field within a row can have different values which means that the values in same row can fall within different Day Light Saving date range.

why does this work?

ojock, July 02, 2019 - 3:50 pm UTC

select cast('TIMESTAMP'' 2019-06-29 00:00:00''' as date) from dual;


Why / How does that work and successfully converts a VARCHAR2 to a DATE ?

Just curious, what format mask is being used to convert that string to a date?

For info, that string literal comes from HIGH_VALUE on USER_TAB_PARTITIONS which is being processed inside PL/SQL, so the LONG column is returned as a VARCHAR2. And it's for a table which is INTERVAL partitioned by day.
Connor McDonald
July 03, 2019 - 1:30 am UTC

When we remove the embedding we see:

select cast(TIMESTAMP ' 2019-06-29 00:00:00' as date) from dual;


The TIMESTAMP function is an ANSI standard way of nominating a timestamp. You can also do:

DATE '2020-01-01'

for dates. And we allow CAST from one to the other. THere are no VARCHAR2's involved here.

why does this work?

ojock, July 03, 2019 - 7:18 am UTC

Thanks Connor.

The snippet of code came from code for converting system generated names for an INTERVAL partitioned table;

    for c_rec in (select partition_name, high_value
                  from   user_tab_partitions
                  where  table_name = 'TEST'
                  and    partition_name like 'SYS%'
                 )
    loop
      execute immediate 'select cast(:hv as date) from dual' into l_highvalue using c_rec.high_value;
      execute immediate 'alter table '||'TEST'||' rename partition '||c_rec.partition_name||' to '||'P_'||to_char(l_highvalue,'YYYYMMDD');                            
    end loop;


I was surprised when the "cast(:hv as date)" worked as I thought inside PL/SQL, the LONG column (HIGH_VALUE) was being treated as a VARCHAR2.

I used it instead of below to avoid hard parsing, and I was thinking if I was performing some dodgy conversion;

execute immediate 'select '||c_rec.highvalue||' from dual' into l_highvalue; 


Connor McDonald
July 05, 2019 - 5:48 am UTC

You just need to take a little care there...

SQL> create table t ( c long );

Table created.

SQL> insert into t values ('date ''2019-12-12''');

1 row created.

SQL> select * from t;

C
--------------------------------------------------------------------------------
date '2019-12-12'

1 row selected.

SQL>
SQL> set serveroutput on
SQL> declare
  2    v long;
  3    d date;
  4  begin
  5    select c into v from t;
  6    execute immediate 'select cast(:hv as date) from dual' into d using  v;
  7    dbms_output.put_line(d);
  8  end;
  9  /
12-DEC-19

PL/SQL procedure successfully completed.

SQL>
SQL> delete t;

1 row deleted.

SQL> insert into t values ('to_date(''12-NOV-2019'',''dd-mon-yyyy'')');

1 row created.

SQL> select * from t;

C
--------------------------------------------------------------------------------
to_date('12-NOV-2019','dd-mon-yyyy')

1 row selected.

SQL>
SQL> declare
  2    v long;
  3    d date;
  4  begin
  5    select c into v from t;
  6    execute immediate 'select cast(:hv as date) from dual' into d using  v;
  7    dbms_output.put_line(d);
  8  end;
  9  /
declare
*
ERROR at line 1:
ORA-01858: a non-numeric character was found where a numeric was expected
ORA-06512: at line 6


SQL>
SQL>


Continuing

Laverde Williams, November 10, 2019 - 5:47 pm UTC

Hi Guys,
I’ve a column of timestamp type for unkown reason also has a FBI trunc(col_timestamp,’mi’). Then via a plsql code we are using a date var and comparing agains the column but no index gets used.
The query is something like:
select count(1) from tab where trunc(col_timestamp,’mi’) = trunc(:var_date,’mi’);
The problem is the query always does a full scan on That big table.
Connor McDonald
November 11, 2019 - 1:31 am UTC

I can't reproduce that in the "general" sense.

SQL> create table t as select * from v$active_session_history;

Table created.

SQL> create index IX on T ( trunc(sample_time,'MI'));

Index created.

SQL> explain plan for select count(1) from t where trunc(sample_time,'mi') = trunc(:var_date,'mi');

Explained.

SQL> @exp

--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      |     1 |     9 |     1   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE   |      |     1 |     9 |            |          |
|*  2 |   INDEX RANGE SCAN| IX   |    52 |   468 |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access(TRUNC(INTERNAL_FUNCTION("SAMPLE_TIME"),'fmmi')=TRUNC(TO_NU
              MBER(:VAR_DATE),TO_NUMBER('mi')))

15 rows selected.




So can we have the DDL for the table and index ?