一、为什么我们需要自动化SQL调优?
SQL优化建议器能够根据数据库内部状态,向数据库管理员(DBA)提供调优建议。在应用这些优化建议后,经过调整的SQL语句性能将得到显著提升。
对于基于单条SQL语句的调优工作,系统可以直接生成并呈现具体的优化建议内容,无需额外进行任务管理或结果查询视图等操作,即可一次性获得全部调优结果。系统的调优任务采用阻塞方式运行以确保执行的稳定性和可靠性。
二、SQL优化建议器到底是个啥?
统计信息更新建议:
在执行SQL语句的过程中,可能会引用一个或多个关系表。如果这些表的统计信息未能及时更新,或者缺乏多列统计信息,可能导致查询优化器生成非最优的执行计划。
这一问题在表数据出现显著倾斜(即数据分布极不均衡)时尤为突出。针对输入的SQL语句,SQL调优建议器将分析其引用的所有关系表,并对每个表进行统计信息检查。
当检测到表中修改的数据量超过autovacuum的计算结果时,系统会自动生成收集统计信息的建议。
索引建议:
对单条SQL语句进行分析时,系统会检查其涉及的表在过滤或连接条件中是否被引用。如果发现相关表缺少必要的索引,则自动应用虚拟索引技术进行处理。通过对比执行计划前后的性能数据,若性能提升幅度达到5%以上,则将给出则给出索引建议、建议索引类型、创建索引的DDL语句、预期收益率等优化方案。
SQL改写建议:
通过对预设改写规则进行配置,系统能够识别符合条件的SQL语句并生成相应的优化建议

三、调优接口说明
报告通过以下接口,通过SQL语句、QUERY_ID指定语句生成。
3.1 调优报告接口 :
| 接口名称 | 说明 |
|---|---|
PERF.QUICK_TUNE_BY_SQL(SQL_TEXT) |
输入一条SQL语句,生成调优建议报告 |
PERF.QUICK_TUNE_BY_QUERYID(QUERY_ID) |
输入一条SQL语句的查询ID,生成调优建议报告 |
PERF.QUICK_TUNE_BY_SQL_TO_FILE(SQL_TEXT, FORMAT, FILE_PATH) |
输入一条SQL语句,生成调优建议报告到指定文件 |
PERF.QUICK_TUNE_BY_QUERYID_TO_FILE(QUERY_ID, FORMAT, FILE_PATH) |
输入查询ID,生成调优建议报告到指定文件 |
3.2 接口参数说明
| 参数名称 | 说明 |
|---|---|
SQL_TEXT |
指定输入的SQL语句 |
QUERY_ID |
指定SQL语句的唯一查询标识符,来源于sys_stat_statements系统视图 |
FORMAT |
报告输出格式,当前仅支持TEXT文本格式 |
FILE_PATH |
报告文件的存储路径和名称 |
四、调优报告生成的基本配置:别急着跑,先配好环境
为提升用户体验,SQL优化功能提供了专门针对数据库管理员(DBA)设计的调优建议报告接口。该报告内容涵盖以下几个方面:
- GENERAL INFORMATION:一般信息
包括任务名称、任务所有者、状态、执行开始和完成时间、SQL ID和SQL语句。
- FINGDING:调优发现
包括优化建议的动作:索引建议和收集统计信息会给出具体SQL,改写建议不会给出改写后的SQL。
预期收益:仅索引建议有, 收集统计信息和SQL改写没有。
基本原理:索引建议和收集统计信息有, 改写建议没有。
- EXPLAIN PLANS:执行计划
优化前后执行计划对比(仅索引建议有, 收集统计信息和SQL改写没有)。
4.1 开启自动调优功能
金仓默认可能没有开启SQL调优相关的扩展,需要手动加载:
sql
-- 检查是否已安装
SELECT * FROM sys_extension WHERE extname = 'sys_sqltune';
-- 如果没有,创建扩展
CREATE EXTENSION IF NOT EXISTS sys_sqltune;
注意:这里要用sys_sqltune,不是其他名字。我第一次搞的时候,看网上有些资料写的名字不对,折腾了半天。
4.2 配置调优参数
在kingbase.conf配置文件中,有几个参数直接影响建议器的分析深度:
ini
# 开启自动SQL调优
sys_sqltune.enabled = on
# 设置调优任务的最大执行时间 秒
sys_sqltune.time_limit = 600
# 是否启用索引建议功能
sys_sqltune.index_advisor_enabled = on
# 是否启用SQL改写建议
sys_sqltune.sql_rewrite_enabled = on
# 设置建议器的详细程度,basic或comprehensive
sys_sqltune.report_level = 'comprehensive'
改完配置记得重启数据库生效。我当时就是改完没重启,查了半天为什么配置不生效,真是低级错误。
4.3 创建调优用的辅助表
建议器需要一些工作表来存储中间结果,建议提前建好:
sql
-- 创建调优任务记录表,如果不存在会自动创建,但手动建可以指定表空间
CREATE TABLE IF NOT EXISTS sys_sqltune_log (
task_id SERIAL PRIMARY KEY,
task_name VARCHAR(100),
sql_text TEXT,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20),
report_content TEXT
) TABLESPACE sys_default;
表名一定要用sys_开头,比如sys_sqltune_log、sys_tuning_tasks,千万别用pg_前缀,那是违规的。
五、生成统计信息的更新建议:让优化器看清数据分布
配置文件kingbase.conf中设置
bash
Autovacuum = off; -- 临时关闭统计信息自动更新
重启数据库
创建表、插入数据
sql
CREATE TABLE t1 (id1 INT);
INSERT INTO t1 SELECT generate_series(1,100000);
调用接口生成调优报告:
sql
SELECT PERF.QUICK_TUNE_BY_SQL('SELECT * FROM t1');

