前言

数据库慢了,日志里翻不出原因,应用层看不出来毛病,但就是慢。

这种情况太多了。问题可能藏在锁等待里,可能是统计信息太久没更新导致执行计划走偏,也可能就是一个该加的索引一直没人加。这篇文章的思路很简单:用系统视图把问题翻出来,用EXPLAIN看明白执行计划,再用Hints在关键位置做调整。三步走,不绕弯。

一、系统视图:数据库内部的"监控探头"

1.1 先知道有什么能用

系统视图不是真正的表,它是数据库暴露给外部的一组虚拟表,里面装的是数据库运行过程中产生的各种元数据和统计数据。你查它,就能知道数据库内部正在发生什么。

不用全记。按用途分个类,脑子里有个大概就行:

类别 代表视图 干什么用的
元信息 sys_classsys_attributesys_index 看表结构、列定义、索引信息
会话与活动 sys_stat_activitysys_stat_database 谁在连、跑什么SQL、忙不忙
表和索引统计 sys_stat_all_tablessys_stat_all_indexes 全表扫描多不多、索引有没有人用
时间模型 sys_stat_dbtimesys_stat_sqltime 时间花在哪:解析、执行还是干等
锁等待 sys_lockssys_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_sqltrack_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_checkpointsscheduled_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_itemsrows写的是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个。这种情况下,加一个statuscreate_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;

实际用的时候LEADINGORDERED更常见,因为它不用把所有表的顺序都写死。

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最好配合LEADINGORDERED一起用。因为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在关键位置做调整。每一步都验证,每一步都有依据,别靠猜。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