KingbaseES电科金仓数据库SQL调优

KingbaseES电科金仓数据库SQL调优

前言

说实话,SQL调优这个话题,很多朋友一听就头疼。我刚开始接触数据库的时候也是这样,看着那些执行计划里密密麻麻的信息,完全不知道从哪儿下手。但是干了这么多年,我发现其实SQL调优没那么玄乎,掌握几个核心思路,很多问题都能迎刃而解。

在数据库应用系统中,SQL语句的执行效率直接影响着整个系统的性能表现。KingbaseES作为国产主流数据库,提供了丰富的SQL优化手段和工具。今天咱们就聊聊KingbaseES的SQL调优,我会尽量用大白话给大家讲清楚。

一、使用索引优化

索引这个东西,我喜欢把它比作书的目录。你想想,如果让你在一本几百页的书里找某个知识点,有目录和没目录,效率能差多少倍?数据库也是一样的道理。

1.1 创建合适的索引

索引不是越多越好,也不是随便建的。我见过有些项目,开发人员一股脑给所有列都建了索引,结果插入数据的时候慢得要命。记住一个原则:在经常查询的列上建索引,在经常更新的列上要慎重

sql 复制代码
-- 创建单列索引
-- 比如你经常按员工姓名查询,那就给name列建个索引
CREATE INDEX idx_employee_name ON employee(name);

-- 创建复合索引
-- 如果你经常按客户ID和订单日期一起查,就建个组合索引
-- 注意:列的顺序很重要!把过滤性强的列放前面
CREATE INDEX idx_order_customer_date ON orders(customer_id, order_date);

-- 创建唯一索引
-- 员工ID肯定不能重复,用唯一索引既能加速查询,又能保证数据唯一性
CREATE UNIQUE INDEX idx_employee_id ON employee(employee_id);

1.2 索引使用的坑

这里我要特别提醒几个常见的坑,我自己都踩过:

第一个坑:在索引列上用函数

sql 复制代码
-- 这样写索引就废了,数据库会全表扫描
SELECT * FROM employee WHERE UPPER(name) = 'ZHANG SAN';

-- 应该这样写
SELECT * FROM employee WHERE name = 'ZHANG SAN';

第二个坑:索引列的顺序搞反了

sql 复制代码
-- 如果你建了索引 (customer_id, order_date)
-- 这个查询能用上索引
SELECT * FROM orders WHERE customer_id = 100 AND order_date = '2024-01-01';

-- 这个也能用上(只用了索引的第一列)
SELECT * FROM orders WHERE customer_id = 100;

-- 但这个就用不上了!因为跳过了第一列
SELECT * FROM orders WHERE order_date = '2024-01-01';

第三个坑:索引也需要维护 索引用久了会产生碎片,就像硬盘碎片整理一样,定期重建一下效果会更好:

sql 复制代码
-- 重建索引,我一般在业务低峰期做
REINDEX INDEX idx_employee_name;

二、更新统计信息

这个问题我遇到过好几次。有一次线上系统突然变慢,查了半天发现是统计信息过期了。数据库优化器就像一个导航系统,如果地图是旧的,它给你规划的路线肯定不是最优的。

2.1 什么时候需要更新统计信息?

  • 大批量导入数据之后
  • 删除了大量数据之后
  • 表结构发生变化之后
  • 查询突然变慢,但SQL和索引都没问题的时候

2.2 怎么更新?

sql 复制代码
-- 更新单表统计信息,这个操作很快,放心用
ANALYZE employee;

-- 如果你想更新所有表,直接这样
ANALYZE;

-- 如果表特别大,可以只更新关键列
ANALYZE employee(name, department_id);

小技巧:我一般会在定时任务里加上ANALYZE,比如每天凌晨跑一次,这样就不用担心统计信息过期了。

2.3 配置自动统计信息收集

sql 复制代码
-- 让数据库自己维护统计信息,省心省力
ALTER TABLE employee SET (autovacuum_enabled = true);

三、调整work_mem参数

work_mem这个参数,说白了就是给排序和哈希操作分配的内存。默认值一般比较保守,如果你的服务器内存够大,适当调大能明显提升性能。

但是注意!这个参数不是越大越好。我见过有人直接设置成几个G,结果系统内存被吃光了。

3.1 什么时候需要调整?

看执行计划的时候,如果看到"external merge"或者"disk"这样的字眼,说明内存不够用了,数据被写到磁盘临时文件里了,这时候就该考虑增大work_mem。

3.2 怎么调整?