六、生成索引的建议
索引建议是我用得最多的功能。金仓的建议器会分析SQL的WHERE条件、JOIN条件、ORDER BY子句,推荐最合适的索引。
6.1 单表查询的索引建议
继续用上面的sys_orders表,假设我们经常按user_id和status组合查询:
sql
SELECT order_id, amount
FROM sys_orders
WHERE user_id = 9527
AND status = 'completed'
ORDER BY order_date DESC;
跑一遍调优任务,报告里的索引建议部分可能是这样的:
索引建议
--------
基于查询条件,建议在表 sys_orders 上创建复合索引:
CREATE INDEX idx_sys_orders_user_status_date
ON sys_orders(user_id, status, order_date);
理由:
1. 等值查询列 user_id 和 status 放在索引前导列
2. 排序列 order_date 放在最后,避免回表排序
3. 预计可将执行代价从 12345 降低至 234
6.2 多表关联的索引建议
实际业务中更常见的是多表关联。咱们再建个用户表:
sql
CREATE TABLE sys_users (
user_id INT PRIMARY KEY,
user_name VARCHAR(50),
register_date TIMESTAMP,
level INT
);
-- 插入测试数据
INSERT INTO sys_users
SELECT generate_series(1, 10000),
'user_' || generate_series(1, 10000),
CURRENT_TIMESTAMP - (random() * 1000 || ' days')::INTERVAL,
(random() * 10)::INT;
现在执行关联查询:
sql
SELECT o.order_id, o.amount, u.user_name
FROM sys_orders o
JOIN sys_users u ON o.user_id = u.user_id
WHERE o.status = 'completed'
AND u.level > 5
AND o.order_date > '2024-01-01';
建议器可能会给出这样的建议:
索引建议
--------
1. 表 sys_orders:创建索引 idx_sys_orders_status_date
ON sys_orders(status, order_date, user_id)
2. 表 sys_users:在 level 列创建索引 idx_sys_users_level
ON sys_users(level)
或考虑创建包含 user_name 的覆盖索引:
ON sys_users(level, user_id, user_name)
我的经验:建议器推荐的索引不一定都要建,要结合业务实际。比如如果level列的区分度很低,只有0和1两个值,建索引反而可能更慢。建议器只是给建议,最终决策还得靠咱们DBA的判断。
七、生成SQL改写建议
有些SQL,即使加了索引、统计信息也最新,但就是因为写法问题导致性能差。金仓的建议器能识别出一些常见的低效写法并给出改写方案。
7.1 避免隐式类型转换
我见过太多人栽在这个坑上。比如:
sql
-- 原始SQL
SELECT * FROM sys_orders
WHERE user_id = '9527';
虽然user_id是INT类型,但传入字符串,数据库会做隐式类型转换,导致索引失效。建议器会提示:
SQL改写建议
-----------
发现隐式类型转换可能影响性能。
建议改写为:
SELECT * FROM sys_orders WHERE user_id = 9527;
7.2 优化IN子查询
IN子查询数据量大时性能很差,建议器会建议改成EXISTS或JOIN:
sql
-- 原始SQL
SELECT * FROM sys_orders
WHERE user_id IN (
SELECT user_id FROM sys_users WHERE level = 9
);
建议器可能建议:
SQL改写建议
-----------
当前IN子查询可能产生大量重复值扫描。
建议改写为:
SELECT o.*
FROM sys_orders o
WHERE EXISTS (
SELECT 1 FROM sys_users u
WHERE u.user_id = o.user_id
AND u.level = 9
);
或者:
或者使用JOIN写法
SELECT DISTINCT o.*
FROM sys_orders o
JOIN sys_users u ON o.user_id = u.user_id
WHERE u.level = 9;
7.3 优化LIMIT分页
深分页是性能杀手。比如:
sql
-- 原始SQL,越往后越慢
SELECT * FROM sys_orders
ORDER BY order_date DESC
LIMIT 10 OFFSET 100000;
建议器会提示:
SQL改写建议
-----------
深分页查询性能较差,建议使用键集分页或基于游标的分页。
改写方案,基于上次查询的最大值
SELECT * FROM sys_orders
WHERE order_date < '上次最后一条的时间'
ORDER BY order_date DESC
LIMIT 10;
这些改写建议,说实话,有些我自己都没想到。特别是复杂的子查询优化,建议器给出的等价改写往往执行计划更优。
八、使用queryId生成建议
前面说的都是针对单条SQL的实时分析。但在生产环境,我们更常遇到的情况是:某个接口响应慢,但SQL是ORM生成的,咱们不知道具体SQL文本,只知道它在数据库里的queryId。
8.1 定位慢SQL的queryId
金仓的sys_stat_statements扩展会记录所有SQL的执行统计:
sql
-- kingbase.conf开启统计收集
sys_stat_statements.track = all
-- 查询最耗时的SQL
SELECT queryid,
query,
calls,
total_exec_time,
mean_exec_time,
rows
FROM sys_stat_statements
WHERE query NOT LIKE '%sys_stat_statements%'
ORDER BY mean_exec_time DESC
LIMIT 10;
假设咱们发现queryId为123456789012345678的SQL平均执行时间很长。
8.2 基于queryId创建调优任务
sql
-- 创建任务
SELECT sys_sqltune.create_tuning_task(
query_id => 123456789012345678,
task_name => 'prod_slow_query_001',
time_limit => 600
) FROM dual;
-- 执行
EXEC sys_sqltune.execute_tuning_task('prod_slow_query_001');
-- 获取详细报告
SELECT sys_sqltune.report_tuning_task('prod_slow_query_001')
INTO report_content;
8.3 实际案例分享
上个月我就遇到个生产问题,有个报表接口超时。通过queryId定位到这条SQL:
sql
-- 这是ORM生成的,实际执行计划很烂
SELECT COUNT(*) FROM sys_orders o
LEFT JOIN sys_users u ON o.user_id = u.user_id
WHERE o.create_time > CURRENT_DATE - INTERVAL '30 days'
AND (o.status = 'pending' OR o.status = 'processing');
用建议器分析后,它给出的建议
- 统计信息:
sys_orders的create_time列统计信息缺失 - 索引:建议创建
(create_time, status, user_id)的复合索引 - SQL改写:建议把
OR条件改写成IN列表,并提示当前写法可能导致索引扫描范围过大
按照建议优化后,这条SQL从8秒降到了150毫秒。最爽的是,我根本不用去理解那条超复杂的ORM生成SQL的具体逻辑,建议器直接告诉我哪里有问题。
九、从建表到优化全流程
为了让大家更直观地理解整个流程,我模拟一个完整的业务场景。
9.1 场景设定
假设咱们有个电商系统,需要分析最近半年的订单数据,关联用户信息和商品类别。
9.2 建表和造数据
sql
-- 订单表
CREATE TABLE sys_order_master (
order_id BIGINT PRIMARY KEY,
user_id INT NOT NULL,
category_id INT,
order_amount DECIMAL(12,2),
order_status SMALLINT, -- 0:待支付 1:已支付 2:已发货 3:已完成 4:已取消
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
pay_time TIMESTAMP
);
-- 用户表
CREATE TABLE sys_user_info (
user_id INT PRIMARY KEY,
user_name VARCHAR(50),
register_time TIMESTAMP,
user_level SMALLINT DEFAULT 1,
is_vip BOOLEAN DEFAULT FALSE
);
-- 商品类别表
CREATE TABLE sys_category (
category_id INT PRIMARY KEY,
category_name VARCHAR(50),
parent_id INT
);
-- 插入测试数据
INSERT INTO sys_order_master
SELECT generate_series(1, 1000000),
(random() * 100000)::INT + 1,
(random() * 100)::INT + 1,
(random() * 10000)::DECIMAL(12,2),
(random() * 5)::SMALLINT,
CURRENT_TIMESTAMP - (random() * 180 || ' days')::INTERVAL,
CASE WHEN random() > 0.3 THEN CURRENT_TIMESTAMP - (random() * 180 || ' days')::INTERVAL ELSE NULL END
FROM generate_series(1, 1000000);
INSERT INTO sys_user_info
SELECT generate_series(1, 100000),
'user_' || generate_series(1, 100000),
CURRENT_TIMESTAMP - (random() * 365 || ' days')::INTERVAL,
(random() * 5)::SMALLINT + 1,
random() > 0.8
FROM generate_series(1, 100000);
INSERT INTO sys_category
SELECT generate_series(1, 100),
'category_' || generate_series(1, 100),
CASE WHEN generate_series > 10 THEN (random() * 10)::INT + 1 ELSE 0 END
FROM generate_series(1, 100);
9.3 模拟慢SQL
业务需要查询:最近30天,VIP用户的已完成订单,按类别汇总金额。
sql
SELECT
c.category_name,
COUNT(*) as order_count,
SUM(o.order_amount) as total_amount,
AVG(o.order_amount) as avg_amount
FROM sys_order_master o
JOIN sys_user_info u ON o.user_id = u.user_id
JOIN sys_category c ON o.category_id = c.category_id
WHERE o.create_time > CURRENT_DATE - INTERVAL '30 days'
AND o.order_status = 3
AND u.is_vip = TRUE
GROUP BY c.category_name
ORDER BY total_amount DESC;
9.4 执行调优分析
sql
-- 创建任务
SELECT sys_sqltune.create_tuning_task(
sql_text => 'SELECT c.category_name, COUNT(*) as order_count, SUM(o.order_amount) as total_amount, AVG(o.order_amount) as avg_amount FROM sys_order_master o JOIN sys_user_info u ON o.user_id = u.user_id JOIN sys_category c ON o.category_id = c.category_id WHERE o.create_time > CURRENT_DATE - INTERVAL ''30 days'' AND o.order_status = 3 AND u.is_vip = TRUE GROUP BY c.category_name ORDER BY total_amount DESC',
task_name => 'business_report_tune',
time_limit => 300
) FROM dual;
-- 执行
EXEC sys_sqltune.execute_tuning_task('business_report_tune');
-- 查看报告
SELECT sys_sqltune.report_tuning_task('business_report_tune');
9.5 优化建议与实施
假设报告给出以下建议:
统计信息建议:
sql
ANALYZE sys_order_master(create_time, order_status, category_id);
ANALYZE sys_user_info(is_vip, user_id);
索引建议:
sql
-- 针对订单表的复合索引
CREATE INDEX idx_sys_order_master_time_status
ON sys_order_master(create_time, order_status, category_id, user_id, order_amount);
-- 针对用户表的索引
CREATE INDEX idx_sys_user_info_vip_id
ON sys_user_info(is_vip, user_id);
SQL改写建议:
原SQL使用了CURRENT_DATE - INTERVAL '30 days',建议器提示这种表达式在WHERE条件中可能导致函数索引无法使用,建议应用层传入具体日期值。
9.6 优化效果对比
优化前:执行时间4.2秒,全表扫描sys_order_master。
优化后:执行时间85毫秒,使用索引扫描,回表次数大幅减少。
十、最佳实践
用了这么久,我也总结出一些经验给大家列出来
10.1 不要全盘接受建议
建议器是死的,业务是活的。它建议的索引可能很多,但你要考虑:
索引维护成本,索引重复度 ,列的区分度
10.2 定期清理调优任务
调优任务和报告会占用空间,建议定期清理,
删除7天前的任务,这个是根据业务来定的
sql
DELETE FROM sys_sqltune_log
WHERE create_time < CURRENT_DATE - INTERVAL '7 days';
10.3 复杂SQL分段调优
对于超复杂的嵌套查询,建议拆分成多个子查询分别调优,再整合建议。一次性分析太复杂的SQL,建议器可能超时或给出的建议不够细致。
十一、结语
使用SQL优化建议的方法可直接针对SQL语句生成调优建议报告,或通过输入的SQL语句的QUERYID参数指定具体查询,并自动生成优化建议,结果将存储于目标文件夹中。
当然,工具始终是辅助,最终的数据库优化决策还得靠咱们DBA对业务的理解。建议器告诉你"该建索引",但建什么类型的索引、要不要做覆盖索引,这些还是需要人工判断。
如果你也在用金仓数据库,强烈建议把SQL优化建议器用起来。特别是面对历史遗留系统,面对成千上万条不知道从哪里下手的慢SQL时,它能帮你快速找到优化突破口。