慢 SQL 优化要通过执行计划确认数据库实际选择了什么访问路径,是全表扫描还是索引范围扫描,是否发生回表,过滤条件是否被下推,多表连接使用了 HASH JOIN 还是 NEST LOOP。确认问题后,再针对性调整索引、SQL 写法、统计信息或 Hint。
2. 测试环境与数据准备
实验使用 TEST_BIG 和 TEST_SMALL 两张表。TEST_BIG 初始 10000 行,TEST_SMALL 999 行。TEST_BIG 包含 ID、NAME、PHONE、
CREATE_TIME、STATUS、REMARK 等字段,并建立 STATUS、PHONE 两个普通索引。
CREATE TABLE TEST_BIG(
ID INT PRIMARY KEY,
NAME VARCHAR(100),
PHONE VARCHAR(20),
CREATE_TIME DATETIME,
STATUS INT,
REMARK VARCHAR(500)
);
-- 插入 10000 条数据
-- NAME 形如 NAME_1 ... NAME_10000
-- PHONE 形如 13800000001 ...
-- STATUS = MOD(ID,5)
CREATE INDEX IDX_TEST_STATUS ON TEST_BIG(STATUS);
CREATE INDEX IDX_TEST_PHONE ON TEST_BIG(PHONE);
CALL SP_TAB_STAT_INIT('SYSDBA','TEST_BIG');
CALL SP_TAB_INDEX_STAT_INIT('SYSDBA','TEST_BIG');
CREATE TABLE TEST_SMALL AS
SELECT ID,STATUS FROM TEST_BIG WHERE ID < 1000;
CALL SP_TAB_STAT_INIT('SYSDBA','TEST_SMALL');
CALL SP_TAB_INDEX_STAT_INIT('SYSDBA','TEST_SMALL');
EXPLAIN
SELECT * FROM TEST_BIG
WHERE NAME = 'NAME_8888';
SELECT * FROM TEST_BIG
WHERE NAME = 'NAME_8888';
执行计划从底层开始读取 TEST_BIG:首先出现 CSCN2,估算扫描 10000 行;随后 SLCT2 对 NAME='NAME_8888' 做过滤,优化器估算过滤后还有 250 行,最后通过 PRJT2 输出 7 个字段。
截图中的用时 3.044 毫秒是 EXPLAIN/命令执行阶段的耗时,不等于真实扫描 SQL 的稳定运行时间。判断优化效果应结合真实 SELECT 多次执行
CREATE INDEX IDX_TEST_NAME ON TEST_BIG(NAME);
CALL SP_TAB_STAT_INIT('SYSDBA','TEST_BIG');
CALL SP_TAB_INDEX_STAT_INIT('SYSDBA','TEST_BIG');
EXPLAIN
SELECT * FROM TEST_BIG
WHERE NAME = 'NAME_8888';
建立 IDX_TEST_NAME 后,计划发生了明显变化:原来的 CSCN2 全表扫描消失,底层变成 SSEK2,直接在 IDX_TEST_NAME 中定位 NAME_8888;由于 SELECT * 需要返回索引之外的 PHONE、CREATE_TIME、STATUS、REMARK 等列,所以还需要 BLKUP2 回表取完整记录。
这一实验说明:高选择性的等值查询列如果经常作为过滤条件,可以考虑建立索引。
PHONE 字段定义为 VARCHAR(20),并且已经存在 IDX_TEST_PHONE。如果查询时把手机号写成数字常量,字段与常量类型不一致,优化器需要进行隐式类型转换。
PHONE 是 VARCHAR,却使用数字常量
EXPLAIN
SELECT * FROM TEST_BIG
WHERE PHONE = 13800008888;
保持与字段类型一致
EXPLAIN
SELECT * FROM TEST_BIG
WHERE PHONE = '13800008888';
第一条 SQL 的 Predicate Information 中出现 exp_cast(TEST_BIG.PHONE),说明数据库需要对 PHONE 做隐式转换。此时底层使用 CSCN2 扫描 TEST_BIG,原本存在的 IDX_TEST_PHONE 没有被使用,优化器估算过滤后约 500 行。
第二条 SQL 把常量改成字符串后,隐式转换消失,执行计划变为 SSEK2 + BLKUP2,直接使用 IDX_TEST_PHONE 的 scan_range 定位目标手机号,估算结果行数下降到 1。
这类问题在业务 SQL 中非常常见。SQL 参数类型应尽量与数据库字段类型一致,否则即使已经建好索引,也可能因为对列做函数/转换而无法按索引范围定位。
TEST_BIG 的 STATUS 原本按 MOD(ID,5) 均匀分布。删除 STATUS=1 的数据后,表从 10000 行减少到约 8000 行,并且 STATUS=1 的真实数据已经为空。随后删除表和列统计信息,用来模拟优化器缺少可靠统计信息的场景。
DELETE FROM TEST_BIG WHERE STATUS = 1;
COMMIT;
CALL SP_TAB_STAT_DEINIT('SYSDBA','TEST_BIG');
CALL SP_TAB_COL_STAT_DEINIT('SYSDBA','TEST_BIG');
EXPLAIN
SELECT * FROM TEST_BIG
WHERE STATUS = 1;
截图中底层 CSCN2 估算表规模为 8000 行,SLCT2 对 STATUS=1 过滤后估算为 1600 行。实际上 STATUS=1 的数据已经被删除,因此真实结果应为 0 行。
6.1 重新收集统计信息
CALL SP_TAB_STAT_INIT('SYSDBA','TEST_BIG');
CALL SP_TAB_INDEX_STAT_INIT('SYSDBA','TEST_BIG');
EXPLAIN
SELECT * FROM TEST_BIG
WHERE STATUS = 1;
重新收集统计信息后,计划从“CSCN2 + SLCT2”变成“IDX_TEST_STATUS 的 SSEK2 + BLKUP2”。估算结果从 1600 行下降到 1 行,说明优化器对 STATUS=1 的选择性判断明显改善。
文章
阅读量
获赞