sql 复制代码
-- 我的建议是先在会话级别试试,看看效果
SET work_mem = '256MB';

-- 跑你的查询
SELECT * FROM large_table ORDER BY column1;

-- 如果效果好,再考虑改全局配置
-- 用完记得恢复,不然可能影响其他查询
RESET work_mem;

3.3 全局配置

在kingbase.conf中配置:

ini 复制代码
# 我一般设置为总内存的1-2%
# 比如64GB内存的服务器,设置64MB-128MB比较合适
work_mem = 64MB

经验之谈:如果你的系统有很多并发连接,work_mem千万别设太大。假设你设置了256MB,有100个连接同时跑排序操作,那就是25GB内存!服务器直接崩溃。

四、使用分区表

分区表这个功能,对付大表特别管用。我之前有个项目,一张订单表有几亿条数据,查询慢得要死。后来按月份做了分区,查询速度提升了十几倍。

4.1 什么样的表适合分区?

  • 数据量特别大的表(比如超过几千万行)
  • 有明显时间特征的表(比如订单表、日志表)
  • 经常按某个范围查询的表

4.2 创建分区表

sql 复制代码
-- 创建范围分区表
-- 这里我按日期分区,因为订单表通常都是按时间查询的
CREATE TABLE sales (
    id INT,
    sale_date DATE,
    amount DECIMAL(10,2)
) PARTITION BY RANGE (sale_date);

-- 创建分区,我习惯按季度分
-- 这样既不会分区太多,也能有效缩小查询范围
CREATE TABLE sales_2024_q1 PARTITION OF sales
    FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');

CREATE TABLE sales_2024_q2 PARTITION OF sales
    FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');

4.3 分区的好处

sql 复制代码
-- 查询2024年第一季度的数据
-- 数据库只会扫描sales_2024_q1这个分区,其他分区根本不碰
SELECT * FROM sales 
WHERE sale_date BETWEEN '2024-01-01' AND '2024-03-31';

注意事项

  • 分区字段最好是不会变的,比如订单日期。如果你按状态分区,状态一变就得跨分区更新,反而更慢
  • 分区不要分太细,我见过有人按天分区,结果一年就365个分区,管理起来很麻烦
  • 记得给每个分区都建索引

五、使用物化视图

物化视图是个好东西,特别适合那种计算复杂、但数据不需要实时更新的场景。比如各种报表、统计数据。

5.1 什么时候用物化视图?

我一般在这几种情况下用:

  • 复杂的多表关联查询
  • 大量聚合计算
  • 查询频繁但数据更新不频繁
  • 报表类查询

5.2 创建物化视图

sql 复制代码
-- 比如你要做一个销售统计报表
-- 每次查都要关联好几张表,还要做聚合,特别慢
-- 不如把结果存起来
CREATE MATERIALIZED VIEW mv_sales_summary AS
SELECT 
    department_id,
    DATE_TRUNC('month', sale_date) AS month,
    SUM(amount) AS total_amount,
    COUNT(*) AS sale_count
FROM sales
GROUP BY department_id, DATE_TRUNC('month', sale_date);

-- 别忘了给物化视图也建索引
CREATE INDEX idx_mv_sales_dept ON mv_sales_summary(department_id);

5.3 刷新物化视图

sql 复制代码
-- 完全刷新,会锁表,适合在业务低峰期做
REFRESH MATERIALIZED VIEW mv_sales_summary;

-- 并发刷新,不会阻塞查询,但需要物化视图有唯一索引
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_sales_summary;

实战经验:我一般会在定时任务里刷新物化视图,比如每天凌晨3点刷新一次。白天业务高峰期,用户查询的都是物化视图,速度飞快。

六、缓存执行计划

这个功能很多人不知道,其实挺有用的。如果你的SQL是参数化的,用PREPARE可以让数据库缓存执行计划,不用每次都重新解析。

6.1 什么时候用?

  • 同一个SQL要执行很多次,只是参数不同
  • 在应用程序里用预编译语句(PreparedStatement)

6.2 怎么用?

sql 复制代码
-- 准备语句,$1是参数占位符
PREPARE get_employee (INT) AS
    SELECT * FROM employee WHERE department_id = $1;

-- 执行,第一次会生成执行计划
EXECUTE get_employee(10);

-- 再执行,直接用缓存的执行计划,快!
EXECUTE get_employee(20);
EXECUTE get_employee(30);

-- 用完记得释放
DEALLOCATE get_employee;

