6

I have a table with a DATE column with time (as usual in Oracle since there isn't a TIME type). When I query that column from JDBC, I have two options:

  • Manually convert the values with Oracle's to_date()
  • Use a java.sql.Timestamp

Both approaches work and have exclusive areas of hideousness. My problem is when I'm SELECTing data. Here are two sample queries:

select *
from TABLE
where TS between {ts '2009-12-08 00:00:00.000'} and {ts '2009-12-09 00:00:00.000'}

select *
from TABLE
where TS between trunc({ts '2009-12-08 00:00:00.000'}) and trunc({ts '2009-12-09 00:00:00.000'})

Both queries work, return the same results and produce the exact same output in EXPLAIN PLAN. This right indexes are used.

Only query one runs 15 minutes while the second query takes 0.031s. Why is that? Is there a central place to fix this or do I have to check all my queries for this column and make utterly sure that the trunc() is in there? How do I fix this issue when I need to select down to a certain second?

[EDIT] The table is partitioned and I'm on Oracle 10.2.0.

Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
  • 3
    Is your table partitioned? Oracle JDBC doesn't seem to use partition pruning when you set parameters as Timestamp, for reasons I've never understood. – skaffman Dec 22 '09 at 10:54
  • 1
    +1 I would like to understand this also. Does this happen only on a huge table? – KLE Dec 22 '09 at 10:59
  • Well, I guess you won't notice with a small table :) – Aaron Digulla Dec 22 '09 at 11:00
  • As you noted, using the {ts ''} syntax makes your code database-agnostic, but is there a way to find out what SQL is really being passed to the database? And how important is it to be database agnostic? If you could post the EXPLAIN PLAN results we might be able to understand more of what's going on. – Bob Jarvis - Слава Україні Dec 22 '09 at 14:10
  • Bob: Explain plan shows the exact same results, even when I use TO_DATE(). – Aaron Digulla Dec 22 '09 at 16:08

4 Answers4

5

This is because TIMESTAMP datatype is more accurate than DATE so when you supply TIMESTAMP parameter value into DATE column condition, Oracle has to convert all DATE values into TIMESTAMP to make a comparison (this is the INTERNAL_FUNCTION usage above) and therefore index has to be full scanned.

FAB
  • 51
  • 1
  • 1
  • Makes sense; the optimizer is probably too "dumb" to understand that downcasting the type would be more efficient in this case. – Aaron Digulla Aug 22 '11 at 12:12
3

I don't understand what {ts '2009-12-08 00:00:00.000'} actually mean, since this isn't Oracle SQL as far as I know. Can you show exactly what the query is you're running?

One possible problem is that you're specifying your range with milliseconds. Oracle's DATE type only goes down to seconds. (Use TIMESTAMP type if you need to store fractions of seconds). But what might be happening is that in the first query, Oracle is converting each DATE value to a TIMESTAMP in order to do the comparison to your specified TIMESTAMP. In the second case, it knows TRUNC() will effectively round your value to something that can be expressed as a DATE, so no conversion is needed.

If you want to avoid such implicit conversions, make sure you're always comparing like with like. eg

select * 
from my_table t
where t.ts between to_date('2009-12-08','YYYY-MM-DD') and to_date('2009-12-09','YYYY-MM-DD')
CMG
  • 361
  • 3
  • 4
  • +1 The syntax of this query seem simpler, more understandable. We use stuff like that, and have no problem. – KLE Dec 22 '09 at 11:10
  • 4
    `{ts ...}`, `{d ...}` is a hidden feature of JDBC which allows you to specify a java.sql.Timestamp, java.sql.Date in a DB agnositic way. – Aaron Digulla Dec 22 '09 at 11:11
  • As for your query, my question is more: How can I avoid this without going through all the sources? – Aaron Digulla Dec 22 '09 at 11:13
  • Hidden feature ... I start to understand ;-) So you don't really know what is the exact SQL received by Oracle! ;-) To find out, maybe you can reproduce the problem directly in Oracle by comparing the performance of several queries, varying only the date format used (in this answer)? But don't forget to keep us updated! :-) – KLE Dec 22 '09 at 11:17
  • 2
    I'm pretty sure the problem is one of implicit type conversion. If you give Oracle a timestamp value to compare against a DATE type column, then it will have to do a conversion, and it will avoid rounding. So the only option is to convert all its DATE values to TIMESTAMP. Hence it will be extremely slow and won't be able to use an index on the DATE col. You might be able to get round this by creating a FUNCTION BASED INDEX on the date col, where the date is expressed as a timestamp. – CMG Dec 22 '09 at 11:19
  • @Aaron What do you mean by "without going through all the sources"? Do you have hundreds of these queries in your source code, that you would like to fix changing a simple configuration parameter? :-) – KLE Dec 22 '09 at 11:26
  • @KLE: I can fix the 10+ places where its broken. I'm wary that someone might break the performance again by creating a new query somewhere. – Aaron Digulla Dec 22 '09 at 11:39
  • @KLE: The JDBC driver sends a java.sql.Timestamp to the DB. But I'm pretty sure that CMG is right: It will cause a widening conversion on the existing column. – Aaron Digulla Dec 22 '09 at 11:40
  • @CMG: I really want to avoid TO_DATE(). What is the correct JDBC type to use for Oracle DATE columns? java.sql.Date will truncate the seconds, java.sql.Time will truncate the date part. There is no sql.DateTime. Do I have to use oracle.sql.DATE? – Aaron Digulla Dec 22 '09 at 11:43
  • In the example you gave, you don't seem to be interested in the time-element anyway (just the date part), so have you tried using the {d ...} shorthand to pass in just the date instead of a timestamp {ts ...} ? – CMG Dec 22 '09 at 13:38
  • Or if you are interested in the time element, maybe try using an expression which adds {d ...} and {t ...} together eg select * from my_table t where t.ts between ({d '2009-12-08'} + {t '12:01:02'}) and ({ d '2009-12-09'} + {t '12:01:02'} ) No idea if this will work or how it will perform, but worth a try – CMG Dec 22 '09 at 13:46
  • @CMG: Doesn't work. I wonder how other people use JDBC with Oracle. Is everyone using Strings and TO_DATE()??? – Aaron Digulla Dec 22 '09 at 16:09
  • Where I work we don't use those magic jdbc {} expressions, but strings and to_date(). We like to know what SQL is being passed to Oracle rather than let something between Java and Oracle invent the SQL on the way through. – CMG Dec 22 '09 at 17:05
