Wrong Results when SQL offloaded with Smart Scan - case study
- Radoslaw Kut
- Exadata , Smart Scan , 12c
- 15 Dec, 2024
Running a database on an Exadata Database Machine can occasionally reveal unexpected challenges, such as an offloaded SQL query returning incorrect results. This kind of behavior, as you can imagine, can have severe consequences — especially when the query is part of a critical reporting system or serves as the single source of truth for your organization.
The hard part at first place is to get aware that the result is innacurate, but that is another part of the story, not covered here.
In the case I’m about to describe, the issue was brought to us by the developers, who were confident that a specific query was returning incorrect results in the production database. The problem surfaced when querying a particular table in the application schema:
SQL> desc F.CUSTOM_LOG;
Name Null? Type
------------------------------------- -------- -----------------
ID_OP NUMBER
TABLES VARCHAR2(30)
COL VARCHAR2(30)
OLD_VAL CLOB
NEW_VAL CLOB
OP CHAR(1)
IDENTITFIER CLOB
USER VARCHAR2(30)
TIME TIMESTAMP(6)
with:
SQL> select count(*) from F.CUSTOM_LOG where TABLES = 'zobo' and IDENTITFIER like 'ID=899691%' and COL = 'CANCEL_DATE';
COUNT(*)
----------
21312
but when compering above result with simillar SQL, result is different:
SQL> select count(*) from F.CUSTOM_LOG where TABLES = 'zobo' and to_char(IDENTITFIER) like 'ID=899691%' and COL = 'CANCEL_DATE';
COUNT(*)
----------
3
Interestingly, the query results on the TEST database were consistent and correct in both cases, returning the expected value of 3.
Both the PROD and TEST databases run on the same Exadata X10M platform, with identical database versions and patch levels. Despite this, the TEST environment consistently provided correct results, while PROD returned incorrect ones. It’s worth noting that both databases are running on the legacy 12.2 release, and PROD was recently upgraded from 12.1 to 12.2 following successful testing in the TEST environment.
To begin the investigation, my first step was to compare the Execution Plans for the queries on PROD and TEST. I aimed to analyze the optimizer’s behavior, the access paths being used, and identify any differences that might exist.
PROD:
SQL> select count(*) from F.CUSTOM_LOG where TABLES = 'zobo' and IDENTITFIER like 'ID=899691%' and COL = 'CANCEL_DATE';
COUNT(*)
----------
21312
Execution Plan
----------------------------------------------------------
Plan hash value: 3967806800
---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 212 | 142K (11)| 00:00:06 |
| 1 | SORT AGGREGATE | | 1 | 212 | | |
|* 2 | TABLE ACCESS STORAGE FULL | CUSTOM_LOG | 3478 | 720K| 142K (11)| 00:00:06 |
---------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - storage("COL"='CANCEL_DATE' AND "TABLES"='zobo' AND "IDENTITFIER" LIKE 'ID=899691%')
filter("COL"='CANCEL_DATE' AND "TABLES"='zobo' AND "IDENTITFIER" LIKE 'ID=899691%')
TEST:
SQL> select count(*) from F_TEST.CUSTOM_LOG where TABLES = 'zobo' and IDENTITFIER like 'ID=899691%' and COL = 'CANCEL_DATE';
COUNT(*)
----------
3
Execution Plan
----------------------------------------------------------
Plan hash value: 3967806800
---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 241 | 337K (11)| 00:00:14 |
| 1 | SORT AGGREGATE | | 1 | 241 | | |
|* 2 | TABLE ACCESS STORAGE FULL | CUSTOM_LOG | 4289 | 1009K| 337K (11)| 00:00:14 |
---------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - storage("COL"='CANCEL_DATE' AND "TABLES"='zobo' AND "IDENTITFIER" LIKE 'ID=899691%')
filter("COL"='CANCEL_DATE' AND "TABLES"='zobo' AND "IDENTITFIER" LIKE 'ID=899691%')
The Execution Plans on both PROD and TEST appeared nearly identical. Key indicators for offloading the query to the storage layer via Smart Scan were present, such as TABLE ACCESS STORAGE FULL and the filtering offload in Predicate Information:
storage("COL"='CANCEL_DATE' AND "TABLES"='zobo' AND "IDENTITFIER" LIKE 'ID=899691%')
.
A quick check of session statistics confirmed that the Offload indeed took place. So why offload on PROD missbehaved ?
Digging deeper into the issue, I came across this MOS note: “Exadata: How to Diagnose Smart Scan and Wrong Results” (Doc ID 1260804.1).
This document provides a test script designed to troubleshoot wrong results scenarios with Smart Scan. The script executes the problematic SQL multiple times, each iteration toggling different Smart Scan mechanisms on and off. This approach can helps narrow down the scope of potential issues within the storage software “smart” layers.
In this case I was able to confirm that when predicate filtering was not performed on storage layer: _kcfis_cell_passthru_enabled=TRUE
( kcfis = kernel cache file management intelligent storage_, _cell_passthru_enabled will not use Filter Processing Library on cell ) or filtering has been emulated within database by setting _rdbms_internal_fplib_enabled=TRUE
results are ok:
'alter session set "_kcfis_cell_passthru_enabled"=TRUE;'
' : see if this is fplib-related'
Session altered.
COUNT(*)
----------
3
1 row selected.
Elapsed: 00:01:20.46
'alter session set "_serial_direct_read"=TRUE'
'enable serial direct read for fplib test'
'alter session set "_rdbms_internal_fplib_enabled"=TRUE;'
' : see if this happens with fplib emulation'
Session altered.
COUNT(*)
----------
3
1 row selected.
Elapsed: 00:00:27.71
By modifying the query to apply the to_char()
function on the IDENTITFIER
column, the DEV team achieved the same effect as the test script mentioned earlier — preventing the offloading of predicate filtering to the storage layer:
SQL> select count(*) from F.CUSTOM_LOG where TABLES = 'zobo' and to_char(IDENTITFIER) like 'ID=899691%' and COL = 'CANCEL_DATE';
COUNT(*)
----------
3
Execution Plan
----------------------------------------------------------
Plan hash value: 3967806800
---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 212 | 142K (11)| 00:00:06 |
| 1 | SORT AGGREGATE | | 1 | 212 | | |
|* 2 | TABLE ACCESS STORAGE FULL | CUSTOM_LOG | 3478 | 720K| 142K (11)| 00:00:06 |
---------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - storage("COL"='CANCEL_DATE' AND "TABLES"='zobo')
filter("COL"='CANCEL_DATE' AND "TABLES"='zobo' AND "IDENTITFIER" LIKE 'ID=899691%')
Execution Plan confirms that - Predicate Information section does not contain filter on IDENTIFIER
in storage("COL"='CANCEL_DATE' AND "TABLES"='zobo')
.
So, why does predicate filtering for IDENTIFIER
when offloaded to storage on PROD database generates wrong result but not on TEST, and why is the to_char()
not offloaded at first place, as according to Exadata Storage Software documentation:
“In Oracle Exadata Storage Server 12.2.1.1.0, offload support has been extended for the following LOB operators: LENGTH, SUBSTR, INSTRM CONCAT, LPAD, RPAD, LTRIM, RTRIM, LOWER, UPPER, NLS_LOWER, NLS_UPPER, NVL, REPLACE, REGEXP_INSTR, TO_CHAR.
Exadata smart scan offload evaluation is supported only on uncompressed inlined LOBs (less than 4 KB in size).”
Compression can be the key to understand the issue (as LOBs are IN_ROW stored). Let’s verify whether the object is compressed and, if so, identify the type of compression used. Checking both databases revealed the same outcome::
SQL> select table_name, compress_for from dba_tables where table_name='CUSTOM_LOG';
TABLE_NAME COMPRESS_FOR
------------ ----------------
CUSTOM_LOG QUERY HIGH
Yes, HCC - Hybrid Columnar Compression with Warehouse compression is enabled for that table and this is the reason why to_char()
predicate is not offloaded to storage and why the query results are correct on both databases when the offload does not occur. This answers one of the questions regarding the behavior of to_char().
However, a key question remains:
If the compression type is the same on TEST and PROD, why does the query return correct results on TEST without to_char()
but fails on PROD when Smart Scan kicks in? Is everything truly identical between the two environments?
Digging more arround table compression, I discovered that the CUSTOM_LOG
is compressed differently on TEST compered to PROD
Despite being configured for QUERY HIGH Warehouse compression, CUSTOM_LOG
appears to be a table with frequent INSERT operations. Just a few days after PROD upgrade from 12.1 to 12.2 and fresh statistics gathering:
SQL> SELECT TABLE_NAME, INSERTS, UPDATES, DELETES, TIMESTAMP FROM DBA_TAB_MODIFICATIONS WHERE TABLE_NAME = 'CUSTOM_LOG' AND TABLE_OWNER = 'F';
TABLE_NAME INSERTS UPDATES DELETES TIMESTAMP
------------------------- ---------- ---------- ---------- ---------
CUSTOM_LOG 159891 0 0 09-DEC-24
Theory says:
- HCC is triggered when INSERTs are done via direct-path load or with array insert (from 12.2), but typical, simple INSERT will cause that database will place data in row format
- For HCC UPDATED data, database moves the updated rows to row format with reduced compression level using
COMP_BLOCK
format, type 64 (check Martins Bach post)
Thanks to the fact of having access to TEST database and inspired by aproach in this Martins Bach post I queried CUSTOM_LOG
table to determine the exact compression types applied to the rows. It took a while to get the answer on TEST, but results were unexpected:
SQL> with comptypes as (
select rownum rn, dbms_compression.get_compression_type('F_TEST', 'CUSTOM_LOG', rowid) ctype
from F_TEST.CUSTOM_LOG
)
select count(*), ctype from comptypes group by ctype;
COUNT(*) CTYPE
---------- ----------
35507960 64
18642218 1
Only two types of compression were detected in this table, and surprisingly, neither of them is Hybrid Columnar Compression (HCC):
- type 64 (former HCC, but now COMP_BLOCK)
- type 1 (No Compression)
To avoid querying the table in the same way on PROD, I opted for a different approach: by checking the FLASHCACHECONTENT
on CELLs. Since the CUSTOM_LOG
table is heavily accessed (hot data), it should reside in the Flash Cache.
To verify this on TEST, I needed the data_object_id
for CUSTOM_LOG
from DBA_OBJECTS
to reference the objectNumber
on the Exadata storage cells. Each cell needs to be checked for a complete picture, but for clarity, here is the output from a single cell (others were similar):
CellCLI> LIST FLASHCACHECONTENT WHERE objectNumber=5481235 detail;
cachedKeepSize: 0
cachedSize: 7322836992
cachedWriteSize: 5535588352
clusterName:
columnarCacheSize: 0
columnarKeepSize: 0
dbID: 401830952
dbUniqueName: TEST
hitCount: 135037
missCount: 95814
objectNumber: 5481235
tableSpaceNumber: 18
On TEST, the columnarCacheSize
value for CUSTOM_LOG
was 0, confirming that no data was cached in HCC columnar format — even though the table resides in Flash Cache.
How about PROD:
CellCLI> LIST FLASHCACHECONTENT WHERE objectNumber=5133442 detail;
cachedKeepSize: 0
cachedSize: 25897451520
cachedWriteSize: 3570089984
clusterName:
columnarCacheSize: 200933376
columnarKeepSize: 0
dbID: 772460701
dbUniqueName: PROD
hitCount: 3860427
missCount: 43421
objectNumber: 5133442
tableSpaceNumber: 47
It’s different. columnarCacheSize
shows some values which means that on PROD CUSTOM_LOG
in some parts is presenet in Flash Cache in HCC format.
This is our key difference
we’ve uncovered that:
- on TEST despite QUERY HIGH compression on table, due to DMLs, table doeas not exists in HCC format any more
- on PROD table is also using QUERY HIGH compression but some part of it exists in that format.
To complete the picture, we must highlight one critical fact::
HCC has been applied to a table containing LOB data, which Oracle explicitly does not recommend. According to the Oracle Hybrid Columnar Compression Guide:
“Hybrid Columnar Compression is designed for relational data, not for unstructured data in BLOBs (or CLOBs).”
The implications are severe:
Offloding quries on HCC objects that include inlined LOBs via Smart Scan to storage layer can produce incorrect results. What’s more concerning is that the database allows HCC compression to be applied to such tables without any warnings, potentially introducing unexpected behavior, as we’ve observed.