KingbaseES系统视图与Hints调优:从诊断到性能优化的进阶之路
前言
数据库慢了,日志里翻不出原因,应用层看不出来毛病,但就是慢。
这种情况太多了。问题可能藏在锁等待里,可能是统计信息太久没更新导致执行计划走偏,也可能就是一个该加的索引一直没人加。这篇文章的思路很简单:用系统视图把问题翻出来,用EXPLAIN看明白执行计划,再用Hints在关键位置做调整。三步走,不绕弯。
文章目录
一、系统视图:数据库内部的"监控探头"
1.1 先知道有什么能用
系统视图不是真正的表,它是数据库暴露给外部的一组虚拟表,里面装的是数据库运行过程中产生的各种元数据和统计数据。你查它,就能知道数据库内部正在发生什么。
不用全记。按用途分个类,脑子里有个大概就行:
| 类别 | 代表视图 | 干什么用的 |
|---|---|---|
| 元信息 | sys_class、sys_attribute、sys_index |
看表结构、列定义、索引信息 |
| 会话与活动 | sys_stat_activity、sys_stat_database |
谁在连、跑什么SQL、忙不忙 |
| 表和索引统计 | sys_stat_all_tables、sys_stat_all_indexes |
全表扫描多不多、索引有没有人用 |
| 时间模型 | sys_stat_dbtime、sys_stat_sqltime |
时间花在哪:解析、执行还是干等 |
| 锁等待 | sys_locks、sys_stat_activity的wait_event列 |
谁堵谁、等什么 |
下面挑几个最常用的,配上能直接跑的SQL来讲。
1.2 谁在搞事情:查活跃会话
排查问题的第一步,十有八九是查sys_stat_activity。这个视图每一行对应一个数据库连接,能看到连上来的用户是谁、在跑什么SQL、当前是什么状态。
SELECT
pid,
usename,
datname,
application_name,
client_addr,
state,
wait_event_type || '.' || COALESCE(wait_event, 'none') AS waiting_for,
now() - query_start AS running_time,
query
FROM sys_stat_activity
WHERE state != 'idle'
ORDER BY running_time DESC;
跑出来以后,主要看两个东西。
一个是state列。active不用解释,正在干活。但idle in transaction要特别注意,这种状态说明应用开了个事务,跑了几条SQL,然后既不提交也不回滚,就这么挂着。时间一长它会堵VACUUM,VACUUM被堵住了表就开始膨胀,膨胀了查询就更慢,恶性循环。
另一个是waiting_for列。只要不是none,就说明这个会话在等什么东西。Lock是等锁,IO是等磁盘,Client是等客户端那边给回应。等锁是最常见的,基本就是两条SQL在抢同一张表甚至同一行。
如果嫌上面那条查出来的信息太多,这里有个更聚焦的版本,专门找那些"卡住了"的会话:
SELECT
pid,
usename,
state,
wait_event_type || '.' || wait_event AS wait_info,
now() - query_start AS stuck_duration,
LEFT(query, 100) AS query_preview
FROM sys_stat_activity
WHERE wait_event IS NOT NULL
AND state = 'active'
ORDER BY stuck_duration DESC
LIMIT 10;
1.3 哪些表在拖后腿:全表扫描和死元组
数据库跑久了,有些表会出毛病。比较常见的两种:一是查询频繁走全表扫描(说明缺索引或者索引没用上),二是死元组堆积(说明VACUUM不及时)。这两个问题都能通过sys_stat_all_tables查到。
SELECT
schemaname,
relname AS table_name,
seq_scan AS full_table_scans,
idx_scan AS index_scans,
n_live_tup AS live_rows,
n_dead_tup AS dead_rows,
CASE WHEN seq_scan + idx_scan > 0
THEN ROUND(seq_scan::numeric / (seq_scan + idx_scan) * 100, 1)
ELSE 0 END AS seq_scan_pct,
last_vacuum,
last_autovacuum,
last_analyze
FROM sys_stat_all_tables
WHERE schemaname NOT IN ('sys_catalog', 'information_schema')
ORDER BY n_dead_tup DESC
LIMIT 20;
拿到结果怎么判断?很简单,看两个数。
全表扫描比例,也就是seq_scan_pct。一张大表如果这个数超过80%,基本就是查询在暴力扫全表。要么缺索引,要么SQL写法导致索引没用上。不管哪种,都得处理。
再看n_dead_tup。这个数代表表里有多少"死掉"的行,也就是被DELETE或UPDATE旧版本留下的垃圾。如果死行数和活行数差不多甚至更多,说明VACUUM已经严重滞后了。查询每次扫表都要跳过这些垃圾行,白费IO。
索引那边也有类似的问题。有些索引建完就没人查过,但每次INSERT和UPDATE都得维护它,纯粹是在拖写入性能。查一下sys_stat_all_indexes:
SELECT
schemaname,
relname AS table_name,
indexrelname AS index_name,
idx_scan AS times_used
FROM sys_stat_all_indexes
WHERE schemaname NOT IN ('sys_catalog', 'information_schema')
ORDER BY idx_scan ASC
LIMIT 20;
idx_scan为0的索引,就是从创建到现在一次都没被走过。发现这种的,跟业务确认一下是不是确实没用了,没用的就删掉,别让它继续拖慢写入。
1.4 时间花在哪了:DB Time分析
有时候你明知道数据库在忙,但不知道忙什么。sys_stat_dbtime这个视图回答的就是这个问题。它把DB Time拆开来看,CPU花了多少,等待花了多少,CPU里面又分成解析、计划、执行这几个阶段。
SELECT
metric,
calls,
total_time,
avg_time,
dbtime_pct
FROM sys_stat_dbtime
ORDER BY total_time DESC;
这个视图有个前提:需要在配置文件里把track_sql和track_instance都设成on,不然没数据。
怎么看结果?就盯一个比例。如果DB CPU占了大头,那瓶颈在"算"上,得从SQL本身入手,加索引、减少计算量、改写法。如果FG Wait占比很高,那瓶颈在"等"上,得去查具体在等什么,是锁冲突还是IO跟不上。
1.5 一条SQL看整体健康度
如果只是想快速了解一下数据库的整体状况,跑sys_stat_database就够了。
SELECT
datname AS db_name,
numbackends AS current_connections,
xact_commit AS commits,
xact_rollback AS rollbacks,
ROUND(
xact_rollback::numeric /
NULLIF(xact_commit + xact_rollback, 0) * 100, 2
) AS rollback_pct,
ROUND(
blks_hit::numeric /
NULLIF(blks_hit + blks_read, 0) * 100, 2
) AS cache_hit_pct,
deadlocks,
temp_files,
temp_bytes
FROM sys_stat_database
WHERE datname IS NOT NULL
ORDER BY xact_commit DESC;
这里面有几个数字值得记住。缓存命中率正常要99%以上,低了说明内存不够或者全表扫描太多。回滚率偶尔有正常,但要是超过5%得去查应用逻辑。死锁数不是0就要排查并发顺序。临时文件在涨的话,说明排序或哈希操作在往磁盘溢写,work_mem可能给小了。
1.6 后台进程也要看看
检查点进程、后台写进程、WAL归档进程,这几个后台进程的状态不太起眼,但它们出问题前端查询就会跟着倒霉。
SELECT
checkpoints_timed AS scheduled_checkpoints,
checkpoints_req AS forced_checkpoints,
buffers_checkpoint,
buffers_clean,
buffers_backend,
ROUND(checkpoint_write_time::numeric / 1000, 2) AS write_seconds,
ROUND(checkpoint_sync_time::numeric / 1000, 2) AS sync_seconds
FROM sys_stat_bgwriter;
SELECT
archived_count,
last_archived_wal,
last_archived_time,
failed_count,
last_failed_wal,
last_failed_time
FROM sys_stat_archiver;
如果forced_checkpoints比scheduled_checkpoints多很多,说明检查点在频繁被WAL堆积"催着跑",可以适当调大max_wal_size让检查点别那么急。归档那边failed_count在涨的话,赶紧查归档命令和存储空间,归档堵住了WAL就删不掉,磁盘可能就被撑满。
二、EXPLAIN:调优之前先学会读执行计划
2.1 三种用法
不看执行计划就动手调优,跟闭着眼睛修车没什么区别。EXPLAIN有三种常见用法,区别在于给的信息量不同。
-- 第一种:只看计划,不真正执行。给出的是优化器的估算值,跑得快
EXPLAIN
SELECT * FROM orders WHERE order_id = 1001;
-- 第二种:真正执行一遍,拿到实际耗时、实际行数。排查时最常用
EXPLAIN ANALYZE
SELECT * FROM orders WHERE order_id = 1001;
-- 第三种:能开的选项全开,代价、缓冲区、时间、详细信息全都有
EXPLAIN (ANALYZE, VERBOSE, COSTS, BUFFERS, TIMING)
SELECT o.order_id, c.customer_name
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_date >= '2025-01-01';
日常排查用第二种就够。但要小心,EXPLAIN ANALYZE是真的会把SQL跑一遍的。查SELECT无所谓,但如果是要调优的UPDATE或DELETE,建议先包一层SELECT看,或者开个事务跑完再回滚,别把生产数据改了。
2.2 怎么读
执行计划的阅读顺序是从下往上、从右往左。最下面最右边的节点先执行,最上面最左边的是最后一步。
来一个例子:
Hash Join
(cost=25.00..1200.00 rows=500 width=80)
(actual time=1.200..35.600 rows=12000 loops=1)
Hash Cond: (o.order_id = i.order_id)
-> Seq Scan on orders o
(cost=0.00..500.00 rows=10000 width=60)
(actual time=0.010..8.200 rows=10000 loops=1)
-> Hash
-> Index Scan using idx_product on order_items i
(cost=0.29..20.00 rows=500 width=20)
(actual time=0.015..3.100 rows=12000 loops=1)
Index Cond: (product_id = 5001)
这里面有四样东西值得看。
第一,节点类型。Seq Scan是全表扫描,Index Scan是走索引,Hash Join是哈希连接,Nested Loop是嵌套循环。大表上出现Seq Scan通常要留个心眼。
第二,预估行数和实际行数的差距。这个例子里,order_items的rows写的是500,但actual rows是12000,差了24倍。这基本可以断定统计信息过时了,优化器在"瞎猜"。
第三,cost值。第一个数字是启动代价(多久能开始返回第一行),第二个是总代价。单个数字不好判断高低,但对比两个方案的时候看相对大小很有用。
第四,Buffers信息。如果在EXPLAIN里加了BUFFERS选项,会看到类似shared read=8320 hit=180的输出。read代表从磁盘读的块数,hit代表在内存里找到的。read越多,IO压力越大。
再看一个更直观的例子:
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM large_table
WHERE status = 'ACTIVE' AND create_time > '2025-06-01';
Seq Scan on large_table
(cost=0.00..15420.00 rows=1000 width=80)
(actual time=0.015..45.230 rows=50000 loops=1)
Filter: ((status = 'ACTIVE') AND (create_time > '2025-06-01'))
Rows Removed by Filter: 950000
Buffers: shared read=8320 hit=180
扫了一百万行,筛掉了95万行,只留下5万行。而且8320个数据块是从磁盘读的,在内存里命中的只有180个。这种情况下,加一个status加create_time的复合索引,效果会非常明显。
2.3 用它验证Hints有没有生效
后面会讲到用Hints干预优化器的决策。但加完Hints之后,一定要用EXPLAIN确认一下它到底听没听你的话。
EXPLAIN SELECT o.*, i.*
FROM orders o JOIN order_items i ON o.order_id = i.order_id;
EXPLAIN SELECT /*+ USE_HASH(o i) */ o.*, i.*
FROM orders o JOIN order_items i ON o.order_id = i.order_id;
对比两次输出,第一次如果是Nested Loop、第二次变成了Hash Join,那就说明Hints生效了。要是没变呢?十有八九是语法写错了。Hints本质是SQL注释,写错了不会报任何错误,数据库只会当没看见。
三、Hints调优:在优化器犯糊涂的时候拉它一把
3.1 什么时候需要Hints
优化器大多数时候是靠谱的。但它依赖统计信息做决策,统计信息不准确的时候它就会做出看起来合理、实际上很慢的选择。另外SQL写得太复杂或者数据分布严重不均匀的时候,优化器也容易判断失误。
Hints就是一种手动干预的手段。写法就是在SQL注释的开头加一个+号,告诉优化器"这里按我说的来"。
SELECT /*+ hint_name(表别名) */ ...
FROM 表名 别名
WHERE ...;
语法本身不难,但有几个细节容易踩坑。
+号必须紧贴/*后面,写成/* +(中间多了空格)就不生效。多个Hints用空格分隔。整段注释必须在一行里,不能换行。如果表起了别名,Hints里也得写别名,不能写真名。最后一条最容易被忽略:写错了不会报错,只会被默默跳过。
-- 正确
SELECT /*+ FULL(e) CACHE(e) */ last_name
FROM employee e
WHERE department_id = 10;
-- 错误:表有别名e,但Hint里写真名employee,不生效也不会报错
SELECT /*+ FULL(employee) */ last_name
FROM employee e
WHERE department_id = 10;
3.2 先想清楚目标
调优之前先问自己一个问题:这条SQL追求的是整体跑完最快,还是前几条数据尽快出来?
如果是跑报表、做数据迁移、批量处理这种场景,要的是整体吞吐量,用ALL_ROWS:
SELECT /*+ ALL_ROWS */ employee_id, last_name, salary, job_id
FROM employees
WHERE department_id = 20;
如果是用户翻页看列表、后台实时查状态这种场景,用户等不了全部数据出来,只要第一屏快就行。这时候用FIRST_ROWS,括号里的数字表示"希望前多少行尽快出来":
SELECT /*+ FIRST_ROWS(20) */ employee_id, last_name, salary
FROM employees
WHERE department_id = 20;
3.3 怎么读这张表:访问路径Hints
FULL:强制全表扫描
SELECT /*+ FULL(e) */ employee_id, last_name
FROM employee e
WHERE last_name LIKE 'S%';
强制走全表扫描听起来有点反直觉,但确实有适用场景。比如一张表80%的数据都符合WHERE条件,走索引反而要不停地回表查,不如直接从头扫到尾来得快。
INDEX和NO_INDEX:指定走哪个索引,或者不走哪个
-- 强制走指定索引
SELECT /*+ INDEX(employees emp_name_idx) */ *
FROM employees
WHERE last_name = 'Smith';
-- 排除某个索引,让优化器考虑其他选择
SELECT /*+ NO_INDEX(employees emp_name_idx) */ *
FROM employees
WHERE last_name = 'Smith';
表上有好几个索引的时候,优化器有时候会选一个不太合适的。INDEX让你指定它该走哪个,NO_INDEX让你把拖后腿的那个排除掉。这两个是配套使用的。
INDEX_DESC:倒着扫索引
SELECT /*+ INDEX_DESC(orders ord_date_idx) */ *
FROM orders
WHERE customer_id = 1001
ORDER BY order_date DESC
LIMIT 10;
"取最新N条"这种需求很常见。索引本身是有序的,倒着扫就能直接拿到最新数据,省掉一个额外的排序步骤。这个Hint在这种场景下特别管用。
3.4 先查谁后查谁:连接顺序Hints
多表关联查询里,先查哪张表后关联哪张表,对性能影响非常大。驱动表选错了,后面怎么关联都慢。
ORDERED的用法是让数据库按FROM子句里写的顺序来连接:
SELECT /*+ ORDERED */ *
FROM orders o, customers c, order_items i
WHERE o.customer_id = c.customer_id
AND o.order_id = i.order_id;
如果你清楚数据分布(比如知道order_items被WHERE条件过滤后只剩几十行,适合当驱动表),用这个最直接。
LEADING稍微灵活一点,只指定驱动表,后面的顺序让优化器自己决定:
SELECT /*+ LEADING(c o) */ *
FROM orders o, customers c
WHERE o.customer_id = c.customer_id;
实际用的时候LEADING比ORDERED更常见,因为它不用把所有表的顺序都写死。
3.5 怎么连:连接方式Hints
连接方式决定了两张表的数据怎么配对。选对了可能快几十倍,选错了也可能慢几十倍。
哈希连接(USE_HASH)
SELECT /*+ USE_HASH(o i) */ *
FROM orders o, order_items i
WHERE o.order_id = i.order_id
AND o.order_id > 2400;
把一张表按连接键建成哈希表,另一张表逐行去匹配。两张大表做等值连接的时候,哈希连接通常是首选。前提是内存够用(work_mem参数要给够),不然哈希表放不下就得溢写到磁盘,反而更慢。
嵌套循环(USE_NL)
SELECT /*+ USE_NL(l h) */ h.customer_id, l.unit_price * l.quantity
FROM orders h, order_items l
WHERE l.order_id = h.order_id;
-- 还可以指定内表走哪个索引
SELECT /*+ USE_NL_WITH_INDEX(l item_product_ix) */ *
FROM orders h, order_items l
WHERE l.order_id = h.order_id
AND l.order_id > 2400;
嵌套循环的逻辑就是外层每取一行,就去内层查一次。它适合外层结果集小、内层有高效索引的场景。如果内层没索引,那就等于外层每来一行就做一次全表扫描,那是灾难。
排序合并(USE_MERGE)
SELECT /*+ USE_MERGE(employee department) */ *
FROM employee, department
WHERE employee.department_id = department.department_id;
两边各自排序,然后像合并两个有序数组一样配对。数据本身已经有序的时候用这个很合适,或者连接条件不是等值(比如大于、小于)的时候。
怎么选?简单记一下就行:两张大表等值连接优先试Hash Join,一小一大且内表有索引试Nested Loop,数据已经有序或非等值连接考虑Merge Join。不确定的话先跑EXPLAIN看优化器选的什么,再用Hint换一种试试看。
有一点要强调:连接方式的Hint最好配合LEADING或ORDERED一起用。因为USE_HASH(A B)里的A是构建表、B是探测表,你不指定顺序的话优化器可能把角色搞反,Hint就直接被忽略了。
3.6 并行执行
-- 开4个进程同时查
SELECT /*+ PARALLEL(orders 4) */ *
FROM orders
WHERE order_date >= '2025-01-01';
-- 关掉并行
SELECT /*+ NO_PARALLEL(orders) */ *
FROM orders
WHERE customer_id = 1001;
-- 批量插入也能并行
INSERT /*+ ENABLE_PARALLEL_DML PARALLEL(target_table, 16) */
INTO target_table
SELECT * FROM source_table;
并行不是万能的。数据量小的时候进程间通信的开销比省下来的时间还多。一般表超过百万行、查询里有聚合或排序的时候并行才真正有用。
3.7 管住优化器的"自作主张"
优化器有时候会自动改写SQL,比如把OR条件拆成UNION ALL,或者把子查询展开跟外层合并。大部分时候改写是对的,但偶尔会帮倒忙。这时候可以用Hint把它管住。
-- 不让优化器拆OR
SELECT /*+ NO_EXPAND */ *
FROM employee e, department d
WHERE e.manager_id = 108
OR d.department_id = 110;
-- 反过来,强制拆OR
SELECT /*+ USE_CONCAT */ *
FROM employee e
WHERE manager_id = 108
OR department_id = 110;
-- 不让优化器把视图合并进外层查询
SELECT /*+ NO_MERGE(v) PUSH_PRED(v) */ *
FROM employee e,
(SELECT manager_id FROM employee) v
WHERE e.manager_id = v.manager_id(+)
AND e.employee_id = 100;
3.8 几个零散但好用的Hint
-- 统计信息不准的时候,让优化器临时采样
SELECT /*+ DYNAMIC_SAMPLING(e 4) */ count(*)
FROM employee e
WHERE salary > 10000;
-- 给查询块起个名字,方便从外面引用
SELECT /*+ QB_NAME(qb) FULL(@qb e) */ employee_id, last_name
FROM employee e
WHERE last_name = 'Smith';
-- 让子查询提前执行(默认是最后才跑的)
SELECT *
FROM orders o
WHERE o.customer_id IN (
SELECT /*+ PUSH_SUBQ */ customer_id
FROM customers
WHERE region = 'EAST'
);
-- 大批量插入用直接路径,跳过缓冲区直接写到表末尾,快很多
INSERT /*+ APPEND */ INTO target_table
SELECT * FROM source_table;
APPEND多说两句。它只对INSERT INTO ... SELECT有效,INSERT INTO ... VALUES的写法用不了。另外用APPEND插入的数据在提交之前当前会话看不到,所以用完记得及时COMMIT。
四、完整走一遍:一条30秒的慢查询怎么优化到0.5秒
第一步:发现慢查询
那天运维告警说有条SQL跑了很久没结束,先查sys_stat_activity:
SELECT pid, query, now() - query_start AS duration
FROM sys_stat_activity
WHERE state = 'active'
AND now() - query_start > interval '5 seconds';
结果里揪出来这么一条,已经跑了30秒:
SELECT o.order_id, o.total_amount, c.customer_name
FROM orders o
JOIN order_items i ON o.order_id = i.order_id
JOIN customers c ON o.customer_id = c.customer_id
WHERE i.product_id = 5001
ORDER BY o.order_date DESC;
第二步:看执行计划
EXPLAIN (ANALYZE, BUFFERS, COSTS)
SELECT o.order_id, o.total_amount, c.customer_name
FROM orders o
JOIN order_items i ON o.order_id = i.order_id
JOIN customers c ON o.customer_id = c.customer_id
WHERE i.product_id = 5001
ORDER BY o.order_date DESC;
出来的执行计划大概是这样(简化过的):
Nested Loop
(cost=0.58..38000.00 rows=50 width=80)
(actual time=0.80..30000.00 rows=12000 loops=1)
-> Index Scan using idx_product on order_items i
Index Cond: (product_id = 5001)
(actual rows=12000)
-> Index Scan using orders_pkey on orders o
Index Cond: (order_id = i.order_id)
(actual time=0.002..2.500 rows=1 loops=12000)
看到问题了。优化器选的是嵌套循环,order_items筛出了12000行,但优化器以为只有50行。然后每一行都要去orders表做一次索引扫描,12000次乘以每次2.5毫秒,光这一步就30秒。
根因很清楚:统计信息过期,行数估错了240倍,导致优化器选了一个完全不适合当前数据量的连接方式。
第三步:先更新统计信息
ANALYZE order_items;
ANALYZE orders;
再跑一遍查询,耗时从30秒降到了8秒。有进步,但还是不够快。再看执行计划,优化器这回换了排序合并连接,两边都要先排序再合并,额外的排序开销把速度又拖回去了。
第四步:用Hint纠正
对这种大表等值关联的场景,哈希连接才是正确答案。
SELECT /*+ LEADING(i o c) USE_HASH(o c) */
o.order_id, o.total_amount, c.customer_name
FROM order_items i
JOIN orders o ON o.order_id = i.order_id
JOIN customers c ON o.customer_id = c.customer_id
WHERE i.product_id = 5001
ORDER BY o.order_date DESC;
第五步:验证
EXPLAIN (ANALYZE, BUFFERS)
SELECT /*+ LEADING(i o c) USE_HASH(o c) */
o.order_id, o.total_amount, c.customer_name
FROM order_items i
JOIN orders o ON o.order_id = i.order_id
JOIN customers c ON o.customer_id = c.customer_id
WHERE i.product_id = 5001
ORDER BY o.order_date DESC;
执行计划确认变成了Hash Join。实际耗时0.5秒,比最初快了60倍。
第六步:防止复发
Hint解决了眼前的问题,但统计信息为什么过期?如果这个问题不解决,以后还会再犯。
SELECT relname, last_autovacuum, last_autoanalyze,
n_mod_since_analyze
FROM sys_stat_all_tables
WHERE relname = 'order_items';
n_mod_since_analyze很大就说明这张表改了大量数据但一直没重新分析。可以让自动分析跑得勤一点:
ALTER TABLE order_items SET (autovacuum_analyze_scale_factor = 0.05);
把整个流程串起来就是:发现问题,看执行计划找瓶颈,先更新统计信息治本,再用Hint治标,验证效果,最后调autovacuum策略防止复发。
五、几个容易踩的坑
关于Hints
写错了不会报错。这一点强调多少次都不够。Hint本质是注释,拼错一个字母、多了个空格、表名写成了真名而不是别名,数据库都不会告诉你,只会默默忽略然后跑它自己选的计划。所以每次加完Hint一定要用EXPLAIN确认。
数据量变了Hint可能帮倒忙。今天哈希连接是最优的,半年后数据翻了一百倍可能嵌套循环反而更快。加过Hint的SQL不能设完就不管了,得定期复查。
不要上来就加Hint。遇到慢查询先检查SQL写法、看有没有该加的索引、统计信息是不是最新的。如果一个索引就能解决问题,那就没必要用Hint。索引是治本,Hint是治标。
关于系统视图
不用每天把所有视图查一遍,没那个必要也不现实。节奏可以这样安排:
每天花5分钟,跑一遍sys_stat_activity看有没有慢查询,sys_stat_database看缓存命中率和死锁。
每周花半小时,查sys_stat_all_tables看死元组堆积,查sys_stat_all_indexes找僵尸索引。
出了问题再针对性地深挖。
这里附一个每日巡检用的脚本,可以直接拿去用:
-- 活跃会话
SELECT pid, usename, state,
now() - query_start AS duration,
LEFT(query, 80) AS sql_preview
FROM sys_stat_activity
WHERE state = 'active'
AND now() - query_start > interval '3 seconds'
ORDER BY duration DESC LIMIT 5;
-- 缓存命中率
SELECT datname,
ROUND(blks_hit::numeric / NULLIF(blks_hit + blks_read, 0) * 100, 2) AS hit_pct
FROM sys_stat_database
WHERE datname IS NOT NULL;
-- 死元组最多的5张表
SELECT relname, n_dead_tup, n_live_tup,
ROUND(n_dead_tup::numeric / NULLIF(n_live_tup, 0) * 100, 1) AS dead_pct
FROM sys_stat_all_tables
WHERE schemaname NOT IN ('sys_catalog', 'information_schema')
ORDER BY n_dead_tup DESC LIMIT 5;
关于调优顺序
最后说一下整体思路。遇到性能问题,很多人第一反应是调参数或者加Hint。其实投入产出比最高的是先改SQL写法,一行代码能搞定的事别搞复杂。然后看要不要加索引,大部分性能问题到这里就解决了。如果还不够,再更新统计信息、用Hint微调、调参数,最后才考虑升硬件。
改写SQL → 加索引 → 更新统计信息 → 用Hint → 调参数 → 升硬件
越靠前的成本越低、见效越快。跳过前面直接上后面的手段,不是不行,但大概率会走弯路。
写在最后
系统视图负责发现问题,EXPLAIN负责分析问题,Hints负责解决问题。三样东西配合起来,就是一套完整的调优工具链。
核心原则就一句话:先用视图看清楚出了什么问题,再用EXPLAIN确认问题出在执行计划的哪个环节,最后用Hint在关键位置做调整。每一步都验证,每一步都有依据,别靠猜。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)