oracle的优化器共有3种:
a. rule (基于规则) b. cost (基于成本) c. choose (选择性)
设置缺省的优化器,可以通过对init.ora文件中optimizer_mode参数的各种声明,如rule,cost,choose,all_rows,first_rows . 你当然也在sql句级或是会话(session)级对其进行覆盖.
为了使用基于成本的优化器(cbo, cost-based optimizer) , 你必须经常运行analyze 命令,以增加数据库中的对象统计信息(object statistics)的准确性.
如果数据库的优化器模式设置为选择性(choose),那么实际的优化器模式将和是否运行过analyze命令有关. 如果table已经被analyze过, 优化器模式将自动成为cbo , 反之,数据库将采用rule形式的优化器.
在缺省情况下,oracle采用choose优化器, 为了避免那些不必要的全表扫描(full table scan) , 你必须尽量避免使用choose优化器,而直接采用基于规则或者基于成本的优化器.
2. 访问table的方式
oracle 采用两种访问表中记录的方式:
a. 全表扫描
全表扫描就是顺序地访问表中每条记录. oracle采用一次读入多个数据块(database block)的方式优化全表扫描.
b. 通过rowid访问表
你可以采用基于rowid的访问方式情况,提高访问表的效率, , rowid包含了表中记录的物理位置信息..oracle采用索引(index)实现了数据和存放数据的物理位置(rowid)之间的联系. 通常索引提供了快速访问rowid的方法,因此那些基于索引列的查询就可以得到性能上的提高.
3. 共享sql语句
为了不重复解析相同的sql语句,在第一次解析之后, oracle将sql语句存放在内存中.这块位于系统全局区域sga(system global area)的共享池(shared buffer pool)中的内存可以被所有的数据库用户共享. 因此,当你执行一个sql语句(有时被称为一个游标)时,如果它
和之前的执行过的语句完全相同, oracle就能很快获得已经被解析的语句以及最好的
执行路径. oracle的这个功能大大地提高了sql的执行性能并节省了内存的使用.
可惜的是oracle只对简单的表提供高速缓冲(cache buffering) ,这个功能并不适用于多表连接查询.
数据库管理员必须在init.ora中为这个区域设置合适的参数,当这个内存区域越大,就可以保留更多的语句,当然被共享的可能性也就越大了.
当你向oracle 提交一个sql语句,oracle会首先在这块内存中查找相同的语句.
这里需要注明的是,oracle对两者采取的是一种严格匹配,要达成共享,sql语句必须
完全相同(包括空格,换行等).
共享的语句必须满足三个条件:
a. 字符级的比较:
当前被执行的语句和共享池中的语句必须完全相同.
例如:
select * from emp;
和下列每一个都不同
select * from emp;
select * from emp;
select * from emp;
b. 两个语句所指的对象必须完全相同:
例如:
用户 对象名 如何访问
jack sal_limit private synonym
work_city public synonym
plant_detail public synonym
jill sal_limit private synonym
work_city public synonym
plant_detail table owner
考虑一下下列sql语句能否在这两个用户之间共享.
sql
能否共享
原因
select max(sal_cap) from sal_limit;
不能
每个用户都有一个private synonym - sal_limit , 它们是不同的对象
select count(*0 from work_city where sdesc like 'new%';
能
两个用户访问相同的对象public synonym - work_city
select a.sdesc,b.location from work_city a , plant_detail b where a.city_id = b.city_id
不能
用户jack 通过private synonym访问plant_detail 而jill 是表的所有者,对象不同.
c. 两个sql语句中必须使用相同的名字的绑定变量(bind variables)
例如:
第一组的两个sql语句是相同的(可以共享),而第二组中的两个语句是不同的(即使在运行时,赋于不同的绑定变量相同的值)
a.
select pin , name from people where pin = :blk1.pin;
select pin , name from people where pin = :blk1.pin;
b.
select pin , name from people where pin = :blk1.ot_ind;
select pin , name from people where pin = :blk1.ov_ind;
4. 选择最有效率的表名顺序(只在基于规则的优化器中有效)
oracle的解析器按照从右到左的顺序处理from子句中的表名,因此from子句中写在最后的表(基础表 driving table)将被最先处理. 在from子句中包含多个表的情况下,你必须选择记录条数最少的表作为基础表.当oracle处理多个表时, 会运用排序及合并的方式连接它们.首先,扫描第一个表(from子句中最后的那个表)并对记录进行派序,然后扫描第二个表(from子句中最后第二个表),最后将所有从第二个表中检索出的记录与第一个表中合适记录进行合并.
例如:
表 tab1 16,384 条记录
表 tab2 1 条记录
选择tab2作为基础表 (最好的方法)
select count(*) from tab1,tab2 执行时间0.96秒
选择tab2作为基础表 (不佳的方法)
select count(*) from tab2,tab1 执行时间26.09秒
如果有3个以上的表连接查询, 那就需要选择交叉表(intersection table)作为基础表, 交叉表是指那个被其他表所引用的表.
例如:
emp表描述了location表和category表的交集.
select *
from location l ,
category c,
emp e
where e.emp_no between 1000 and 2000
and e.cat_no = c.cat_no
and e.locn = l.locn
将比下列sql更有效率
select *
from emp e ,
location l ,
category c
where e.cat_no = c.cat_no
and e.locn = l.locn
and e.emp_no between 1000 and 2000
5. where子句中的连接顺序.
oracle采用自下而上的顺序解析where子句,根据这个原理,表之间的连接必须写在其他where条件之前, 那些可以过滤掉最大数量记录的条件必须写在where子句的末尾.
例如:
(低效,执行时间156.3秒)
select …
from emp e
where sal > 50000
and job = ‘manager’
and 25 < (select count(*) from emp
where mgr=e.empno);
(高效,执行时间10.6秒)
select …
from emp e
where 25 < (select count(*) from emp
where mgr=e.empno)
and sal > 50000
and job = ‘manager’;
6. select子句中避免使用 ‘ * ‘
当你想在select子句中列出所有的column时,使用动态sql列引用 ‘*’ 是一个方便的方法.不幸的是,这是一个非常低效的方法. 实际上,oracle在解析的过程中, 会将’*’ 依次转换成所有的列名, 这个工作是通过查询数据字典完成的, 这意味着将耗费更多的时间.
7. 减少访问数据库的次数
当执行每条sql语句时, oracle在内部执行了许多工作: 解析sql语句, 估算索引的利用率, 绑定变量 , 读数据块等等. 由此可见, 减少访问数据库的次数 , 就能实际上减少oracle的工作量.
例如,
以下有三种方法可以检索出雇员号等于0342或0291的职员.
方法1 (最低效)
select emp_name , salary , grade
from emp
where emp_no = 342;
select emp_name , salary , grade
from emp
where emp_no = 291;
方法2 (次低效)
declare
cursor c1 (e_no number) is
select emp_name,salary,grade
from emp
where emp_no = e_no;
begin
open c1(342);
fetch c1 into …,..,.. ;
…..
open c1(291);
fetch c1 into …,..,.. ;
close c1;
end;
方法3 (高效)
select a.emp_name , a.salary , a.grade,
b.emp_name , b.salary , b.grade
from emp a,emp b
where a.emp_no = 342
and b.emp_no = 291;
注意:
在sql*plus , sql*forms和pro*c中重新设置arraysize参数, 可以增加每次数据库访问的检索数据量 ,建议值为200
8. 使用decode函数来减少处理时间
使用decode函数可以避免重复扫描相同记录或重复连接相同的表.
例如:
select count(*),sum(sal)
from emp
where dept_no = 0020
and ename like ‘smith%’;
select count(*),sum(sal)
from emp
where dept_no = 0030
and ename like ‘smith%’;
你可以用decode函数高效地得到相同结果
select count(decode(dept_no,0020,’x’,null)) d0020_count,
count(decode(dept_no,0030,’x’,null)) d0030_count,
sum(decode(dept_no,0020,sal,null)) d0020_sal,
sum(decode(dept_no,0030,sal,null)) d0030_sal
from emp where ename like ‘smith%’;
类似的,decode函数也可以运用于group by 和order by子句中.
9. 整合简单,无关联的数据库访问
如果你有几个简单的数据库查询语句,你可以把它们整合到一个查询中(即使它们之间没有关系)
例如:
select name
from emp
where emp_no = 1234;
select name
from dpt
where dpt_no = 10 ;
select name
from cat
where cat_type = ‘rd’;
上面的3个查询可以被合并成一个:
select e.name , d.name , c.name
from cat c , dpt d , emp e,dual x
where nvl(‘x’,x.dummy) = nvl(‘x’,e.rowid(+))
and nvl(‘x’,x.dummy) = nvl(‘x’,d.rowid(+))
and nvl(‘x’,x.dummy) = nvl(‘x’,c.rowid(+))
and e.emp_no(+) = 1234
and d.dept_no(+) = 10
and c.cat_type(+) = ‘rd’;
(译者按: 虽然采取这种方法,效率得到提高,但是程序的可读性大大降低,所以读者 还是要权衡之间的利弊)
10. 删除重复记录
最高效的删除重复记录方法 ( 因为使用了rowid)
delete from emp e
where e.rowid > (select min(x.rowid)
from emp x
where x.emp_no = e.emp_no);
11. 用truncate替代delete
当删除表中的记录时,在通常情况下, 回滚段(rollback segments ) 用来存放可以被恢复的信息. 如果你没有commit事务,oracle会将数据恢复到删除之前的状态(准确地说是
恢复到执行删除命令之前的状况)
而当运用truncate时, 回滚段不再存放任何可被恢复的信息.当命令运行后,数据不能被恢复.因此很少的资源被调用,执行时间也会很短.
(译者按: truncate只在删除全表适用,truncate是ddl不是dml)
12. 尽量多使用commit
只要有可能,在程序中尽量多使用commit, 这样程序的性能得到提高,需求也会因为commit所释放的资源而减少:
commit所释放的资源:
a. 回滚段上用于恢复数据的信息.
b. 被程序语句获得的锁
c. redo log buffer 中的空间
d. oracle为管理上述3种资源中的内部花费
(译者按: 在使用commit时必须要注意到事务的完整性,现实中效率和事务完整性往往是鱼和熊掌不可得兼)
13. 计算记录条数
和一般的观点相反, count(*) 比count(1)稍快 , 当然如果可以通过索引检索,对索引列的计数仍旧是最快的. 例如 count(empno)
(译者按: 在csdn论坛中,曾经对此有过相当热烈的讨论, 作者的观点并不十分准确,通过实际的测试,上述三种方法并没有显著的性能差别)
14. 用where子句替换having子句
避免使用having子句, having 只会在检索出所有记录之后才对结果集进行过滤. 这个处理需要排序,总计等操作. 如果能通过where子句限制记录的数目,那就能减少这方面的开销.
例如:
低效:
select region,avg(log_size)
from location
group by region
having region region != ‘sydney’
and region != ‘perth’
高效
select region,avg(log_size)
from location
where region region != ‘sydney’
and region != ‘perth’
group by region
(译者按: having 中的条件一般用于对一些集合函数的比较,如count() 等等. 除此而外,一般的条件应该写在where子句中)
15. 减少对表的查询
在含有子查询的sql语句中,要特别注意减少对表的查询.
例如:
低效
select tab_name
from tables
where tab_name = ( select tab_name
from tab_columns
where version = 604)
and db_ver= ( select db_ver
from tab_columns
where version = 604)
高效
select tab_name
from tables
where (tab_name,db_ver)
= ( select tab_name,db_ver)
from tab_columns
where version = 604)
update 多个column 例子:
低效:
update emp
set emp_cat = (select max(category) from emp_categories),
sal_range = (select max(sal_range) from emp_categories)
where emp_dept = 0020;
高效:
update emp
set (emp_cat, sal_range)
= (select max(category) , max(sal_range)
from emp_categories)
where emp_dept = 0020;
16. 通过内部函数提高sql效率.
select h.empno,e.ename,h.hist_type,t.type_desc,count(*)
from history_type t,emp e,emp_history h
where h.empno = e.empno
and h.hist_type = t.hist_type
group by h.empno,e.ename,h.hist_type,t.type_desc;
通过调用下面的函数可以提高效率.
function lookup_hist_type(typ in number) return varchar2
as
tdesc varchar2(30);
cursor c1 is
select type_desc
from history_type
where hist_type = typ;
begin
open c1;
fetch c1 into tdesc;
close c1;
return (nvl(tdesc,’?’));
end;
function lookup_emp(emp in number) return varchar2
as
ename varchar2(30);
cursor c1 is
select ename
from emp
where empno=emp;
begin
open c1;
fetch c1 into ename;
close c1;
return (nvl(ename,’?’));
end;
select h.empno,lookup_emp(h.empno),
h.hist_type,lookup_hist_type(h.hist_type),count(*)
from emp_history h
group by h.empno , h.hist_type;
(译者按: 经常在论坛中看到如 ’能不能用一个sql写出….’ 的贴子, 殊不知复杂的sql往往牺牲了执行效率. 能够掌握上面的运用函数解决问题的方法在实际工作中是非常有意义的)
17. 使用表的别名(alias)
当在sql语句中连接多个表时, 请使用表的别名并把别名前缀于每个column上.这样一来,就可以减少解析的时间并减少那些由column歧义引起的语法错误.
(译者注: column歧义指的是由于sql中不同的表具有相同的column名,当sql语句中出现这个column时,sql解析器无法判断这个column的归属)
18. 用exists替代in
在许多基于基础表的查询中,为了满足一个条件,往往需要对另一个表进行联接.在这种情况下, 使用exists(或not exists)通常将提高查询的效率.
低效:
select *
from emp (基础表)
where empno > 0
and deptno in (select deptno
from dept
where loc = ‘melb’)
高效:
select *
from emp (基础表)
where empno > 0
and exists (select ‘x’
from dept
where dept.deptno = emp.deptno
and loc = ‘melb’)
(译者按: 相对来说,用not exists替换not in 将更显著地提高效率,下一节中将指出)
19. 用not exists替代not in
在子查询中,not in子句将执行一个内部的排序和合并. 无论在哪种情况下,not in都是最低效的 (因为它对子查询中的表执行了一个全表遍历). 为了避免使用not in ,我们可以把它改写成外连接(outer joins)或not exists.
例如:
select …
from emp
where dept_no not in (select dept_no
from dept
where dept_cat=’a’);
为了提高效率.改写为:
(方法一: 高效)
select ….
from emp a,dept b
where a.dept_no = b.dept(+)
and b.dept_no is null
and b.dept_cat(+) = ‘a’
(方法二: 最高效)
select ….
from emp e
where not exists (select ‘x’
from dept d
where d.dept_no = e.dept_no
and dept_cat = ‘a’);
20. 用表连接替换exists
通常来说 , 采用表连接的方式比exists更有效率
select ename
from emp e
where exists (select ‘x’
from dept
where dept_no = e.dept_no
and dept_cat = ‘a’);
(更高效)
select ename
from dept d,emp e
where e.dept_no = d.dept_no
and dept_cat = ‘a’ ;
(译者按: 在rbo的情况下,前者的执行路径包括filter,后者使用nested loop)
21. 用exists替换distinct
当提交一个包含一对多表信息(比如部门表和雇员表)的查询时,避免在select子句中使用distinct. 一般可以考虑用exist替换
例如:
低效:
select distinct dept_no,dept_name
from dept d,emp e
where d.dept_no = e.dept_no
高效:
select dept_no,dept_name
from dept d
where exists ( select ‘x’
from emp e
where e.dept_no = d.dept_no);
exists 使查询更为迅速,因为rdbms核心模块将在子查询的条件一旦满足后,立刻返回结果.
22. 识别’低效执行’的sql语句
用下列sql工具找出低效sql:
select executions , disk_reads, buffer_gets,
round((buffer_gets-disk_reads)/buffer_gets,2) hit_radio,
round(disk_reads/executions,2) reads_per_run,
sql_text
from v$sqlarea
where executions>0
and buffer_gets > 0
and (buffer_gets-disk_reads)/buffer_gets < 0.8
order by 4 desc;
(译者按: 虽然目前各种关于sql优化的图形化工具层出不穷,但是写出自己的sql工具来解决问题始终是一个最好的方法)
23. 使用tkprof 工具来查询sql性能状态
sql trace 工具收集正在执行的sql的性能状态数据并记录到一个跟踪文件中. 这个跟踪文件提供了许多有用的信息,例如解析次数.执行次数,cpu使用时间等.这些数据将可以用来优化你的系统.
设置sql trace在会话级别: 有效
alter session set sql_trace true
设置sql trace 在整个数据库有效仿, 你必须将sql_trace参数在init.ora中设为true, user_dump_dest参数说明了生成跟踪文件的目录
(译者按: 这一节中,作者并没有提到tkprof的用法, 对sql trace的用法也不够准确, 设置sql trace首先要在init.ora中设定timed_statistics, 这样才能得到那些重要的时间状态. 生成的trace文件是不可读的,所以要用tkprof工具对其进行转换,tkprof有许多执行参数. 大家可以参考oracle手册来了解具体的配置. )
24. 用explain plan 分析sql语句
explain plan 是一个很好的分析sql语句的工具,它甚至可以在不执行sql的情况下分析语句. 通过分析,我们就可以知道oracle是怎么样连接表,使用什么方式扫描表(索引扫描或全表扫描)以及使用到的索引名称.
你需要按照从里到外,从上到下的次序解读分析的结果. explain plan分析的结果是用缩进的格式排列的, 最内部的操作将被最先解读, 如果两个操作处于同一层中,带有最小操作号的将被首先执行.
nested loop是少数不按照上述规则处理的操作, 正确的执行路径是检查对nested loop提供数据的操作,其中操作号最小的将被最先处理.
译者按:
通过实践, 感到还是用sqlplus中的set trace 功能比较方便.
举例:
sql> list
1 select *
2 from dept, emp
3* where emp.deptno = dept.deptno
sql> set autotrace traceonly /*traceonly 可以不显示执行结果*/
sql> /
14 rows selected.
execution plan
----------------------------------------------------------
0 select statement optimizer=choose
1 0 nested loops
2 1 table access (full) of 'emp'
3 1 table access (by index rowid) of 'dept'
4 3 index (unique scan) of 'pk_dept' (unique)
statistics
----------------------------------------------------------
0 recursive calls
2 db block gets
30 consistent gets
0 physical reads
0 redo size
2598 bytes sent via sql*net to client
503 bytes received via sql*net from client
2 sql*net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
14 rows processed
通过以上分析,可以得出实际的执行步骤是:
1. table access (full) of 'emp'
2. index (unique scan) of 'pk_dept' (unique)
3. table access (by index rowid) of 'dept'
4. nested loops (joining 1 and 3)
注: 目前许多第三方的工具如toad和oracle本身提供的工具如oms的sql analyze都提供了极其方便的explain plan工具.也许喜欢图形化界面的朋友们可以选用它们.
25. 用索引提高效率
索引是表的一个概念部分,用来提高检索数据的效率. 实际上,oracle使用了一个复杂的自平衡b-tree结构. 通常,通过索引查询数据比全表扫描要快. 当oracle找出执行查询和update语句的最佳路径时, oracle优化器将使用索引. 同样在联结多个表时使用索引也可以提高效率. 另一个使用索引的好处是,它提供了主键(primary key)的唯一性验证.
除了那些long或long raw数据类型, 你可以索引几乎所有的列. 通常, 在大型表中使用索引特别有效. 当然,你也会发现, 在扫描小表时,使用索引同样能提高效率.
虽然使用索引能得到查询效率的提高,但是我们也必须注意到它的代价. 索引需要空间来
存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时, 索引本身也会被修改. 这意味着每条记录的insert , delete , update将为此多付出4 , 5 次的磁盘i/o . 因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢.
译者按:
定期的重构索引是有必要的.
alter index <indexname> rebuild <tablespacename>
26. 索引的操作
oracle对索引有两种访问模式.
索引唯一扫描 ( index unique scan)
大多数情况下, 优化器通过where子句访问index.
例如:
表lodging有两个索引 : 建立在lodging列上的唯一性索引lodging_pk和建立在manager列上的非唯一性索引lodging$manager.
select *
from lodging
where lodging = ‘rose hill’;
在内部 , 上述sql将被分成两步执行, 首先 , lodging_pk 索引将通过索引唯一扫描的方式被访问 , 获得相对应的rowid, 通过rowid访问表的方式 执行下一步检索.
如果被检索返回的列包括在index列中,oracle将不执行第二步的处理(通过rowid访问表). 因为检索数据保存在索引中, 单单访问索引就可以完全满足查询结果.
下面sql只需要index unique scan 操作.
select lodging
from lodging
where lodging = ‘rose hill’;
索引范围查询(index range scan)
适用于两种情况:
1. 基于一个范围的检索
2. 基于非唯一性索引的检索
例1:
select lodging
from lodging
where lodging like ‘m%’;
where子句条件包括一系列值, oracle将通过索引范围查询的方式查询lodging_pk . 由于索引范围查询将返回一组值, 它的效率就要比索引唯一扫描
低一些.
例2:
select lodging
from lodging
where manager = ‘bill gates’;
这个sql的执行分两步, lodging$manager的索引范围查询(得到所有符合条件记录的rowid) 和下一步同过rowid访问表得到lodging列的值. 由于lodging$manager是一个非唯一性的索引,数据库不能对它执行索引唯一扫描.
由于sql返回lodging列,而它并不存在于lodging$manager索引中, 所以在索引范围查询后会执行一个通过rowid访问表的操作.
where子句中, 如果索引列所对应的值的第一个字符由通配符(wildcard)开始, 索引将不被采用.
select lodging
from lodging
where manager like ‘%hanman’;
在这种情况下,oracle将使用全表扫描.
27. 基础表的选择
基础表(driving table)是指被最先访问的表(通常以全表扫描的方式被访问). 根据优化器的不同, sql语句中基础表的选择是不一样的.
如果你使用的是cbo (cost based optimizer),优化器会检查sql语句中的每个表的物理大小,索引的状态,然后选用花费最低的执行路径.
如果你用rbo (rule based optimizer) , 并且所有的连接条件都有索引对应, 在这种情况下, 基础表就是from 子句中列在最后的那个表.
举例:
select a.name , b.manager
from worker a,
lodging b
where a.lodging = b.loding;
由于lodging表的loding列上有一个索引, 而且worker表中没有相比较的索引, worker表将被作为查询中的基础表.
28. 多个平等的索引
当sql语句的执行路径可以使用分布在多个表上的多个索引时, oracle会同时使用多个索引并在运行时对它们的记录进行合并, 检索出仅对全部索引有效的记录.
在oracle选择执行路径时,唯一性索引的等级高于非唯一性索引. 然而这个规则只有
当where子句中索引列和常量比较才有效.如果索引列和其他表的索引类相比较. 这种子句在优化器中的等级是非常低的.
如果不同表中两个想同等级的索引将被引用, from子句中表的顺序将决定哪个会被率先使用. from子句中最后的表的索引将有最高的优先级.
如果相同表中两个想同等级的索引将被引用, where子句中最先被引用的索引将有最高的优先级.
举例:
deptno上有一个非唯一性索引,emp_cat也有一个非唯一性索引.
select ename,
from emp
where dept_no = 20
and emp_cat = ‘a’;
这里,deptno索引将被最先检索,然后同emp_cat索引检索出的记录进行合并. 执行路径如下:
table access by rowid on emp
and-equal
index range scan on dept_idx
index range scan on cat_idx
29. 等式比较和范围比较
当where子句中有索引列, oracle不能合并它们,oracle将用范围比较.
举例:
deptno上有一个非唯一性索引,emp_cat也有一个非唯一性索引.
select ename
from emp
where deptno > 20
and emp_cat = ‘a’;
这里只有emp_cat索引被用到,然后所有的记录将逐条与deptno条件进行比较. 执行路径如下:
table access by rowid on emp
index range scan on cat_idx
30. 不明确的索引等级
当oracle无法判断索引的等级高低差别,优化器将只使用一个索引,它就是在where子句中被列在最前面的.
举例:
deptno上有一个非唯一性索引,emp_cat也有一个非唯一性索引.
select ename
from emp
where deptno > 20
and emp_cat > ‘a’;
这里, oracle只用到了dept_no索引. 执行路径如下:
table access by rowid on emp
index range scan on dept_idx
译者按:
我们来试一下以下这种情况:
sql> select index_name, uniqueness from user_indexes where table_name = 'emp';
index_name uniquenes
------------------------------ ---------
empno unique
emptype nonunique
sql> select * from emp where empno >= 2 and emp_type = 'a' ;
no rows selected
execution plan
----------------------------------------------------------
0 select statement optimizer=choose
1 0 table access (by index rowid) of 'emp'
2 1 index (range scan) of 'emptype' (non-unique)
虽然empno是唯一性索引,但是由于它所做的是范围比较, 等级要比非唯一性索引的等式比较低
最大的网站源码资源下载站,
新闻热点
疑难解答