SQL> SELECT COUNT(*)
FROM bigtab
WHERE TRUNC(last_ddl_time) = to_date('20170908','YYYYMMDD');
COUNT(*)
----------
1772
Elapsed: 00:00:07.23
Execution Plan
----------------------------------------------------------
Plan hash value: 2140185107
-----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 8 | 427 (1) | 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | 8 | | |
|* 2 | TABLE ACCESS FULL| BIGTAB | 908 | 7264 | 427 (1) | 00:00:01 |
-----------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter(TRUNC(INTERNAL_FUNCTION("LAST_DDL_TIME"))=TO_DATE('
2017-09-08 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))
At this point you are wondering what's wrong with that query. Below I have another one that looks somewhat more complicated and longer and uses " between conditions". It does the same thing as the query above and returns the same results. But it does it 35x faster.
SQL> SELECT COUNT(*)
FROM bigtab
WHERE last_ddl_time BETWEEN to_date('20170908','YYYYMMDD')
AND to_date('20170908-23:59:59','YYYYMMDD-HH24:MI:SS');
COUNT(*)
----------
1772
Elapsed: 00:00:00.21
Execution Plan
----------------------------------------------------------
Plan hash value: 397841597
--------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 8 | 7 (0) | 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | 8 | | |
|* 2 | INDEX RANGE SCAN | BIGTAB_IDX | 228 | 1824 | 7 (0) | 00:00:01 |
--------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("LAST_DDL_TIME">=TO_DATE(' 2017-09-08 00:00:00',
'syyyy-mm-dd hh24:mi:ss') AND "LAST_DDL_TIME"<=TO_DATE(' 2017-09-08
23:59:59', 'syyyy-mm-dd hh24:mi:ss'))
The explanation for the difference here is that Oracle cannot use a regular index if you wrap the indexed column inside of a function. One may argue that a function based index on trunc(last_ddl_time) would solve this but replacing the index would also mean that all queries would now have to use the trunc() function or do without an index. As a bonus you also have the chance for partition pruning if you are not using a function. If you are lucky enough to be running on Exadata the same is true for smart scans and storage indexes.
SQL> SELECT COUNT(*)
FROM bigtab
WHERE created BETWEEN to_date('20170908','YYYYMMDD')
AND to_date('20170908-23:59:59','YYYYMMDD-HH24:MI:SS');
COUNT(*)
----------
408
Elapsed: 00:00:05.13
In this case, the lazier syntax with trunc() took more than twice as long to run even though both queries are performing a (fully cached) full table scan.
SQL> SELECT COUNT(*)
FROM bigtab
WHERE TRUNC(created) = to_date('20110908','YYYYMMDD');
COUNT(*)
----------
408
Elapsed: 00:00:12.32
The reason for the difference in execution time here is that with the trunc() syntax Oracle still has to apply the function to every row it scans, taking up extra context switches and CPU cycles even though the amount of logical IOs is the same.
I hope I made the point clearly enough to not use functions like trunc() in the where-clause of your queries. The "between" syntax is more verbose and requires a little more typing but you get rewarded with better performance because Oracle may still use regular indexes, partition pruning, smart scans and at the very least avoid context switching to evaluate a function.
Ready to optimize your Oracle Database for the future?