注意:如果参数的数据分布差异很大,缓存的执行计划可能不是最优的。比如某个部门有1万人,另一个部门只有10个人,用同一个执行计划就不合适了。

七、调整性能参数

数据库的性能参数,就像汽车的调校一样,需要根据实际情况调整。默认配置是为了兼容各种环境,往往比较保守。

7.1 关键性能参数

sql 复制代码
-- shared_buffers:数据库的缓存池
# 我的经验是设置为系统内存的25%左右
# 比如32GB内存的服务器,设置8GB
shared_buffers = 8GB

-- effective_cache_size:告诉优化器系统有多少内存可以用来缓存
# 这个可以设大一点,50-75%都行
# 它不会真的占用内存,只是给优化器一个参考
effective_cache_size = 24GB

-- random_page_cost:随机读取的成本
# 如果用的是SSD,可以设小一点,1.1左右
# 机械硬盘的话,保持默认的4.0
random_page_cost = 1.1

-- maintenance_work_mem:维护操作的内存
# 用于CREATE INDEX、VACUUM等操作
# 可以设大一点,512MB-1GB
maintenance_work_mem = 512MB

7.2 查询优化参数

sql 复制代码
-- JIT编译,对复杂查询有帮助
# 但也会增加编译开销,简单查询反而可能变慢
jit = on

-- 并行查询的工作进程数
# 根据CPU核心数设置,一般设置为核心数的一半
max_parallel_workers_per_gather = 4

调参心得

  1. 一次只改一个参数,改完测试效果
  2. 改参数前先备份配置文件
  3. 不要盲目照搬别人的配置,每个系统的情况都不一样
  4. 改完参数记得重启数据库(有些参数需要重启才能生效)

八、使用并行查询

并行查询就是让数据库用多个CPU核心同时处理一个查询。对于大表扫描和聚合操作,效果特别明显。

8.1 什么时候用并行?

  • 表数据量很大(至少几百万行)
  • 查询需要扫描大量数据
  • 服务器CPU核心数比较多
  • 当前系统负载不高

8.2 启用并行查询

sql 复制代码
-- 设置并行度,不要超过CPU核心数
SET max_parallel_workers_per_gather = 4;

-- 如果数据库不愿意用并行,可以降低并行的成本估算
-- 但这是临时方案,不建议长期使用
SET parallel_setup_cost = 0;
SET parallel_tuple_cost = 0;

8.3 查看是否使用了并行

sql 复制代码
-- 用EXPLAIN看执行计划
-- 如果看到"Parallel Seq Scan"或"Gather",说明用上并行了
EXPLAIN (ANALYZE, BUFFERS) 
SELECT department_id, COUNT(*) 
FROM large_employee_table 
GROUP BY department_id;

实战经验

  • 并行查询不是银弹,小表用并行反而更慢(因为有并行协调的开销)
  • 如果系统并发很高,不要开太多并行,会抢占其他查询的资源
  • OLTP系统慎用并行,更适合OLAP场景

九、SQL改写优化

有时候,同样的业务逻辑,换一种写法,性能能差好几倍。这里分享几个我常用的改写技巧。

9.1 避免SELECT *

sql 复制代码
-- 这是我见过最多的坏习惯
-- 你真的需要所有列吗?很多时候只需要几个字段
SELECT * FROM employee WHERE department_id = 10;

-- 应该这样写,需要什么查什么
-- 这样不仅减少网络传输,还能用上覆盖索引
SELECT employee_id, name, salary FROM employee WHERE department_id = 10;

真实案例:我之前有个项目,一张表有50多个字段,其中有几个是TEXT类型的大字段。开发人员图省事用SELECT *,结果查询特别慢。后来改成只查需要的列,速度提升了5倍。

9.2 使用EXISTS代替IN

sql 复制代码
-- IN子查询,如果子查询结果集很大,性能会很差
SELECT * FROM employee 
WHERE department_id IN (SELECT id FROM department WHERE location = 'Beijing');

-- EXISTS通常更快,因为找到第一条匹配就停止了
SELECT * FROM employee e
WHERE EXISTS (
    SELECT 1 FROM department d 
    WHERE d.id = e.department_id AND d.location = 'Beijing'
);

9.3 避免隐式类型转换

sql 复制代码
-- 这个坑很隐蔽,如果employee_id是整型
-- 你用字符串去比较,索引就废了
SELECT * FROM employee WHERE employee_id = '1001';

-- 应该用正确的类型
SELECT * FROM employee WHERE employee_id = 1001;