3

I have a similar problem here:

Non-negligible execution plan difference with Oracle when using jdbc Timestamp or Date

In my example it essentially comes down to the fact that when using JDBC Timestamp, an INTERNAL_FUNCTION is applied to the filter column, not the bind variable. Thus, the index cannot be used for RANGE SCANS or UNIQUE SCANS anymore:

// execute_at is of type DATE.
PreparedStatement stmt = connection.prepareStatement(
    "SELECT /*+ index(my_table my_index) */ * " + 
    "FROM my_table " +
    "WHERE execute_at > ? AND execute_at < ?");

These two bindings result in entirely different behaviour (to exclude bind variable peeking issues, I actually enforced two hard-parses):

// 1. with timestamps
stmt.setTimestamp(1, start);
stmt.setTimestamp(2, end);

// 2. with dates
stmt.setDate(1, start);
stmt.setDate(2, end);

1) With timestamps, I get an INDEX FULL SCAN and thus a filter predicate

--------------------------------------------------------------
| Id  | Operation                    | Name                  |
--------------------------------------------------------------
|   0 | SELECT STATEMENT             |                       |
|*  1 |  FILTER                      |                       |
|   2 |   TABLE ACCESS BY INDEX ROWID| my_table              |
|*  3 |    INDEX FULL SCAN           | my_index              |
--------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter(:1<:2)"
   3 - filter((INTERNAL_FUNCTION(""EXECUTE_AT"")>:1 AND 
               INTERNAL_FUNCTION(""EXECUTE_AT"")<:2))

2) With dates, I get the much better INDEX RANGE SCAN and an access predicate

--------------------------------------------------------------
| Id  | Operation                    | Name                  |
--------------------------------------------------------------
|   0 | SELECT STATEMENT             |                       |
|*  1 |  FILTER                      |                       |
|   2 |   TABLE ACCESS BY INDEX ROWID| my_table              |
|*  3 |    INDEX RANGE SCAN          | my_index              |
--------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter(:1<:2)"
   3 - access(""EXECUTE_AT"">:1 AND ""EXECUTE_AT""<:2)

Solving this problem inside third-party APIs

For the record, this problem can also be solved within third-party APIs for instance in Hibernate:

Or in jOOQ:

Community
  • 1
  • 1
Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509
2

I had this problem on a project a while ago and setting the connection property oracle.jdbc.V8Compatible=true fixed the problem.

Dougman's link tells you how to set it:

You set the connection property by adding it to the java.util.Properties object passed to DriverManager.getConnection or to OracleDataSource.setConnectionProperties. You set the system property by including a -D option in your java command line.

java -Doracle.jdbc.V8Compatible="true" MyApp

Note for 11g and this property is apparently not used.

From http://forums.oracle.com/forums/thread.jspa?messageID=1659839 :

One additional note for those who are using the 11gR1 (and on) JDBC thin driver: the V8Compatible connection property no longer exist, so you can't rely on that to send your java.sql.Timestamp as a SQLDATE. What you can do however is call:

setObject(i, aTimestamp, java.sql.Types.DATE) sends data as SQLDATE
setObject(i, aDate) sends data as SQLDATE
setDate(i, aDate) sends data as SQLDATE
setDATE(i, aDATE) (non standard) sends data as SQLDATE

setObject(i, aTimestamp) sends data as SQLTIMESTAMP
setTimestamp(i, aTimestamp) sends data as SQLTIMESTAMP
setObject(i, aTimestamp) sends data as SQLTIMESTAMP
setTIMESTAMP(i, aTIMESTAMP) (non standard) sends data as SQLTIMESTAMP
Angus
  • 121
  • 2