查询访问一个表的两个索引

原文地址:http://yangtingkun.itpub.net/post/468/499769

经常看到有人提出这样的疑问,我在同一张表上建立了多个索引,为什么Oracle每次
都选择一个,而不能同时利用多个索引呢。一般来说,常见的访问同一张表的两个以
上索引,存在三种情况,AND-EQUAL、INDEX HASH JOIN和BITMAP INDEX AND/OR。

此外,还有一个设计上的疑问,如果有A、B、C三个字段,都可能作为查询条件,是
建立多个复合索引好,还是建立三个单列的索引。这个问题之所以不好回答是因为
和业务或者说和查询的模式有很大的关系,不过如果理解了Oracle什么时候会选择
一个以上的索引来访问表,就会对于理解如何设计合理的索引有很大的帮助。

简单介绍一下AND-EQUAL执行计划。
首先建立一个测试表:

SQL> CREATE TABLE T_DOUBLE_IND 
2 (ID NUMBER, 
3 NAME VARCHAR2(30), 
4 TYPE VARCHAR2(30), 
5 CONTENTS VARCHAR2(4000));

Table created.

SQL> INSERT INTO T_DOUBLE_IND
2 SELECT ROWNUM, 
3 OBJECT_NAME, 
4 OBJECT_TYPE, 
5 LPAD('A', 1000, 'A')
6 FROM DBA_OBJECTS;

75856 rows created.

SQL> COMMIT;

Commit complete.

SQL> CREATE INDEX IND_DOUBLE_NAME
2 ON T_DOUBLE_IND (NAME);

Index created.

SQL> CREATE INDEX IND_DOUBLE_TYPE
2 ON T_DOUBLE_IND (TYPE);

Index created.

现在建立了一个测试表和两个索引,下面首先来看看AND-EQUAL执行方式:

SQL> SELECT ID, NAME, TYPE
2 FROM T_DOUBLE_IND
3 WHERE NAME = 'T_DOUBLE_IND'
4 AND TYPE = 'TABLE';

ID NAME TYPE
---------- ------------------------------ ------------------------------
75688 T_DOUBLE_IND TABLE


Execution Plan
----------------------------------------------------------
Plan hash value: 474554719

-------------------------------------------------------
| Id | Operation | Name |
-------------------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | TABLE ACCESS BY INDEX ROWID| T_DOUBLE_IND |
| 2 | AND-EQUAL | |
|* 3 | INDEX RANGE SCAN | IND_DOUBLE_NAME |
|* 4 | INDEX RANGE SCAN | IND_DOUBLE_TYPE |
-------------------------------------------------------

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

3 - access("NAME"='T_DOUBLE_IND')
4 - access("TYPE"='TABLE')

Note
-----
- rule based optimizer used (consider using cbo)

由于指定了两个列,且两个列上都包含索引,Oracle选择了扫描两个索引,并
使用了AND-EQUAL执行计划。这种扫描方式是分别通过两个索引获取索引键值
对应的ROWID,然后合并两个扫描中相等的ROWID,并通过这个ROWID来扫描表。

不过观察执行计划可以发现,当前的优化模式是RBO,如果收集一下索引,
再次运行同样的查询:

SQL> SHOW PARAMETER OPTIMIZER_MODE

NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
optimizer_mode string CHOOSE
SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'T_DOUBLE_IND')

PL/SQL procedure successfully completed.

SQL> SELECT ID, NAME, TYPE
2 FROM T_DOUBLE_IND
3 WHERE NAME = 'T_DOUBLE_IND'
4 AND TYPE = 'TABLE';

ID NAME TYPE
---------- ------------------------------ ------------------------------
75688 T_DOUBLE_IND TABLE


Execution Plan
----------------------------------------------------------
Plan hash value: 3887138334

--------------------------------------------------------------------------------------------
| Id| Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------------
| 0| SELECT STATEMENT | | 1 | 37 | 3 (0)| 00:00:01 |
|* 1| TABLE ACCESS BY INDEX ROWID| T_DOUBLE_IND | 1 | 37 | 3 (0)| 00:00:01 |
|* 2| INDEX RANGE SCAN | IND_DOUBLE_NAME | 2 | | 1 (0)| 00:00:01 |
--------------------------------------------------------------------------------------------

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

1 - filter("TYPE"='TABLE')
2 - access("NAME"='T_DOUBLE_IND')

可以看到,收集统计信息后,优化模式变为CBO,但是Oracle只选择了一个
索引进行扫描。对于当前的情况,NAME列的选择度非常高,因此这种方式
的代价最低。

