索引关键特性
- 更新机制:索引在数据变更时自动同步更新,不是查询时才更新
- 类型对比:唯一索引比普通索引多一次唯一性校验
- NULL处理:MySQL允许多个NULL值,Oracle单列唯一索引只允许单个NULL
易错点解析
空表判断误区:
- 错误:使用COUNT(*)全表扫描
- 正确:使用EXISTS或LIMIT 1高效查询
NULL值查询:
- 错误:WHERE indexed_col=NULL
- 正确:WHERE indexed_col IS NULL
统计信息陷阱:
- NUM_ROWS是统计快照,非实时数据
- 依赖它判断空表可能导致误判
跨数据库差异
- MySQL:支持索引合并,不支持位图索引
- Oracle:支持位图索引和函数索引,索引组织表更成熟
- 函数索引实现方式不同:MySQL需虚拟列,Oracle原生支持
最佳实践
- 空表判断:使用EXISTS(SELECT 1 FROM table LIMIT 1)
- 索引选择:高并发选MySQL,数据仓库选Oracle
- 统计信息:定期收集但不可依赖其判断实时状态
SQL示例:分别使用 MySQL 和 Oracle 创建表,MySQL 插入数据建索引(自增主键、指定主键的区别,VARCHAR,VARCHAR2)
数据库索引与统计信息知识总结
本文整理了关于索引更新时机、索引类型对比、MySQL与Oracle索引差异,以及容易混淆的NUM_ROWS统计信息等核心知识点。
MySQL普通索引和唯一索引的区别
| 索引类型 | 关键字 | 特点 |
|---|---|---|
| 唯一索引 | UNIQUE INDEX |
不允许重复值,可快速去重 |
| 普通索引 | INDEX |
允许重复值,仅提高查询速度 |
索引更新时机对比
| 操作类型 | 普通索引更新时机 | 唯一索引更新时机 | 说明 |
|---|---|---|---|
| INSERT | 插入时立即更新 | 插入时立即更新 + 唯一性校验 | 唯一索引需额外检查是否冲突 |
| UPDATE | 更新时立即修改索引项 | 更新时立即修改 + 唯一性校验 | 更新索引列时触发 |
| DELETE | 删除时立即移除索引项 | 删除时立即移除索引项 | 两者无差异 |
核心结论
索引在数据变更时自动同步更新,不是查询时才更新。
唯一区别:唯一索引每次插入/更新前会多一次唯一性检查。
索引在数据变更时自动同步更新,不是查询时才更新。
使用索引判断空表查询的易错点
常见误区表格
| 错误做法 | 问题描述 | 正确做法 |
|---|---|---|
WHERE indexed_col = NULL |
索引不存储NULL值,无法命中 | 使用 IS NULL |
用 COUNT(*) 判断空表 |
全表扫描,性能差 | 使用 EXISTS 或 LIMIT 1 |
| 假设索引列全为NULL时索引有效 | 全NULL值时索引基本不使用 | 考虑添加 NOT NULL 约束 |
空表查询易错点详解
1. NULL 值与索引
sql
sql
-- ❌ 错误:索引无法使用,查不到任何记录(除非明确插入NULL)
SELECT * FROM actor WHERE first_name = NULL;
-- ✅ 正确:使用 IS NULL
SELECT * FROM actor WHERE first_name IS NULL;
| 数据库 | NULL在索引中的处理 |
|---|---|
| MySQL | 允许NULL,但NULL值聚集在一起,效率较低 |
| Oracle | 单列索引不存储全NULL值 |
| SQLite | NULL被视为最小值,存储在索引中 |
2. 判断空表的错误方式
sql
sql
-- ❌ 错误方式1:COUNT(*) 会全表扫描
SELECT COUNT(*) FROM actor; -- 返回0时为空表
-- ❌ 错误方式2:虽然用了索引,但仍需扫描所有索引项
SELECT COUNT(actor_id) FROM actor;
-- ✅ 正确方式:使用 EXISTS 快速判断
SELECT EXISTS(SELECT 1 FROM actor LIMIT 1);
-- ✅ 或者用 LIMIT
SELECT 1 FROM actor LIMIT 1;
3. 性能对比
| 查询方式 | 空表时 | 非空表时 |
|---|---|---|
SELECT COUNT(*) |
全表/全索引扫描 | 全表/全索引扫描 |
SELECT EXISTS(... LIMIT 1) |
找一条即停 | 找一条即停 |
SELECT 1 LIMIT 1 |
扫描第一条 | 扫描第一条 |
索引使用的易错点总结表
| 易错场景 | 错误示例 | 正确示例 | 原因 |
|---|---|---|---|
| NULL判断 | col = NULL |
col IS NULL |
NULL是未知值,不能用=比较 |
| 不等于 | col != 'A' |
col > 'A' OR col < 'A' |
不等于通常无法使用索引 |
| 函数包裹 | WHERE DATE(col) = '2024-01-01' |
WHERE col BETWEEN '2024-01-01' AND '2024-01-01 23:59:59' |
函数会让索引失效 |
| 隐式转换 | WHERE col_int = '123' |
WHERE col_int = 123 |
类型不匹配时函数转换 |
| LIKE前缀通配 | LIKE '%abc' |
LIKE 'abc%' |
前缀通配无法用B-Tree索引 |
| OR条件 | WHERE a=1 OR b=2 |
UNION 或分别查 |
OR可能不走索引 |
统计信息判断空表的陷阱
陷阱场景 NUM_ROWS 实际值 真实表状态 判断结果 风险 从未收集统计信息 NULL有数据 ❌ 误判为空表 漏查数据 删除数据后未更新统计 1000空表 ❌ 漏判为非空表 无效扫描 统计信息过时 500已清空 ❌ 漏判为非空表 性能问题 统计信息过时 0已有新数据 ❌ 误判为空表 漏查数据
正确的空表判断方法对比
方法 实时性 准确度 性能 推荐场景 SELECT COUNT(*) FROM table✅ 实时 ✅ 精确 ❌ 差(全表扫描) 小表 EXISTS (SELECT 1 FROM table)✅ 实时 ✅ 精确 ✅ 好(一条即停) 推荐 SELECT 1 FROM table ROWNUM=1(Oracle)✅ 实时 ✅ 精确 ✅ 好 推荐 USER_TABLES.NUM_ROWS❌ 过时 ❌ 不准 ✅ 极快 ❌ 不推荐
快速判断空表的最佳实践
Oracle 判断空表(实时)
sql
-- ✅ 方法1:EXISTS(推荐)
SELECT CASE WHEN EXISTS (SELECT 1 FROM actor) THEN '非空' ELSE '空' END FROM DUAL;
-- ✅ 方法2:ROWNUM
SELECT COUNT(*) FROM (SELECT 1 FROM actor WHERE ROWNUM = 1);
-- ✅ 方法3:查询是否返回行
DECLARE
v_cnt NUMBER;
BEGIN
SELECT COUNT(*) INTO v_cnt FROM (SELECT 1 FROM actor WHERE ROWNUM = 1);
IF v_cnt = 0 THEN
DBMS_OUTPUT.PUT_LINE('空表');
ELSE
DBMS_OUTPUT.PUT_LINE('非空表');
END IF;
END;
/
MySQL 判断空表(实时)
sql
-- ✅ 方法1:EXISTS(推荐)
SELECT EXISTS(SELECT 1 FROM actor);
-- ✅ 方法2:LIMIT
SELECT 1 FROM actor LIMIT 1;
| 方案 | 性能 | 精确度 | 推荐场景 |
|---|---|---|---|
EXISTS |
最快 | 实时准确 | ✅ 判断空表 |
information_schema |
快 | 近似值 | 估算行数 |
COUNT(*) |
慢 | 精确 | 需要精确计数 |
统计信息的正确使用场景
| 场景 | 是否适用 NUM_ROWS | 解决方案 |
|---|---|---|
| 判断实时空表 | ❌ 不适用 | 用 EXISTS 或 LIMIT |
| 查询性能优化 | ✅ 适用 | 定期 GATHER_STATS |
| 估算表大小 | ✅ 适用 | 允许误差 |
| 执行计划生成 | ✅ 适用 | 需要新鲜统计信息 |
总结
核心教训:
NUM_ROWS是统计信息快照,不是实时数据判断空表唯一可靠的方法:
EXISTS (SELECT 1 FROM table)
| 问题 | 后果 | 解决方案 |
|---|---|---|
| 统计信息未更新 | 误判空表/非空表 | 用实时查询 + EXISTS |
依赖 NUM_ROWS=0 |
逻辑错误 | 定期收集统计信息 |
大表 COUNT(*) |
性能差 | 用 LIMIT/ROWNUM + 游标 |
MySQL 与 Oracle 索引差异性总结
| 对比维度 | MySQL | Oracle |
|---|---|---|
| 索引类型 | B-Tree、Hash、Full-text、Spatial | B-Tree、Bitmap、Function-Based、Domain |
| 位图索引 | ❌ 不支持 | ✅ 支持(适合低基数、DML少的场景) |
| 函数索引 | ✅ 支持(MySQL 8.0+需显式定义虚拟列) | ✅ 原生支持(直接创建) |
| 反向键索引 | ❌ 不支持 | ✅ 支持(减少索引热点) |
| 索引组织表 | ✅ 支持(InnoDB主键即为聚簇索引) | ✅ 支持(IOT - Index Organized Table) |
| 空值处理 | NULL值被存储(B-Tree中视为最小) | 单列索引不存储全NULL值 |
| 唯一约束与NULL | 允许多个NULL值 | 允许单个NULL值 |
| 索引创建语法 | CREATE INDEX idx ON t(col) |
CREATE INDEX idx ON t(col) LOCAL/UNIQUE |
| 分区索引 | 仅本地索引(分区表自动维护) | 支持本地索引 + 全局索引 |
| 不可见索引 | ✅ 支持(MySQL 8.0+) | ✅ 支持(19c+) |
| 索引合并 | ✅ Index Merge(多个索引组合使用) | ❌ 不支持(一个查询仅用一个索引) |
| 在线重建 | ❌ 使用 ALTER TABLE 重建(锁表时间较长) |
✅ ALTER INDEX ... REBUILD ONLINE |
| 监控使用情况 | 需开启 innodb_monitor 或 performance_schema |
❌ 无内置监控,需第三方工具 |
| 跳过扫描 | ❌ 不支持 | ✅ Index Skip Scan(MySQL 8.0.13+支持) |
| 空间索引 | R-Tree(MyISAM/InnoDB) | 空间索引(Oracle Spatial) |
关键差异详解
1. 空值处理差异
sql
sql
-- MySQL: 允许多个NULL
CREATE INDEX idx_name ON t(col);
INSERT INTO t(col) VALUES (NULL), (NULL); -- ✅ 成功
-- Oracle: 单列唯一索引不允许重复NULL
CREATE UNIQUE INDEX idx_name ON t(col);
INSERT INTO t(col) VALUES (NULL); -- 成功
INSERT INTO t(col) VALUES (NULL); -- ❌ ORA-0001: 违反唯一约束
2. 位图索引场景
sql
sql
-- Oracle 专有
CREATE BITMAP INDEX idx_gender ON employees(gender);
-- 适用场景:低基数(男/女)、读多写少
-- MySQL 替代方案:普通B-Tree索引(效果较差)
CREATE INDEX idx_gender ON employees(gender);
3. 函数索引差异
sql
sql
-- Oracle: 直接创建
CREATE INDEX idx_upper_name ON employees(UPPER(name));
-- MySQL 8.0+: 需要虚拟列
ALTER TABLE employees ADD COLUMN name_upper VARCHAR(45)
GENERATED ALWAYS AS (UPPER(name));
CREATE INDEX idx_upper_name ON employees(name_upper);
4. 索引合并 vs 单索引
sql
sql
-- MySQL: 可合并多个单列索引
SELECT * FROM t WHERE a=1 AND b=2;
-- 可能同时使用 idx_a 和 idx_b
-- Oracle: 只能用一个索引(需建复合索引)
-- 需创建 idx_a_b 复合索引
索引维护对比
| 操作 | MySQL | Oracle |
|---|---|---|
| 创建索引(在线) | ALTER TABLE ADD INDEX(InnoDB支持轻度锁) |
CREATE INDEX ONLINE(完全在线) |
| 查看索引 | SHOW INDEX FROM table |
DBA_INDEXES / USER_INDEXES |
| 索引监控 | performance_schema |
AWR 报告(需诊断包) |
| 索引重建 | OPTIMIZE TABLE(锁表) |
ALTER INDEX REBUILD |
选择建议
| 场景 | 推荐数据库 |
|---|---|
| 高并发Web应用 | MySQL(主键聚簇索引优势大) |
| 数据仓库/BITable | Oracle(位图索引、分析函数) |
| 函数频繁转换查询 | Oracle(函数索引原生支持) |
| 大表在线DDL | Oracle(ONLINE重建灵活) |
| 开源/成本敏感 | MySQL(免费、生态好) |
总结:索引在MySQL和Oracle中的差异
| 对比维度 | MySQL | Oracle |
|---|---|---|
| 位图索引 | ❌ 不支持 | ✅ 支持 |
| 函数索引 | ✅ 需虚拟列(8.0+) | ✅ 原生支持 |
| 空值处理(唯一索引) | 允许多个NULL | 允许单个NULL |
| 索引合并 | ✅ 支持 | ❌ 不支持 |
| 在线重建 | ❌ 锁表时间较长 | ✅ 支持ONLINE |
| 跳过扫描 | 8.0.13+支持 | ✅ 支持 |
NUM_ROWS 不是行号
这是一个非常常见的语义误解。
NUM_ROWS 正确含义
| 术语 | 实际含义 | 举例 |
|---|---|---|
| NUM_ROWS | 表的行数(总记录数量) | NUM_ROWS = 1000 表示表中有 1000 条记录 |
| ROW_NUM / ROWNUM | 行号(记录的顺序编号) | ROWNUM = 1 表示查询结果中的第 1 行 |
命名来源解析
| 单词 | 含义 |
|---|---|
| NUM | Number(数量)的缩写 |
| ROWS | 行(复数) |
| NUM_ROWS = 行的数量 = 行数 ✅ |
| 容易混淆的词 | 正确含义 |
|---|---|
NUM_ROWS |
Number of Rows(行数) |
ROW_NUMBER |
行的编号(行号) |
实际对比示例
sql
sql
-- 假设表中有 3 条数据
-- NUM_ROWS(统计信息中的行数)
SELECT NUM_ROWS FROM USER_TABLES WHERE TABLE_NAME = 'ACTOR';
-- 结果:3 ← 这是表中总共有多少行
-- ROWNUM(Oracle 中的行号)
SELECT ROWNUM, first_name FROM actor WHERE ROWNUM <= 2;
-- 结果:
-- ROWNUM | first_name
-- 1 | PENELOPE ← 这是第 1 行的编号
-- 2 | NICK ← 这是第 2 行的编号
不同数据库中的对应关系
| 数据库 | 行数(记录数量) | 行号(记录编号) |
|---|---|---|
| Oracle | NUM_ROWS |
ROWNUM 或 ROW_NUMBER() |
| MySQL | TABLE_ROWS |
@rownum 变量 或 ROW_NUMBER() (8.0+) |
| PostgreSQL | reltuples |
ROW_NUMBER() |
| SQL Server | rows |
ROW_NUMBER() |
记忆技巧
| 词根 | 含义 | 例子 |
|---|---|---|
| NUM = Number(数量) | 统计有多少个 | NUM_ROWS = 总共有多少行 |
| ROW + NUMBER | 给每一行编个号 | ROW_NUMBER = 当前是第几行 |
一句话区分:
NUM_ROWS问你:"表里有多少行?" → 回答一个数字(如 1000)
ROW_NUMBER问你:"这是第几行?" → 回答一个序号(如 第 1 行)
您之前的困惑现在可以完全解开了
| 您之前的理解 | 正确理解 |
|---|---|
| 以为 NUM_ROWS 是行号 | ❌ NUM_ROWS 是行数(总记录数) |
| 疑惑为什么 NUM_ROWS 不准 | ✅ 它是统计信息的快照,不是实时的 |
| 混淆索引和统计信息的实时性 | ✅ 索引实时更新,NUM_ROWS 是过时快照 |
NUM_ROWS 是统计信息中的"总数",不是给每行编的"号码"
索引 vs NUM_ROWS(统计信息)- 易混淆点
这是最容易混淆的核心概念:
| 对比维度 | 索引 (Index) | NUM_ROWS (统计信息) |
|---|---|---|
| 更新时机 | 数据变更时实时自动更新 | 手动执行 GATHER_STATS 后才更新 |
| 实时性 | ✅ 强一致(与表数据同步) | ❌ 可能过时(是快照/缓存) |
| 维护者 | 数据库自动维护 | 需手动触发或定时任务 |
| 用途 | 加速查询、保证唯一性 | 优化器选择执行计划 |
| 能否反映空表 | 不能直接用于判断 | 不可靠(可能不准) |
| NULL值处理 | 一般存储NULL值 | NULL视为0行 |
核心区别一句话
索引是"同步实时更新"的,统计信息是"异步定期采样"的
典型混淆场景
| 场景 | 索引行为 | NUM_ROWS行为 | 容易产生的误解 |
|---|---|---|---|
| 插入100万行 | 立即更新100万条索引记录 | 仍是旧值(0或很小) | 以为表和索引都没数据 |
| 删除所有数据 | 立即删除所有索引项 | 可能还是显示100万 | 以为还有数据 |
查询 COUNT(*) |
可能走索引扫描 | 直接读取缓存值 | 误以为NUM_ROWS是实时的 |
验证示例
sql
-- 创建空表
CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(10));
-- 查看统计信息(NULL 或 0)
SELECT NUM_ROWS FROM USER_TABLES WHERE TABLE_NAME = 'T';
-- 插入数据
INSERT INTO t VALUES (1, 'a'), (2, 'b');
-- 此时索引已更新(可以用索引快速查到数据)
SELECT /*+ INDEX(t) */ * FROM t WHERE id = 2; -- ✅ 立即查到
-- 但统计信息未变(还是 NULL/0)
SELECT NUM_ROWS FROM USER_TABLES WHERE TABLE_NAME = 'T'; -- ❌ 还是旧值
-- 只有手动收集后才会更新
EXEC DBMS_STATS.GATHER_TABLE_STATS('SCOTT', 'T');
SELECT NUM_ROWS FROM USER_TABLES WHERE TABLE_NAME = 'T'; -- 现在显示 2
记忆口诀
| 对象 | 记忆点 |
|---|---|
| 索引 | "数据变我就变,实时响应" |
| NUM_ROWS | "你不叫我我不变,是个懒汉" |
索引 = 实时镜像
统计信息 = 过期快照
正确使用场景
| 需求 | 用什么 |
|---|---|
| 判断空表 | EXISTS + 索引(实时) |
| 加速查询 | 索引 |
| 优化器选计划 | 统计信息 |
| 估算表大小 | 统计信息 |
| 强制数据一致性 | 唯一索引 |
总结:索引 vs NUM_ROWS
| 对比维度 | 索引 | NUM_ROWS(统计信息) |
|---|---|---|
| 更新时机 | 数据变更时实时自动更新 | 需手动GATHER_STATS |
| 实时性 | ✅ 强一致 | ❌ 可能过时(快照) |
| 维护者 | 数据库自动 | 需手动或定时任务 |
| 用途 | 加速查询、保证唯一性 | 优化器选择执行计划 |
不同数据库中NUM_ROWS的对应名称
NUM_ROWS并不是 Oracle 独有的概念,几乎所有主流数据库都有类似的机制,只是在不同的数据库中,存放这个信息的"视图"或"系统表"名字不同。
下表展示了
NUM_ROWS在不同数据库中的对应名称和特性:
数据库 对应视图/属性 说明与实时性 Oracle USER_TABLES.NUM_ROWSALL_TABLES.NUM_ROWS典型代表。通过 ANALYZE或DBMS_STATS收集,是过时的快照数据。MySQL INFORMATION_SCHEMA.TABLES.TABLE_ROWS注意 :对于 InnoDB 引擎,该值是估算值,与实际值可能相差 40-50%。 PostgreSQL pg_class.reltuples由 ANALYZE命令更新,是一个估算值,并非实时精确数据。SQL Server sys.partitions.rows虽然名称不同,但本质相同。它也依赖于统计信息,在你查看的瞬间是准的,但不会立即随数据变化。 其他 (如MariaDB) INFORMATION_SCHEMA.TABLES.TABLE_ROWS与 MySQL、Oracle 逻辑类似,均为基于统计信息的估算值,非实时精确。 💡 核心回顾:为什么它会"不准确"?
正如你在使用中发现的那样,
NUM_ROWS容易让人混淆。这主要是因为:
设计目的 :它是专门为数据库的查询优化器服务的。优化器只需要知道表的大概大小来选择执行计划(例如,是小表全表扫描好,还是大表走索引好),并不需要一个实时的精确数字。
更新机制 :为了性能,数据库不会在每次数据变化时都去更新这个统计信息。它是在特定时刻(如执行
ANALYZE或自动维护任务时)拍的一张快照。拍完照之后,数据继续增加或删除,但快照里的数字不会变,需要下次拍照才会更新。✅ 如何正确获取实时行数
要获取绝对精确的、实时的行数,所有数据库都遵循同一个做法:执行
COUNT(*)查询。不过,
COUNT(*)会全表扫描,对于大表性能较差。如果你只是想判断空表(就像你之前提到的场景),最推荐的高效方法是使用EXISTS:sql
sql-- 适用于 Oracle / PostgreSQL / SQL Server 等 SELECT CASE WHEN EXISTS (SELECT 1 FROM your_table) THEN '非空' ELSE '空' END FROM DUAL;sql
sql-- 适用于 MySQL SELECT EXISTS (SELECT 1 FROM your_table);掌握它们各自的名称和"估算"这一共性,是避免未来在数据库行数统计上踩坑的关键。
| 数据库 | 对应视图/属性 | 实时性 |
|---|---|---|
| Oracle | USER_TABLES.NUM_ROWS |
过时快照 |
| MySQL | INFORMATION_SCHEMA.TABLES.TABLE_ROWS |
估算值 |
| PostgreSQL | pg_class.reltuples |
估算值 |
| SQL Server | sys.partitions.rows |
基于统计信息 |
判断空表的正确方法
sql
sql
-- Oracle(推荐)
SELECT CASE WHEN EXISTS (SELECT 1 FROM actor) THEN '非空' ELSE '空' END FROM DUAL;
-- MySQL(推荐)
SELECT EXISTS(SELECT 1 FROM actor);
关键SQL语句速查
创建索引
sql
sql
-- 唯一索引
CREATE UNIQUE INDEX uniq_idx_firstname ON actor(first_name);
-- 普通索引
CREATE INDEX idx_lastname ON actor(last_name);
批量插入(忽略重复)
sql
sql
INSERT IGNORE INTO actor (actor_id, first_name, last_name, last_update)
VALUES (3, 'ED', 'CHASE', '2006-02-15 12:34:33');
创建表并从另一表导入数据
sql
sql
CREATE TABLE actor_name AS
SELECT first_name, last_name FROM actor;
总结
| 易错点 | 正确理解 |
|---|---|
| 索引查询时才更新 | ❌ 索引是数据变更时实时更新 |
| NUM_ROWS实时准确 | ❌ 是统计信息快照,可能过时 |
| VARCHAR2跨数据库通用 | ❌ 仅Oracle特有 |
| 自增主键优于指定主键 | ⚠️ 按场景选择 |
| 唯一索引性能优于普通索引 | ⚠️ 多一次唯一性校验 |
| 用COUNT(*)判断空表 | ✅ 用EXISTS更高效 |