【问题标题】:Oracle SQL execution plan changes due to SYS_OP_C2C internal conversionOracle SQL 执行计划因 SYS_OP_C2C 内部转换而改变
【发布时间】:2014-12-05 10:25:30
【问题描述】:

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

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 行。

【问题讨论】:

  • 你能把两张表的行数都贴出来吗?
  • @realspirituals 看到我的编辑。
  • 您是否收集了有关表格的统计信息?这里最大的不同是优化器猜测地址表中的 3 行满足street='01',但只有 1 行满足street=N'01'。第一种情况优化器使用适合相等谓词的基数估计算法,另一种情况优化器看到一个函数应用于表中的列,这意味着它必须猜测 - 可能猜测“大约 5% 的行数桌子。”
  • @KimBergHansen,我不是 SQL 开发人员,所以我什至不知道如何存储表统计信息(顺便说一句,我该如何检查它?它是否存储在 db 中的某个位置?)。
  • 收集统计数据不会产生任何影响。无论如何都会应用内部函数,并且两个不同应用的过滤器的基数估计会有所不同。

标签: sql oracle oracle11g


【解决方案1】:

SYS_OP_C2C 是一个internal function,它使用TO_NCHAR 函数将varchar2implicit conversion 转换为national character set。因此,与使用普通比较的过滤器相比,过滤器完全改变了。

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

让我们尝试在测试用例中逐步查看。

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。因此,甲骨文 在过滤器操作中应用内部函数。我的建议 是,要很好地了解你的数据,以便你在使用类似的数据类型 设计阶段。

【讨论】:

  • +1,您可以通过更改字符集来避免这种情况。不是吗
  • 我相信,最好保持数据类型相同。如果要传递的值是 nvarchar2,则保持列数据类型为 nvarchar2。这样您就永远不会遇到内部转换的情况。
  • @LalitKumarB,我同意你的看法。我只是在玩 sql 查询性能时遇到了这个问题。
【解决方案2】:

当担心解释计划时,是否有关于表的当前统计信息很重要。如果统计信息不能很好地代表实际数据,那么优化器就会出错并错误地估计基数。

您可以通过查询数据字典来查看多久以前收集的统计信息:

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));

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

【讨论】:

  • 我已经说过,收集统计数据不会避免内部转换,这是OP的基本问题。最好的解决方案是拥有正确的数据类型。
  • 我完全同意你的回答。但是 cmets 中的 OP 也表明他不了解统计数据收集。如果他收集统计数据,他可能会看到相反的情况——他可能会看到没有转换的查询估计的基数要小得多,然后他就不会真的问这个问题。我的观点是,如果没有当前的统计数据,没有人应该尝试理解解释计划;-) 然后他们应该始终使用正确的数据类型,因为您的回答清楚地证明了我所说的 - 隐式转换给出了错误的基数。
【解决方案3】:

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

不要与

混淆

N'01' 需要另外将 varchar 转换为 nvarchar

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

【讨论】:

  • 为什么这两个(几乎相同)查询返回不同的行数?
  • 软解析与硬解析不会改变执行计划中的成本估算。
  • 你的意思是“所以你的预言机工作的时间越长它变得越快。”?
  • Oracle 不会在解析时将 varchar2 转换为 nvarchar2。在解析时,优化器观察到在第一个查询中将 varchar2 与 varchar2 进行比较,很好。但在第二个查询中,它观察到 varchar2 列与 nvarchar2 文字进行比较,因此执行计划将包括将 varchar2 列隐式转换为 nvarchar2(SYS_OP_C2C 函数调用),这将改变优化器估计基数的方式。在运行时会发生隐式转换——无论是硬解析还是软解析。
  • 没错,它构建了包含函数 SYS_OP_C2C 的重写查询。每次执行重写的查询时都会发生函数的实际执行(varchar2 到 nvarchar2 的转换)。因此,当您说它仅在硬解析时将 varchar 转换为 nvarchar 时,这是不正确的。转换在每次执行时发生在运行时。硬解析只创建包含转换函数的重写查询。
猜你喜欢
  • 1970-01-01
  • 2021-06-16
  • 1970-01-01
  • 1970-01-01
  • 2021-07-23
  • 2020-08-04
  • 1970-01-01
  • 2012-03-07
  • 2012-09-02
相关资源
最近更新 更多