SQL> SELECT /*+ INDEX(A IND_DOUBLE_NAME) INDEX(A IND_DOUBLE_TYPE) */
2 ID, NAME, TYPE
3 FROM T_DOUBLE_IND
4 WHERE NAME = 'T_DOUBLE_IND'
5 AND TYPE = 'TABLE';

ID NAME TYPE
---------- ------------------------------ ------------------------------
75689 T_DOUBLE_IND TABLE


Execution Plan
----------------------------------------------------------
Plan hash value: 3887138334

--------------------------------------------------------------------------------------------
| Id| Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 37 | 3 (0)| 00:00:01 |
|*1 | TABLE ACCESS BY INDEX ROWID| T_DOUBLE_IND | 1 | 37 | 3 (0)| 00:00:01 |
|*2 | INDEX RANGE SCAN | IND_DOUBLE_NAME | 2 | | 1 (0)| 00:00:01 |
--------------------------------------------------------------------------------------------

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

1 - filter("TYPE"='TABLE')
2 - access("NAME"='T_DOUBLE_IND')

即使通过HINT指定两个索引,CBO优化器也只是会选择其中一个索引来进行扫描,
而自动忽略另一个选择度低的索引。只有使用AND_EQUAL提示,才能在CBO的情况
下使用AND-EQUAL执行计划:

SQL> SELECT /*+ AND_EQUAL(A IND_DOUBLE_NAME IND_DOUBLE_TYPE) */ ID, NAME, TYPE
2 FROM T_DOUBLE_IND A
3 WHERE NAME = 'T_DOUBLE_IND'
4 AND TYPE = 'TABLE';

Execution Plan
----------------------------------------------------------
Plan hash value: 474554719

--------------------------------------------------------------------------------------------
| Id| Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 37 | 21 (0)| 00:00:01 |
|*1 | TABLE ACCESS BY INDEX ROWID| T_DOUBLE_IND | 1 | 37 | 21 (0)| 00:00:01 |
| 2 | AND-EQUAL | | | | | |
|*3 | INDEX RANGE SCAN | IND_DOUBLE_NAME | 2 | | 1 (0)| 00:00:01 |
|*4 | INDEX RANGE SCAN | IND_DOUBLE_TYPE | 9286 | | 20 (0)| 00:00:01 |
--------------------------------------------------------------------------------------------

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

1 - filter("NAME"='T_DOUBLE_IND' AND "TYPE"='TABLE')
3 - access("NAME"='T_DOUBLE_IND')
4 - access("TYPE"='TABLE')

在CBO下,Oracle不会自动选择AND-EQUAL执行计划,这是因为Oracle可以根据直方图来
判断一个列上各个值的选择度,但是从统计信息无法获得两个不同的列合AND-EQUAL之
后的选择度,这个操作后,如果得到的ROWID记录很少,那么这个查询的效率就会很高,
如果AND-EQUAL后得到大量的ROWID,那么查询的效率就会很差,这时应该选择全表扫描
或其他的执行计划。由于CBO都是根据统计信息分析得到的结果,而AND-EQUAL的结果对
于CBO是未知的,因此CBO不会选择AND-EQAUL这种扫描方式。

如果要Oracle获得这种统计信息,最简单的方法就是建立一个复合索引,Oracle在分析
索引列的时候自动会分析两个列的组合情况,从而在查询的时候可以准确的判断是否应
该使用这个复合索引。

SQL> SET AUTOT OFF
SQL> CREATE INDEX IND_DOU_NAM_TYP 
2 ON T_DOUBLE_IND
3 (NAME, TYPE);

Index created.

SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'T_DOUBLE_IND')

PL/SQL procedure successfully completed.

SQL> SET AUTOT TRACE EXP
SQL> SELECT ID, NAME, TYPE
2 FROM T_DOUBLE_IND
3 WHERE NAME = 'T_DOUBLE_IND'
4 AND TYPE = 'TABLE';

Execution Plan
----------------------------------------------------------
Plan hash value: 1808990274

--------------------------------------------------------------------------------------------
| Id| Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 2 | 74 | 3 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| T_DOUBLE_IND | 2 | 74 | 3 (0)| 00:00:01 |
|*2 | INDEX RANGE SCAN | IND_DOU_NAM_TYP | 2 | | 1 (0)| 00:00:01 |
--------------------------------------------------------------------------------------------

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

2 - access("NAME"='T_DOUBLE_IND' AND "TYPE"='TABLE')

