问题 由于SYS_OP_C2C内部转换,Oracle SQL执行计划发生了变化


我想知道为什么这个查询的成本

select * from address a
left join name n on n.adress_id=a.id
where a.street='01';

高于

select * from address a
left join name n on n.adress_id=a.id
where a.street=N'01';

地址表看起来像这样

ID              NUMBER
STREET          VARCHAR2(255 CHAR)
POSTAL_CODE     VARCHAR2(255 CHAR)

和名称表看起来像这样

ID              NUMBER
ADDRESS_ID      NUMBER
NAME            VARCHAR2(255 CHAR)
SURNAME         VARCHAR2(255 CHAR)

这些是解释计划返回的成本

解释'01'的计划

-----------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name                 | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |                      |  3591 |  1595K|    87   (0)| 00:00:02 |
|   1 |  NESTED LOOPS OUTER          |                      |  3591 |  1595K|    87   (0)| 00:00:02 |
|*  2 |   TABLE ACCESS FULL          | ADDRESS              |     3 |   207 |     3   (0)| 00:00:01 |
|   3 |   TABLE ACCESS BY INDEX ROWID| NAME                 |  1157 |   436K|    47   (0)| 00:00:01 |
|*  4 |    INDEX RANGE SCAN          | NAME_HSI             |  1157 |       |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------

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

   2 - filter("A"."STREET"='01')
   4 - access("N"."ADDRESS_ID"(+)="A"."ID")

解释N'01'的计划

-----------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name                 | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |                      |   347 |   154K|    50   (0)| 00:00:01 |
|   1 |  NESTED LOOPS OUTER          |                      |   347 |   154K|    50   (0)| 00:00:01 |
|*  2 |   TABLE ACCESS FULL          | ADDRESS              |     1 |    69 |     3   (0)| 00:00:01 |
|   3 |   TABLE ACCESS BY INDEX ROWID| NAME                 |  1157 |   436K|    47   (0)| 00:00:01 |
|*  4 |    INDEX RANGE SCAN          | NAME_HSI             |  1157 |       |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------

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

   2 - filter(SYS_OP_C2C("A"."STREET")=U'01')
   4 - access("N"."ADDRESS_ID"(+)="A"."ID")

如您所见,N'01'查询的成本低于'01'的成本。知道为什么吗? N'01'还需要将varchar转换为nvarchar,因此成本应该更高(SYS_OP_C2C())。另一个问题是为什么N'01'查询处理的行低于'01'?

[编辑]

  • address 有30行。
  • name 有19669行。

1931
2017-10-10 09:31


起源

你可以在两个表上发布行数吗? - SriniV
@realspirituals看到我的编辑。 - pepuch
你收集了关于桌子的统计数据吗?这里最大的区别是优化器猜测地址表中的3行满足 street='01' 但只有1排满足 street=N'01'。第一种情况是优化器使用适合于等式谓词的基数估计算法,另一种情况是优化器看到函数应用于表中的列,这意味着它必须猜测 - 可能猜测“大约5%的行数桌子。” - Kim Berg Hansen
@KimBergHansen,我不是SQL开发人员,所以我甚至不知道如何存储表统计信息(BTW我该如何检查它?它存储在db中的某个位置吗?)。 - pepuch
收集统计数据不会有任何区别。无论如何都将应用内部函数,并且两个不同应用的滤波器的基数估计值将不同。 - Lalit Kumar B


答案:


SYS_OP_C2C 是一个 internal function 哪个 implicit conversion 的 varchar2 至 national character set 运用 TO_NCHAR 功能。因此,与使用正常比较的滤波器相比,滤波器完全改变。

我不确定行数的原因 ,但我可以保证它可以 更多 太。成本估算不会受到影响。

让我们试着逐步看一下测试用例。

SQL> CREATE TABLE t AS SELECT 'a'||LEVEL col FROM dual CONNECT BY LEVEL < 1000;

Table created.

SQL>
SQL> EXPLAIN PLAN FOR SELECT * FROM t WHERE col = 'a10';

Explained.

SQL> SELECT * FROM TABLE(dbms_xplan.display);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 1601196873

--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      |     1 |     5 |     3   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| T    |     1 |     5 |     3   (0)| 00:00:01 |
--------------------------------------------------------------------------

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

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------

   1 - filter("COL"='a10')

13 rows selected.

SQL>

到现在为止还挺好。由于只有一行的值为'a10',优化器估计一行。

让我们看看国家字符转换。

SQL> EXPLAIN PLAN FOR SELECT * FROM t WHERE col = N'a10';

Explained.

SQL> SELECT * FROM TABLE(dbms_xplan.display);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 1601196873

--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      |    10 |    50 |     3   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| T    |    10 |    50 |     3   (0)| 00:00:01 |
--------------------------------------------------------------------------

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

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------

   1 - filter(SYS_OP_C2C("COL")=U'a10')

13 rows selected.

SQL>

这里发生了什么?我们可以看到 filter(SYS_OP_C2C("COL")=U'a10'),这意味着应用了一个内部函数,它转换了 varchar2 价值 nvarchar2。过滤器现在找到10行。

这也将抑制任何索引使用,因为现在在列上应用了一个函数。我们可以通过创建一个来调整它 function-based index 避免 full table scan

SQL> create index nchar_indx on t(to_nchar(col));

Index created.