血泪教训:有一次线上查询突然变慢,查了半天发现是前端传过来的参数类型不对。明明是数字,传成了字符串,导致索引失效,全表扫描。

9.4 使用UNION ALL代替UNION

sql 复制代码
-- UNION会自动去重,需要排序,很慢
SELECT name FROM employee_2023
UNION
SELECT name FROM employee_2024;

-- 如果你确定没有重复,或者不在乎重复,用UNION ALL
-- 性能能提升好几倍
SELECT name FROM employee_2023
UNION ALL
SELECT name FROM employee_2024;

9.5 其他改写技巧

用JOIN代替子查询

sql 复制代码
-- 子查询写法,可读性好但性能一般
SELECT e.name, 
       (SELECT d.name FROM department d WHERE d.id = e.department_id) as dept_name
FROM employee e;

-- JOIN写法,性能更好
SELECT e.name, d.name as dept_name
FROM employee e
LEFT JOIN department d ON d.id = e.department_id;

避免在WHERE子句中使用OR

sql 复制代码
-- OR可能导致索引失效
SELECT * FROM employee WHERE department_id = 10 OR department_id = 20;

-- 改成IN,或者用UNION ALL
SELECT * FROM employee WHERE department_id IN (10, 20);

十、执行计划分析

执行计划是SQL调优的基础,不会看执行计划,调优就是瞎猫碰死耗子。我刚开始的时候也看不懂,后来慢慢摸索,总结了一些经验。

10.1 怎么看执行计划?

sql 复制代码
-- 最基础的,看预估的执行计划
EXPLAIN SELECT * FROM employee WHERE department_id = 10;

-- 加上ANALYZE,看实际执行情况
-- 这个会真的执行SQL,所以对于UPDATE/DELETE要小心
EXPLAIN ANALYZE SELECT * FROM employee WHERE department_id = 10;

-- 详细版本,包含缓冲区信息
-- 我一般用这个,信息最全
EXPLAIN (ANALYZE, BUFFERS, VERBOSE) 
SELECT * FROM employee WHERE department_id = 10;

10.2 执行计划里的关键信息

扫描方式

  • Seq Scan(顺序扫描):全表扫描,小表无所谓,大表就要注意了
  • Index Scan(索引扫描):用上索引了,通常是好事
  • Index Only Scan(仅索引扫描):最优情况,只扫描索引,不回表
  • Bitmap Index Scan:位图索引扫描,适合返回大量数据的情况

关联方式

  • Nested Loop:嵌套循环,适合小表关联
  • Hash Join:哈希关联,适合大表关联
  • Merge Join:归并关联,需要数据已排序

成本信息

ini 复制代码
cost=0.00..35.50 rows=10 width=244
  • cost:预估成本,前面是启动成本,后面是总成本
  • rows:预估返回行数
  • width:每行的平均字节数

实际执行信息

ini 复制代码
actual time=0.015..0.023 rows=10 loops=1
  • actual time:实际执行时间(毫秒)
  • rows:实际返回行数
  • loops:执行次数

10.3 怎么判断有问题?

预估和实际差距很大

ini 复制代码
-- 预估10行,实际10000行,说明统计信息不准
Seq Scan on employee  (cost=0.00..35.50 rows=10 width=244) 
                      (actual time=0.015..15.234 rows=10000 loops=1)

这时候需要ANALYZE更新统计信息。

看到Seq Scan on 大表

ini 复制代码
-- 如果employee表有几百万行,这就是问题
Seq Scan on employee  (cost=0.00..350000.00 rows=10 width=244)

应该考虑加索引。

看到"external merge"或"disk"

sql 复制代码
Sort Method: external merge  Disk: 102400kB

说明内存不够,数据写到磁盘了,需要增大work_mem。

Nested Loop的loops很大

ini 复制代码
Nested Loop  (cost=0.00..1000.00 rows=100 width=244)
             (actual time=0.015..500.234 rows=100 loops=10000)

这说明外层循环了10000次,每次都要执行内层查询,性能肯定差。

十一、监控和诊断

调优不是一次性的工作,需要持续监控。我一般会设置一些监控指标,定期检查。

11.1 找出慢查询

sql 复制代码
-- 启用慢查询日志
-- 记录超过1秒的查询,这个阈值可以根据业务调整
SET log_min_duration_statement = 1000;

-- 查看当前正在执行的查询
-- 这个在排查问题时特别有用
SELECT 
    pid,
    usename,
    state,
    query,
    query_start,
    NOW() - query_start AS duration
FROM sys_stat_activity
WHERE state = 'active'
ORDER BY duration DESC;

