摘自《成功之路:Oracle 11g学习笔记》
17.3.4 SQL语句的优化过程
本节介绍SQL语句的性能优化过程,并引入和SQL语句优化相关的一些知识。SQL语句的优化过程如图17-3所示。
图17-3 SQL语句的优化过程
1.找出高负荷SQL
如果数据库运行缓慢,或者应用程序的某些功能出现停滞现象,可能是高负荷SQL语句(High-Load SQL)阻塞了应用程序的执行,这时候,我们要抓出高负荷的SQL语句。所谓“高负荷SQL语句”是指那些性能低下,且消耗过多系统资源的SQL语句。下面的方法(工具)可以用于找出高负荷SQL语句:
Ø 性能检测器ADDM
Ø 自动化SQL调整(Automatic SQL Tuning)
Ø 自动工作负载库AWR
Ø 查看视图V$SQL
Ø 来自客户的报告
Ø SQL跟踪工具(SQL Trace)
2.查看执行计划
一旦获得高负荷的SQL语句,就需要对该SQL语句进行调整,SQL语句调整的工具比较多,这些工具使SQL语句的调整越来越智能化,简单化!这些工具是:
Ø SQL Trace(SQL跟踪)提供单条SQL语句的性能信息(包括统计信息)。
Ø AUTOTRACE(自动跟踪)是SQL*Plus的一个功能,也是用于产生某条SQL语句的性能信息。
Ø SQL Access Advisor(SQL访问指导)通过建议要创建、删除或保留的索引、物化视图、物化视图日志或分区来确定并帮助解决与 SQL 语句执行相关的性能问题。
Ø SQL Tuning Advisor(SQL调整指导)可以为SQL的优化提供建议,使SQL的调整简单化。
Ø SQL Performance Analyzer(SQL性能分析器)可用于预测数据库环境改变对SQL执行造成的负面影响。
有SQL调整经验的朋友都知道,SQL调整是一个重复的过程。从使用方便的角度考虑,笔者个人喜欢使用AUTOTRACE(因为SQL Trace使用比较麻烦,SQL Access Advisor和SQL Tuning Advisor通常又依赖于运行在服务器端的数据库控件)。从智能化的角度考虑,可以使用SQL Access Advisor和SQL Tuning Advisor。本书重点介绍AUTOTRACE。
& 配置AUTOTRACE
在第一次使用AUTOTRACE时,需要配置AUTOTRACE。AUTOTRACE要求创建执行计划表(PLAN_TABLE)和角色PLUSTRACE,并把角色PLUSTRACE授予要使用AUTOTRACE的用户(本例中是ITME)。详细过程说明如下。
注:本例中的ORACLE_HOME是C:\app\Administrator\product\11.1.0\db_3。
创建执行计划表PLAN_TABLE。
SQL> CONNECT itme
SQL> @$ORACLE_HOME\rdbms\admin\utlxplan.sql
创建角色PLUSTRACE。
SQL> CONNECT / AS SYSDBA
SQL> @$ORACLE_HOME\sqlplus\admin\plustrce.sql
把角色PLUSTRACE授予用户itme。
SQL> CONNECT / AS SYSDBA
SQL> GRANT PLUSTRACE TO itme;
& 运行AUTOTRACE(获得SQL语句的执行计划)
每次用SQL*Plus登录数据库以后,需要打开AUTOTRACE,接下来执行的每条SQL都会自动显示执行计划,这是笔者最喜欢AUTOTRACE的原因。打开的AUTOTRACE只对本次会话生效,下次启动SQL*Plus,还需要重新打开AUTOTRACE。AUTOTRACE的选项开关如下所示:
Ø SET AUTOT[RACE] OFF关闭AUTOTRACE。
Ø SET AUTOT[RACE] ON打开AUTOTRACE,显示SQL语句的执行计划和统计信息,还显示SQL语句的执行结果。
Ø SET AUTOT[RACE] TRACEONLY 打开AUTOTRACE,仅显示AUTOTRACE信息,不显示SQL语句的执行结果。
Ø SET AUTOT[RACE] ON EXPLAIN 打开AUTOTRACE,仅显示SQL语句的执行计划。
Ø SET AUTOT[RACE] ON STATISTICS打开AUTOTRACE,仅显示SQL语句的统计信息。
AUTOTRACE的使用过程说明如下。
打开AUTOTRACE(可使用AUTOTRACE的各种选项开关)。
SET AUTOTRACE TRACEONLY EXPLAIN STATISTICS;
这是笔者进行SQL调整时用得最多的AUTOTRACE选项组合。它只显示执行计划和统计信息,不显示SQL的返回结果。
注:执行计划涉及的表和数据,请参考光盘\script\执行计划案例脚本\。
执行有问题的SQL语句,我们将自动获得有问题的SQL语句的执行计划。
SQL> SELECT a.op_no, a.fnm, b.menu_id, b.gwa_code, b.tr_code, b.button_flg
2 FROM tb_user a, tb_user_grant b
3 WHERE a.op_no = b.op_no;
已选择256行。
执行计划
----------------------------------------------------------
Plan hash value: 3384233913
-----------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 256 | 9984 | 7 (15)| 00:001 |
|* 1 | HASH JOIN | | 256 | 9984 | 7 (15)| 00:001 |
| 2 | TABLE ACCESS FULL| TB_USER | 113 | 1695 | 3 (0)| 00:001 |
| 3 | TABLE ACCESS FULL| TB_USER_GRANT | 257 | 6168 | 3 (0)| 00:001 |
-----------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - access("A"."OP_NO"="B"."OP_NO")
统计信息
----------------------------------------------------------
0 recursive calls
0 db block gets
32 consistent gets
0 physical reads
0 redo size
7748 bytes sent via SQL*Net to client
603 bytes received via SQL*Net FROM client
19 SQL*Net roundtrips to/FROM client
0 sorts (memory)
0 sorts (disk)
256 rows processed
SQL>
上面的SQL报告分成两部分:第一部分显示SQL语句的执行计划;第二部分显示SQL语句的统计信息。
AUTOTRACE打开之后(只需打开一次),我们可以执行其他SQL语句,获得其他SQL语句的执行计划。
3.执行计划的阅读
本节需要了解执行计划的阅读方法和执行步骤(访问路径)的含义。
& 阅读方法
执行计划是一个树状结构。每一行就是一个执行步骤(操作),每个步骤有编号(ID),每个步骤会返回数据行(Rows),这些数据行会被下一步进行处理,每个步骤还会显示处理了多少字节(Bytes)的数据及该步骤的执行代价(Cost,这个值是相对值)和消耗的时间(以秒为单位)。Oracle最先从叶子节点(FIRST GRANDCHILD)开始执行,然后执行到上一层(FIRST CHILD、SECOND CHILD),层层递进,最后执行到顶层(PARENT)。如果同一层中有并列的多个步骤(如FIRST CHILD、SECOND CHILD),最上面的步骤(FIRST CHILD)首先被执行。ID前面有星号(*),表示下面有详细的解释。一个执行计划由多个执行步骤组成。
PARENT(父亲)
FIRST CHILD (孩子)
FIRST GRANDCHILD (孙子)
SECOND CHILD(孩子)
SQL语句的执行过程说明如下(参考上一节产生的执行计划):
SELECT a.op_no, a.fnm, b.menu_id, b.gwa_code, b.tr_code, b.button_flg
FROM tb_user a, tb_user_grant b
WHERE a.op_no = b.op_no;
对表TB_USER执行全表扫描(TABLE ACCESS FULL),返回113条记录,执行代价是3,执行时间是00:001秒。
对表TB_USER_GRANT执行全表扫描(TABLE ACCESS FULL),返回257条记录。
对两次扫描的数据进行连接(HASH JOIN),共处理256条记录。
返回结果集。
& 访问路径(Access Path)
访问路径是指从数据库中检索数据的方法。访问路径有全表扫描(Full Table Scans)、行ID扫描(Rowid Scans)、索引扫描(Index Scans)、聚簇访问(Cluster Access),哈希访问(Hash Access)和采样表扫描(Sample Table Scans)。
全表扫描:Oracle顺序地读取分配给表的每个数据块,直到读到表的高水线位线(High Water Mark,HWM,用于标识表的最后一个数据块)。说白了,全表扫描会读取表中所有的行。
索引扫描:先通过索引查找到数据对应的ROWID值,然后根据ROWID直接从表中得到具体的数据。一个ROWID唯一地标识一行数据。
行ID扫描:通过行ID直接获得需要的数据,这是访问数据最快的一种方式。行ROWID定义了一行数据所在的数据文件、数据块以及行在该块中的位置。通过ROWID访问数据,需要首先获取被访问行的ROWID。
聚簇访问:访问存储在聚簇中的表。在聚簇中,具有相同聚簇值(Cluster Key Value)的行被物理地存储在一起。聚簇访问首先扫描聚簇索引,从聚簇索引中获得ROWID,然后根据ROWID定位需要访问的数据。
哈希访问:哈希扫描用于在哈希聚簇中定位数据行,在哈希聚簇中,具有相同哈希值的行被存放在相同的数据块中。进行哈希扫描时,Oracle应用哈希函数得到哈希值,然后根据哈希值扫描数据块。
采样表扫描:采样表扫描指按照一定的百分比对表的数据进行采样。如:
SQL> SELECT *
2 FROM tb_user_grant SAMPLE BLOCK (10);
已选择257行。
执行计划
----------------------------------------------------------
Plan hash value: 3704821630
-----------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
| 0 | SELECT STATEMENT | | 26 | 832 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS SAMPLE| TB_USER_GRANT | 26 | 832 | 2 (0)| 00:00:01 |
上述语句访问表tb_user_grant中10%的数据。
& 连接(Join)方法
连接是指从一个或者多个表中检索数据。Oracle主要有3种连接方法:嵌套循环连接(Nested Loop Joins)、哈希连接(Hash Joins)和排序合并连接(Sort Merge Joins)。在SELECT语句中,连接方法至关重要,它直接影响查询的性能。
& 嵌套循环连接(Nested Loop Joins)
嵌套循环连接适合处理小量数据。Oracle首先选择一张表作为连接的驱动表(Driving Table),这张表也被叫做外部表(Outer Table),其他表被当做内部表(Inner Table),内部表也叫被驱动表。对于驱动表中的每行,Oracle将访问内部表中的所有行。
嵌套循环连接可以快速返回查询结果的前几行,这是因为,它不需要等到全部循环结束再返回结果集,而是不断地返回查询的结果,也就是语句边执行边返回结果。
& 哈希连接(Hash Joins)
哈希连接用于处理大数据量的数据集(特别是一个大表和一个小表的连接)。Oracle从两个表中选择一个较小的表,按照连接关键字(Join Key),在内存中建立哈希表(因此哈希表中较小的表适合放在内存中),然后,Oracle扫描另外一个表(大表),从中发现可以被连接的行。
& 排序合并连接(Sort Merge Joins)
排序合并连接的原理是,两个表先按照连接关键字(Join Key)进行排序,再把排好序的两个结果集合并在一起。所以,如果其中一个结果集已经排好序,“排序合并连接”方式的速度将比“哈希连接”方式快。
4.统计信息的解释
通过AUTOTRACE,我们不仅可以获得SQL语句的执行计划,还可以获得SQL语句的统计信息。接下来,我们将对SQL语句的统计信息进行解释。
0 recursive calls——递归调用的次数。
0 db block gets——当前块(Current Block)被请求的次数,当前块是指未被修改过的块。
32 consistent gets——对块一致性读的请求次数,一致性读涉及读回滚段。
0 physical reads——从磁盘读的块的数量。
0 redo size——产生的回滚数据的大小,单位是字节(Bytes)。
7748 bytes sent via SQL*Net to client——后台进程发送给客户端的信息量大小,单位是字节。
603 bytes received via SQL*Net FROM client——客服端发给数据库服务器的信息量的大小,单位是字节。
19 SQL*Net roundtrips to/FROM client——Oracle Net发送和接受的信息数量,单位是条。
0 sorts (memory)——内存排序的次数。
0 sorts (disk)——磁盘排序的次数。
256 rows processed——处理数据的行数(通常指返回的记录数)。
5.SQL优化的目标
有的朋友说,减少一条SQL语句的执行时间是我们优化SQL语句的目标。对,没错,但是在大部分情况下,我们不能在生产系统中直接执行调试中的SQL语句,以获得该语句的执行时间。不过,在不真正执行SQL语句的情况下,我们有一些衡量指标,这些指标可以帮助我们衡量优化后的SQL语句是否真的比以前的SQL语句更加高效。因此,根据经验,笔者总结SQL调整的目标是:
Ø 减小consistent gets的值
Ø 减小db block gets的值
Ø 减少排序,尤其磁盘排序sorts(disk)
Ø 减少递归调用(recursive calls)
Ø 减小执行代价(cost)
要实现SQL调整的目标,我们需要采取诸如加索引、改写SQL语句等措施。
知识点索引——执行代价
执行代价与SQL语句使用的资源(I/O、CPU、内存等)有关。如果一条SQL语句使用的资源较多,我们就说该SQL语句的执行代价大。执行代价的大小是衡量SQL语句是否高效的标准之一,但是,执行代价是一个相对值。
6.SQL优化措施(基本准则)
本节将介绍SQL语句优化的措施。SQL语句的优化是一个复杂而循序渐进的过程。在对SQL语句实行优化措施之后,我们要查看衡量SQL语句的各项指标是否下降。有的优化措施不仅不能加速SQL语句的执行,反而使SQL语句的执行更加缓慢。进行SQL优化时,我们可以采取下面的措施:
Ø 收集统计信息
Ø 重构索引
Ø 重构数据
Ø 禁用约束和触发器
Ø 尽量使用相同的SQL语句
Ø 改写SQL语句
& 收集统计信息
统计信息是存放在数据字典中的一系列的数据,它用于精准地描述数据库和数据库中的对象(如表、索引)。统计信息包括:表的统计信息、列的统计信息、索引的统计信息、系统的统计信息(包括CPU和I/O的)。统计信息能够帮助优化器选择最好的执行计划,所以,统计信息的精确性尤为重要。如果你没有收集统计信息或者很长时间没有重新收集统计信息(旧的统计信息不能准确地反映数据变化),都将影响优化器选择最优的执行计划。
因为数据库中的对象和数据在不断变化,所以我们应该定期收集统计信息。有以下两种方法收集统计信息。
& 自动收集统计信息
在默认情况下,Oracle启动了统计信息的自动收集功能,如果你禁用了统计信息的自动收集,可以使用下面的命令启用统计信息的自动收集。
BEGIN
DBMS_AUTO_TASK_ADMIN.ENABLE(
client_name => 'auto optimizer stats collection',
operation => NULL,
window_name => NULL);
END;
自动收集统计信息会消耗一部分系统资源,如果没有必要可以关闭该功能。关闭自动收集统计信息的命令如下所示:
BEGIN
DBMS_AUTO_TASK_ADMIN.DISABLE(
client_name => 'auto optimizer stats collection',
operation => NULL,
window_name => NULL);
END;
& 手动收集统计信息
如果我们不想启用“自动收集统计信息”,可以利用包DBMS_STATS来手工收集统计信息。包DBMS_STATS包含如下的过程:
¢ 收集索引的统计信息
EXECUTE DBMS_STATS.GATHER_INDEX_STATS('ITME','KK');
上述命令收集用户模式itme下的索引kk。
¢ 收集表的统计信息
EXECUTE DBMS_STATS.GATHER_TABLE_STATS('ITME','STUDENTBOOK');
上述命令收集用户itme的表studentbook的统计信息。
¢ 收集用户拥有的所有对象的统计信息
EXECUTE DBMS_STATS.GATHER_SCHEMA_STATS('ITME');
上述命令收集用户itme拥有的所有对象的统计信息。
¢ 收集数据字典对象的统计信息
EXECUTE DBMS_STATS.GATHER_DICTIONARY_STATS;
¢ 收集数据库中所有对象的统计信息
EXECUTE DBMS_STATS.GATHER_DATABASE_STATS;
& 重构索引
重构索引的工作包括以下几方面:
Ø 删除多余的索引,以加速DML语句的执行。如果一个表上的操作是以DML为主,考虑删除冗余的DML。
Ø 删除组成索引列中无用的列。因为有的列在WHERE条件中很少使用或者从未使用,这样的列不仅不利于查询,可能会反而阻碍DML语句的执行。
Ø 改变索引中列的顺序,使索引中列的顺序和WHERE条件中列的顺序相同。
Ø 考虑往索引中添加新的列。
Ø 重建索引,消除索引中的碎片。
注意:索引不是灵丹妙药。有时候索引却会加重系统的负担。
& 重构数据
如果数据分布不合理,即使我们再怎么调整SQL语句,效果也只是杯水车薪。数据的分布是否合理将直接影响SQL的性能。在此,我们主要讨论数据分布的两项技术。
Ø 数据分布的其中一项技术是分区(Partitioning),理想的分区技术是,将一个大表按照一定的逻辑分成n块,每块单独存放在一个物理磁盘中,这时候需要n块磁盘,并在分区表上创建相应的索引。
Ø 数据分布的另外一项技术是聚簇(Cluster)。如果两个表经常按照某列进行连接(Join),则可以把这两个表放到聚簇中。这项技术使两个表中的数据物理地存放在一起,减少I/O,加速对两个表的访问。
& 禁用约束和触发器
使用触发器会消耗系统资源。如果有太多的触发器存在,系统性能势必受到影响,这时候,可以考虑禁用没有必要的触发器。同理,也需要考虑禁用不必要的约束。
& 尽量使用相同的SQL语句
在编码时,应该尽量使用相同的SQL语句,使用相同的SQL语句可以减少解析时间。我们都知道,解析要做好多工作,这些工作将延长整条SQL语句的执行时间。
两条SQL语句相同必须满足的条件是:
Ø 空格、大小写、换行、注释、表名、关键字、值相同。
Ø 引用的对象必须属于相同的模式。
Ø 如果使用绑定变量,绑定变量的名字、类型、长度必须相同。
例:两条不同的SQL语句(大小写不同):
SELECT * FROM Qmm;
SELECT * FROM qmm;
& 改写SQL语句
在对SQL语句的外围环境(如索引、统计信息、数据分布等)进行调整后,如果还不能获得预期的执行速度,就需要对原来的SQL语句进行改写。改写SQL语句是一项很有挑战性的工作,它不仅需要实现与原来的SQL相同的功能,新的SQL语句还必须能够高速运行(相对原来的SQL语句)。改写SQL语句有时候需要使SQL语句的结构发生彻底的改变,而不仅仅局限于SQL语句的小修小补。改写SQL语句时,可以参考下面的原则:
Ø 要从执行计划中仔细查看、分析SQL语句中的每个表是否被高效地访问。
Ø 避免在WHERE子句中的列上使用函数,这样会使列上的索引失效。如果真的想在列上使用函数,则需要创建函数索引。
例:
在列上使用函数,该列order_name上的索引失效。
SELECT * FROM T_ORDER
WHERE UPPER(order_name)='PC';
如果想在列order_name上利用函数,必须创建函数索引indf3。
CREATE INDEX indf3 on T_ORDER(UPPER(order_name));
Ø 尽量使用等价连接。
Ø 排序列上应该建立索引。
Ø 保持排序列和索引列次序一致。
Ø 尽量保持WHERE子句中列的顺序和索引列的顺序一致。
Ø 尽量少用嵌套查询。如果非要使用,请用NOT EXIST代替NOT IN子句。
Ø 用多表连接代替EXISTS子句。
Ø 选择适合的连接(join)。如果关注响应时间,可以使用嵌套循环连接(Nested Loop Joins);如果关注吞吐量,且数据源已经排序,可以使用排序合并连接(Sort Merge Joins);如果是一个小表和大表的连接,考虑使用哈希连接(Hash Joins)。
Ø 在连接(Joins)中,注意表的连接顺序。要选择好驱动表(Outer Table)和被驱动表(Inner Table)。通常,驱动表是由过滤条件限制返回记录最少的那张表,而不是数据量最少的那张表。
Ø 大量的排序操作影响系统性能,所以尽量减少排序操作。GROUP BY、ORDER BY、 ROLLUP、DISTINCT等都会产生排序。少用DISTINCT,用EXISTS代替DISTINCT。
Ø 避免使用数据类型自动转换功能,自动数据类型转换会增加额外的系统开销,还会使索引无效(也就是不能利用存在的索引)。
Ø 启用并行执行。并行执行机制使多个服务器进程并行地执行一条SQL语句,这是Oracle的强大之处。
Ø 应尽量使用VARCHAR2类型代替CHAR类型,这样可以节省磁盘空间,减少兼容带来的问题。
Ø 连接符OR、IN、AND以及=、<=、>=等前后加上一个空格。这是一种良好的书写习惯,防止产生两条不同的SQL语句(而实际上它们是同一条SQL语句,只是不好的书写习惯让Oracle把它们当成两条SQL语句)。
Ø WHERE子句中尽量使用变量,这样可以减少语句的解析时间。
例:
查询语句1:
SELECT COUNT(*) FROM T_ORDER
WHERE ORDER_NO>10;
查询语句2:
SELECT COUNT(*) FROM T_ORDER
WHERE ORDER_NO>5;
以上两条SQL语句被Oracle当成两条独立的语句,会耗费额外的解析时间。解决方法是使用变量代替常量。
SELECT COUNT(*) FROM T_ORDER
WHERE ORDER_NO>:a;
Ø Oracle的关键字(如内置函数名、SQL保留字大写等)要大写,其他的字符都用小写。这样做的目的也是为了防止产生两条不同的SQL语句。
LIKE用于模糊查询,如果百分号前面有字符,将会用到索引。
SELECT * FROM tb_user WHERE fnm like 'Zhang%';
如果百分号在字符串的中间,将会利用索引。
SELECT * FROM tb_user WHERE fnm like 'Zh%g';
如果百分号前面和后面都有字符,不会用到索引。
SELECT * FROM tb_user WHERE fnm like '%Zhang%';
如果只有百分号后面有字符,将不会用到索引。
SELECT * FROM tb_user WHERE fnm like '%Zhang';
Ø 要避免使用结构复杂的视图,结构复杂的视图隐含查询性能问题。
Ø 如果可以,把查询结果放在一个中间表中,程序需要时,直接查询中间表,这是改善查询最有效的手段之一。
Ø 考虑使用物化视图(Materialized Views)代替结构复杂的查询。
Ø 减少对表的扫描,可以把多个扫描合并成一个扫描。
例:
查询语句1对表t_order进行了1次扫描。
SELECT COUNT (*)
FROM t_order
WHERE ORDER_NO < 1000;
查询语句2对表t_order进行了1次扫描。
SELECT COUNT (*)
FROM t_order
WHERE ORDER_NO BETWEEN 1000 AND 3000;
查询语句3对表t_order进行了1次扫描。
SELECT COUNT (*)
FROM t_order
WHERE ORDER_NO>3000;
以上3条SQL语句共对表t_order扫描了3次。
解决方法是合并SQL,把3次扫描变成1次扫描。
SELECT COUNT (CASE WHEN ORDER_NO < 1000
THEN 1 ELSE null END) cnt1,
COUNT (CASE WHEN ORDER_NO BETWEEN 1001 AND 3000
THEN 1 ELSE null END) cnt2,
COUNT (CASE WHEN ORDER_NO > 3000
THEN 1 ELSE null END) cnt3
FROM t_order;
Ø 使用带RETURNING子句的INSERT、UPDATE、DELETE语句完成选择数据和修改数据两个功能,减少对数据库的调用。
Ø 尽量让一条SQL语句完成更多的功能。
Ø 在WHERE子句中使用IS NULL或者NOT NULL将使索引失效。
例:在表tb_user_grant的列menu_id上创建索引。
CREATE INDEX kt2 ON tb_user_grant(menu_id);
虽然列上存在索引,但IS NULL使查询SQL语句不能够利用索引,在表tb_user_grant上执行的仍然是全表扫描。
SQL> SELECT * FROM tb_user_grant WHERE menu_id IS NULL and op_no='5454';
执行计划
----------------------------------------------------------
Plan hash value: 3624973521
-----------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 56 | 1344 | 171 (1)| 00:00:03 |
|* 1 | TABLE ACCESS FULL| TB_USER_GRANT | 56 | 1344 | 171 (1)| 00:00:03
Ø 不要在WHERE子句中使用连接符(||),连接符将忽略索引。
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/13804621/viewspace-683181/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/13804621/viewspace-683181/