SQL> SET AUTOT OFF
SQL> UPDATE T_DOUBLE_IND
2 SET NAME = 'T_DOUBLE_IND'
3 WHERE ID <= 20000;

20000 rows updated.

SQL> COMMIT;

Commit complete.

SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'T_DOUBLE_IND')

PL/SQL procedure successfully completed.

SQL> SET AUTOT TRACE EXP
SQL> SELECT ID, NAME, TYPE
2 FROM T_DOUBLE_IND
3 WHERE NAME = 'T_DOUBLE_IND'
4 AND TYPE = 'TABLE';

Execution Plan
----------------------------------------------------------
Plan hash value: 1808990274

--------------------------------------------------------------------------------------------
| Id| Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 2277 | 77418 | 1225 (1)| 00:00:18 |
| 1 | TABLE ACCESS BY INDEX ROWID| T_DOUBLE_IND | 2277 | 77418 | 1225 (1)| 00:00:18 |
|*2 | INDEX RANGE SCAN | IND_DOU_NAM_TYP | 2277 | | 11 (0)| 00:00:01 |
--------------------------------------------------------------------------------------------

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

2 - access("NAME"='T_DOUBLE_IND' AND "TYPE"='TABLE')

SQL> SET AUTOT OFF
SQL> UPDATE T_DOUBLE_IND
2 SET TYPE = 'TABLE'
3 WHERE ID <= 20000;

20000 rows updated.

SQL> COMMIT;

Commit complete.

SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'T_DOUBLE_IND')

PL/SQL procedure successfully completed.

SQL> SET AUTOT TRACE EXP
SQL> SELECT ID, NAME, TYPE
2 FROM T_DOUBLE_IND
3 WHERE NAME = 'T_DOUBLE_IND'
4 AND TYPE = 'TABLE';

Execution Plan
----------------------------------------------------------
Plan hash value: 4134729579

----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 7778 | 258K| 2010 (1)| 00:00:29 |
|* 1 | TABLE ACCESS FULL| T_DOUBLE_IND | 7778 | 258K| 2010 (1)| 00:00:29 |
----------------------------------------------------------------------------------

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

1 - filter("NAME"='T_DOUBLE_IND' AND "TYPE"='TABLE')

在建立复合索引后,对于刚才的查询,Oracle会自动选择复合索引。即使将表中
的20000条记录全部更新为NAME=’T_DOUBLE_BIN’,由于TYPE列的选项条件,在加
上索引相对于表来说要窄得多,Oracle仍然选择了复合索引扫描。

如果将TYPE列也更新20000条记录,使得NAME=’T_DOUBLE_IND’和TYPE=’TABLE’的
选择性很差,这时Oracle就会自动选择全表扫描来代替索引扫描。

