MySQL、Oracle 数据库:唯一索引、普通索引、NUM_ROWS(行数)、ROW_NUM / ROWNUM(行号)

索引关键特性

  • 更新机制:索引在数据变更时自动同步更新,不是查询时才更新
  • 类型对比:唯一索引比普通索引多一次唯一性校验
  • NULL处理:MySQL允许多个NULL值,Oracle单列唯一索引只允许单个NULL

易错点解析

  1. 空表判断误区

    • 错误:使用COUNT(*)全表扫描
    • 正确:使用EXISTS或LIMIT 1高效查询
  2. NULL值查询

    • 错误:WHERE indexed_col=NULL
    • 正确:WHERE indexed_col IS NULL
  3. 统计信息陷阱

    • NUM_ROWS是统计快照,非实时数据
    • 依赖它判断空表可能导致误判

跨数据库差异

  • MySQL:支持索引合并,不支持位图索引
  • Oracle:支持位图索引和函数索引,索引组织表更成熟
  • 函数索引实现方式不同:MySQL需虚拟列,Oracle原生支持

最佳实践


Oracle 建表复制结构、精准排查并安全删除空表(附:DDL、DML、DQL、DCL)


数据库索引与统计信息知识总结

本文整理了关于索引更新时机、索引类型对比、MySQL与Oracle索引差异,以及容易混淆的NUM_ROWS统计信息等核心知识点。


MySQL普通索引和唯一索引的区别

索引类型 关键字 特点
唯一索引 UNIQUE INDEX 不允许重复值,可快速去重
普通索引 INDEX 允许重复值,仅提高查询速度

索引更新时机对比

操作类型 普通索引更新时机 唯一索引更新时机 说明
INSERT 插入时立即更新 插入时立即更新 + 唯一性校验 唯一索引需额外检查是否冲突
UPDATE 更新时立即修改索引项 更新时立即修改 + 唯一性校验 更新索引列时触发
DELETE 删除时立即移除索引项 删除时立即移除索引项 两者无差异

核心结论

索引在数据变更时自动同步更新,不是查询时才更新。

唯一区别:唯一索引每次插入/更新前会多一次唯一性检查。

索引在数据变更时自动同步更新,不是查询时才更新。


使用索引判断空表查询的易错点


常见误区表格

错误做法 问题描述 正确做法
WHERE indexed_col = NULL 索引不存储NULL值,无法命中 使用 IS NULL
COUNT(*) 判断空表 全表扫描,性能差 使用 EXISTSLIMIT 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 解决方案
判断实时空表 ❌ 不适用 EXISTSLIMIT
查询性能优化 ✅ 适用 定期 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_monitorperformance_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 ROWNUMROW_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_ROWS ALL_TABLES.NUM_ROWS 典型代表。通过 ANALYZEDBMS_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更高效
相关推荐
阿坤带你走近大数据1 小时前
OracleSQL优化案例-3
数据库·oracle·sql优化
空空潍1 小时前
MySQL存储引擎与索引深度解析
后端·sql·mysql·innodb
六月雨滴1 小时前
Oracle RAC 环境详解
数据库·oracle
ChoSeitaku1 小时前
13.MySQL使用C语言链接及图形化界面
数据库·mysql
HalvmånEver2 小时前
MySQL 使用 C 语言连接
linux·数据库·学习·mysql
YL200404262 小时前
MySQL-进阶篇-索引
数据库·mysql
xcLeigh2 小时前
KES数据库自动创建表空间目录,不用再提前建文件夹了
数据库·oracle·自动化·表空间·文件夹
辞旧 lekkk10 小时前
【Qt】信号和槽
linux·开发语言·数据库·qt·学习·mysql·萌新
笨蛋不要掉眼泪12 小时前
Mysql架构揭秘:update语句的执行流程
数据库·mysql·架构