实战技巧 :如果发现某个查询执行了很久还没结束,可以用这个SQL找出来,然后用SELECT sys_cancel_backend(pid)终止它。

11.2 查看表和索引的统计信息

sql 复制代码
-- 查看表的统计信息
-- 可以看到表的大小、扫描次数、索引使用情况等
SELECT 
    schemaname,
    relname,
    seq_scan,  -- 顺序扫描次数
    seq_tup_read,  -- 顺序扫描读取的行数
    idx_scan,  -- 索引扫描次数
    idx_tup_fetch,  -- 索引扫描获取的行数
    n_tup_ins,  -- 插入的行数
    n_tup_upd,  -- 更新的行数
    n_tup_del  -- 删除的行数
FROM sys_stat_user_tables 
WHERE relname = 'employee';

-- 查看索引使用情况
-- 如果idx_scan是0,说明这个索引从来没用过,可以考虑删掉
SELECT 
    schemaname,
    relname,
    indexrelname,
    idx_scan,  -- 索引被使用的次数
    idx_tup_read,  -- 索引扫描返回的行数
    idx_tup_fetch  -- 索引扫描获取的行数
FROM sys_stat_user_indexes 
WHERE relname = 'employee'
ORDER BY idx_scan;

11.3 查看缓存命中率

sql 复制代码
-- 查看缓存命中率
-- 如果命中率低于95%,说明shared_buffers可能设置得太小了
SELECT 
    sum(heap_blks_read) as heap_read,
    sum(heap_blks_hit) as heap_hit,
    sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) as ratio
FROM sys_statio_user_tables;

11.4 查看锁等待

sql 复制代码
-- 查看锁等待情况
-- 如果经常有锁等待,说明可能有长事务或者死锁
SELECT 
    blocked_locks.pid AS blocked_pid,
    blocked_activity.usename AS blocked_user,
    blocking_locks.pid AS blocking_pid,
    blocking_activity.usename AS blocking_user,
    blocked_activity.query AS blocked_statement,
    blocking_activity.query AS blocking_statement
FROM sys_catalog.sys_locks blocked_locks
JOIN sys_catalog.sys_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN sys_catalog.sys_locks blocking_locks 
    ON blocking_locks.locktype = blocked_locks.locktype
    AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
    AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
    AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
    AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
    AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
    AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
    AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
    AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
    AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
    AND blocking_locks.pid != blocked_locks.pid
JOIN sys_catalog.sys_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;

总结

说了这么多,其实SQL调优的核心思想就几条:

  1. 减少数据扫描量:能用索引就用索引,能分区就分区
  2. 减少数据传输量:只查需要的列,避免SELECT *
  3. 合理使用内存:该给的内存要给,但也别给太多
  4. 利用缓存:统计信息要准确,执行计划能缓存就缓存
  5. 持续监控:定期检查慢查询,及时发现问题

最后说句心里话:SQL调优没有银弹,没有一招鲜吃遍天的方法。每个系统的情况都不一样,需要具体问题具体分析。我给大家的这些经验,是我这些年踩过的坑总结出来的,希望能帮大家少走点弯路。

调优这个事情,三分靠技术,七分靠经验。多看执行计划,多分析慢查询,慢慢就有感觉了。遇到问题别慌,一步一步排查,总能找到原因。

好了,今天就聊到这里。如果大家在实际工作中遇到SQL性能问题,可以按照我说的这些方法试试。记住,调优是个持续的过程,不是一劳永逸的。祝大家的SQL都能跑得飞快!

相关推荐
这周也會开心3 小时前
SpringBoot的搭建方式
java·spring boot·后端
varin3 小时前
分析OpenManus源码,建立拥有完全自主规划的AI智能体
后端
Tech有道3 小时前
字节跳动面试:Redis 数据结构有哪些?分别怎么实现的?
后端·面试
9ilk3 小时前
【仿RabbitMQ的发布订阅式消息队列】--- 介绍
linux·笔记·分布式·后端·rabbitmq
Tech有道3 小时前
滴滴面试题:一道“轮询算法”的面试题,让我意识到自己太天真了
后端·面试
golang学习记3 小时前
Go 1.25 Flight Recorder:线上偶发问题的“时间回放”利器
后端
ZZHHWW4 小时前
Redis 主从复制详解
后端
ZZHHWW4 小时前
Redis 集群模式详解(上篇)
后端
EMQX4 小时前
技术实践:在基于 RISC-V 的 ESP32 上运行 MQTT over QUIC
后端