SQL>
SQL> EXPLAIN PLAN FOR SELECT * FROM t WHERE to_nchar(col) = N'a10';

Explained.

SQL> SELECT * FROM TABLE(dbms_xplan.display);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 1400144832

--------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name       | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |            |    10 |    50 |     2   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| T          |    10 |    50 |     2   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN                  | NCHAR_INDX |     4 |       |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
---------------------------------------------------

   2 - access(SYS_OP_C2C("COL")=U'a10')

14 rows selected.

SQL>

但是,这会使执行计划类似吗?不,我想 两个不同的角色 ,过滤器不会被应用。因此,不同之处在于。

我的研究说,

通常,当通过应用程序发送数据时会出现这种情况   是nvarchar2类型,但表列是varchar2。因此,Oracle   在过滤器操作中应用内部函数。我的建议   是,以便更好地了解您的数据,以便您在使用期间使用类似的数据类型   设计阶段。


7
2017-10-10 11:01



+1,只需通过更改字符集即可避免此问题。不是吗 - SriniV
我相信,最好保持数据类型相同。如果要传递的值是nvarchar2,则将列数据类型保留为nvarchar2。这样你就不会面临内部转换的情况。 - Lalit Kumar B
@LalitKumarB,我同意你的看法。我在玩sql查询性能时遇到了这个问题。 - pepuch


在担心解释计划时,重要的是是否有关于表格的最新统计数据。如果统计数据不能很好地表示实际数据,那么优化器将会出错并错误地估计基数。

您可以通过查询数据字典来检查收集统计信息的时间:

select table_name, last_analyzed
  from user_tables
 where table_name in ('ADDRESS','NAME');

您可以通过调用收集要使用的优化程序的统计信息 DBMS_STATS

begin
   dbms_stats.gather_table_stats(user, 'ADDRESS');
   dbms_stats.gather_table_stats(user, 'NAME');
end;

因此,在收集统计数据后,您可能会得到不同的解释计也许不是。

解释计划的不同主要是因为优化器估计在两种情况下它在地址表中找到的行数会有所不同。

在第一种情况下,您有一个具有相同数据类型的等式谓词 - 这是好的,优化器通常可以很好地估计基数(行数)这样的情况。

在第二种情况下,一个函数应用于列 - 这通常是坏的(除非你有基于函数的索引)并将强制优化器采取疯狂的猜测。由于优化器的开发人员试图对其进行改进,因此在不同版本的Oracle中,这种疯狂的问题会有所不同。一些版本的疯狂猜测将简单地类似于“我猜表中行数的5%。”

在比较不同的数据类型时,最好避免隐式转换,特别是在这种情况下,隐式转换会在  而不是文字。如果您遇到的值为NVARCHAR2数据类型且需要在上面的谓词中使用它,那么将值显式转换为列的数据类型可能是个好主意。

select * from address a
left join name n on n.adress_id=a.id
where a.street = CAST( N'01' AS VARCHAR2(255));

在这种情况下,使用文字它当然没有意义。在这里,您只需使用第一个查询。但如果它是一个变量或函数参数,也许你可以使用用例来做这样的事情。


3
2017-10-10 11:17



正如我已经说过的,收集统计数据不会避免内部转换,这是OP的基本问题。最好的解决方案是拥有正确的数据类型。 - Lalit Kumar B
我完全同意你的回答。但OP也在评论中表明他不了解统计数据。如果他收集统计数据,他可能会看到相反的情况 - 他可能会看到没有转换的查询估计小得多的基数,然后他就不会真的问过这个问题。我的观点是,没有人应该尝试理解解释计划,如果他们没有当前的统计数据;-)然后他们应该总是使用正确的数据类型,因为你的答案清楚地表明我也说过 - 隐式转换给出了错误的基数。 - Kim Berg Hansen
这应该是OP的一个很好的附加信息:-) - Lalit Kumar B


正如我所看到的,第一个查询返回3591行,第二个返回347行。因此Oracle需要较少的I / O操作,这就是成本较低的原因。

不要混淆

N'01'还需要将varchar转换为nvarchar

Oracle执行一次硬解析,然后对相同的查询使用软解析。所以oracle工作的时间越长,它就越快。


0
2017-10-10 10:09



为什么这两个(几乎相同的)查询返回不同的行数? - pepuch
软解与硬解析不会改变执行计划中的成本估算。 - a_horse_with_no_name
你在这里是什么意思“所以oracle工作的时间越长,它变得越快。”? - SriniV
Oracle在解析时不会将varchar2转换为nvarchar2。在解析时,优化器观察到在第一个查询中将varchar2与varchar2进行比较,罚款。但是在第二个查询中,它观察到varchar2列与nvarchar2文字进行比较,因此执行计划将包括将varchar2列隐式转换为nvarchar2(SYS_OP_C2C 函数调用)将改变优化器估计基数的方式。在运行时,隐式转换将发生 - 无论是硬分析还是软分析。 - Kim Berg Hansen
确切地说,它构建了包含函数SYS_OP_C2C的重写查询。每次执行重写查询时都会发生函数的实际执行(varchar2到nvarchar2转换)。所以当你说它只在硬分析时将varchar转换为nvarchar时,情况就不是这样了。转换在每次执行时都会运行。硬解析仅创建包含转换函数的重写查询。 - Kim Berg Hansen