<think>首先,用户的问题是关于数据库的聚集索引的限制,具体是“一个是否可以有多个聚集索引”。我需要根据系统级指令来构建我的回答。 系统级指令的关键点: - 所有行内数学达式使用$...$格式。 - 独立公式使用$$...$$并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如[^1]。 用户的问题是关于聚集索引的,所以我需要参考提供的引用内容。引用[1]和引用[2]讨论了聚集索引和非聚集索引的区别。 从引用[1]: - “一个只能创建一个聚集索引” - “叶子节点就是对应的数据全部列的值数据行的物理顺序与列值的顺序相同” 从引用[2]: - “一个只能创建一个聚集索引” - “聚集索引查询效率要比非聚集索引的效率要高,但是如果需要频繁去改变聚集索引的值,写入性能并不高” 所以,核心点是:一个只能有一个聚集索引。 我的回答需要: 1. 结构清晰,逐步解决问题。 2. 确保回答真实可靠。 3. 使用中文。 4. 生成相关问题。 5. 在引用段落末尾添加引用标识。 回答结构: - 先直接回答问题:一个不能有多个聚集索引。 - 解释原因:因为聚集索引决定了数据的物理存储顺序。 - 引用提供的引用内容来支持。 - 最后生成相关问题。 由于没有数学达式,我可能不需要使用LaTeX,但如果有机会,我应该包括一些。用户的问题是关于数据库的,可能不涉及数学,但如果有相关公式,我可以添加。例如,索引效率或查询复杂度,但引用中没有提到数学公式,所以我应该避免强制添加。 生成相关问题: - 基于主题,如聚集索引的优缺点、非聚集索引、如何选择索引等。 - 类似示例中的“相关问题”部分。 最终回答格式: - 以自然语言开头。 - 必要时包括代码块(如SQL示例)。 - 添加相关问题。 现在,构建回答: - 开头:直接回答用户问题。 - 解释:为什么只能有一个聚集索引。 - 引用:自然地引用[^1]和[^2]。 - 相关问题:在最后添加“相关问题”部分。 回答内容: - 用户问:一个是否可以有多个聚集索引? - 答案:否,一个只能有一个聚集索引。 - 原因:聚集索引决定了中数据的物理存储顺序。每个的数据只能按一种方式物理排序,因此只能有一个聚集索引。 - 引用[^1]:指出“一个只能创建一个聚集索引”。 - 引用[^2]:强调“聚集索引的叶子节点存储了实际数据行,物理顺序与索引顺序相同”。 最后,生成相关问题,例如: 1. 聚集索引和非聚集索引有什么区别? 2. 如何优化聚集索引的性能? 3. 在什么情况下应该使用聚集索引? 确保回答是中文。</think>### 聚集索引的限制:一个是否可以有多个聚集索引? 在数据库中,聚集索引(Clustered Index)是一种特殊的索引类型,它直接决定了中数据行的物理存储顺序。关于您的问题“一个是否可以有多个聚集索引”,答案是**否定的**。一个只能有一个聚集索引。下面我将逐步解释原因、限制和实际影响,帮助您全面理解。 #### 1. **为什么只能有一个聚集索引?** - 聚集索引的核心特性是它重新组织数据行的物理存储顺序,使其与索引键的顺序一致。例如,如果在一个的 `id` 列上创建聚集索引,数据行会按照 `id` 值从小到大(或从大到小)物理存储在磁盘上。 - 由于数据行的物理位置只能有一种排序方式(例如,磁盘上的连续块),一个无法同时按多个列进行物理排序。这就像一本书的页码只能按一种顺序排列,无法同时按章节和标题两种方式物理编排[^1]。 - 引用内容支持:聚集索引的叶子节点存储了实际数据行的全部值,因此物理顺序与索引顺序必须完全一致,这限制了每个只能创建一个聚集索引[^1][^2]。 #### 2. **聚集索引的限制** - **唯一性限制**:每个最多只能有一个聚集索引。如果您尝试创建第二个,数据库系统(如 SQL Server 或 MySQL)会报错。 - **写入性能影响**:当聚集索引键的值被修改时(例如更新 `id` 列),数据行可能需要移动物理位置以维持顺序,这会导致较高的写入开销[^2]。 - **查询效率优势**:虽然只能有一个聚集索引,但它的查询效率通常高于非聚集索引,因为数据可以直接通过索引访问,无需二次查找。例如,查询 `WHERE id = 7` 只需一次磁盘读取[^2]。 - **与非聚集索引的区别**:非聚集索引(Non-clustered Index)可以有多个(例如,在 `name` 或 `email` 列上),因为它们不改变物理存储顺序,而是存储索引键值和指向数据行的指针。查询非聚集索引时,可能需要二次查找(先找指针,再找数据),效率较低[^1][^2]。 #### 3. **实际示例** 假设有一个用户 `users`,我们创建聚集索引在 `id` 列上: ```sql CREATE TABLE users ( id INT PRIMARY KEY, -- 主键默认创建聚集索引(在某些数据库系统中) name VARCHAR(50), email VARCHAR(50) ); CREATE CLUSTERED INDEX idx_users_id ON users (id); -- 显式创建聚集索引 ``` - 如果尝试添加第二个聚集索引(如 `CREATE CLUSTERED INDEX idx_name ON users (name);`),系统会拒绝并提示错误。 - 替代方案:您可以创建非聚集索引在 `name` 列上,以支持快速查询: ```sql CREATE NONCLUSTERED INDEX idx_users_name ON users (name); -- 允许创建多个非聚集索引 ``` #### 4. **最佳实践建议** - **选择聚集索引键**:优先用于频繁查询且值稳定的列(如主键 `id`),避免频繁更新以减少性能开销。 - **优化非聚集索引**:通过覆盖索引(Include 列)减少二次查找,提升查询速度。 - **何时使用**:聚集索引适合范围查询(如 `WHERE id BETWEEN 10 AND 20`),因为数据物理连续;非聚集索引更适合点查询或选择性高的列[^2]。 总之,一个只能有一个聚集索引,这是由数据的物理存储特性决定的。如果您需要支持多列查询,应利用非聚集索引或组合索引来平衡性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值