自动SQL优化实战|吃透调优接口+报告配置+统计+索引全流程落地

一、为什么我们需要自动化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)设计的调优建议报告接口。该报告内容涵盖以下几个方面:

  1. GENERAL INFORMATION:一般信息

包括任务名称、任务所有者、状态、执行开始和完成时间、SQL ID和SQL语句。

  1. FINGDING:调优发现

包括优化建议的动作:索引建议和收集统计信息会给出具体SQL,改写建议不会给出改写后的SQL。

预期收益:仅索引建议有, 收集统计信息和SQL改写没有。

基本原理:索引建议和收集统计信息有, 改写建议没有。

  1. 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_logsys_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_idstatus组合查询:

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');

用建议器分析后,它给出的建议

  1. 统计信息:sys_orderscreate_time列统计信息缺失
  2. 索引:建议创建(create_time, status, user_id)的复合索引
  3. 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时,它能帮你快速找到优化突破口。

相关推荐
TDengine (老段)1 小时前
# TDengine TMQ 最佳实践 — 可靠消费、容错与监控
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据
杨云龙UP2 小时前
Windows SQL Server 备份无法传输至异地共享目录排查处理
运维·服务器·数据库·windows·共享·smb·异地备份传输
落寞的电源2 小时前
Delegate = Object + MethodInfo
开发语言·数据库·c#
要开心吖ZSH2 小时前
处方物流信息同步优化:从 36 秒到亚秒级的踩坑记录
java·数据库·mysql·性能优化
隐形的萝莉2 小时前
再回到技术面,研究 T-SQL 的 UNION、EXISTS、EXCEPT、INTERSECT 运算符。
数据库·sql
吴声子夜歌3 小时前
SQL进阶——窗口函数进行行间比较
数据库·sql
Nturmoils3 小时前
JOIN 不是拼上就完事,多表查询这几个坑先跑一遍
数据库·后端
yoothey3 小时前
MySQL 数据库连接池调优实战
数据库·mysql
水无痕simon3 小时前
5 多表操作
java·开发语言·数据库