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、当前是什么状态。

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在抢同一张表甚至同一行。

如果嫌上面那条查出来的信息太多,这里有个更聚焦的版本,专门找那些"卡住了"的会话:

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查到。

sql 复制代码
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:

sql 复制代码
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里面又分成解析、计划、执行这几个阶段。

sql 复制代码
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就够了。

sql 复制代码
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归档进程,这几个后台进程的状态不太起眼,但它们出问题前端查询就会跟着倒霉。

sql 复制代码
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;
sql 复制代码
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有三种常见用法,区别在于给的信息量不同。

sql 复制代码
-- 第一种:只看计划,不真正执行。给出的是优化器的估算值,跑得快
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压力越大。

再看一个更直观的例子:

sql 复制代码
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确认一下它到底听没听你的话。

sql 复制代码
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注释的开头加一个+号,告诉优化器"这里按我说的来"。

sql 复制代码
SELECT /*+ hint_name(表别名) */ ...
FROM 表名 别名
WHERE ...;

语法本身不难,但有几个细节容易踩坑。

+号必须紧贴/*后面,写成/* +(中间多了空格)就不生效。多个Hints用空格分隔。整段注释必须在一行里,不能换行。如果表起了别名,Hints里也得写别名,不能写真名。最后一条最容易被忽略:写错了不会报错,只会被默默跳过。

sql 复制代码
-- 正确
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:

sql 复制代码
SELECT /*+ ALL_ROWS */ employee_id, last_name, salary, job_id
FROM employees
WHERE department_id = 20;

如果是用户翻页看列表、后台实时查状态这种场景,用户等不了全部数据出来,只要第一屏快就行。这时候用FIRST_ROWS,括号里的数字表示"希望前多少行尽快出来":

sql 复制代码
SELECT /*+ FIRST_ROWS(20) */ employee_id, last_name, salary
FROM employees
WHERE department_id = 20;

3.3 怎么读这张表:访问路径Hints

FULL:强制全表扫描
sql 复制代码
SELECT /*+ FULL(e) */ employee_id, last_name
FROM employee e
WHERE last_name LIKE 'S%';

强制走全表扫描听起来有点反直觉,但确实有适用场景。比如一张表80%的数据都符合WHERE条件,走索引反而要不停地回表查,不如直接从头扫到尾来得快。

INDEX和NO_INDEX:指定走哪个索引,或者不走哪个
sql 复制代码
-- 强制走指定索引
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:倒着扫索引
sql 复制代码
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子句里写的顺序来连接:

sql 复制代码
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稍微灵活一点,只指定驱动表,后面的顺序让优化器自己决定:

sql 复制代码
SELECT /*+ LEADING(c o) */ *
FROM orders o, customers c
WHERE o.customer_id = c.customer_id;

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

3.5 怎么连:连接方式Hints

连接方式决定了两张表的数据怎么配对。选对了可能快几十倍,选错了也可能慢几十倍。

哈希连接(USE_HASH)
sql 复制代码
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)
sql 复制代码
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)
sql 复制代码
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 并行执行

sql 复制代码
-- 开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把它管住。

sql 复制代码
-- 不让优化器拆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

sql 复制代码
-- 统计信息不准的时候,让优化器临时采样
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:

sql 复制代码
SELECT pid, query, now() - query_start AS duration
FROM sys_stat_activity
WHERE state = 'active'
  AND now() - query_start > interval '5 seconds';

结果里揪出来这么一条,已经跑了30秒:

sql 复制代码
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;

第二步:看执行计划

sql 复制代码
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倍,导致优化器选了一个完全不适合当前数据量的连接方式。

第三步:先更新统计信息

sql 复制代码
ANALYZE order_items;
ANALYZE orders;

再跑一遍查询,耗时从30秒降到了8秒。有进步,但还是不够快。再看执行计划,优化器这回换了排序合并连接,两边都要先排序再合并,额外的排序开销把速度又拖回去了。

第四步:用Hint纠正

对这种大表等值关联的场景,哈希连接才是正确答案。

sql 复制代码
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;

第五步:验证

sql 复制代码
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解决了眼前的问题,但统计信息为什么过期?如果这个问题不解决,以后还会再犯。

sql 复制代码
SELECT relname, last_autovacuum, last_autoanalyze,
       n_mod_since_analyze
FROM sys_stat_all_tables
WHERE relname = 'order_items';

n_mod_since_analyze很大就说明这张表改了大量数据但一直没重新分析。可以让自动分析跑得勤一点:

sql 复制代码
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找僵尸索引。

出了问题再针对性地深挖。

这里附一个每日巡检用的脚本,可以直接拿去用:

sql 复制代码
-- 活跃会话
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在关键位置做调整。每一步都验证,每一步都有依据,别靠猜。

相关推荐
姚远Oracle ACE2 分钟前
对比测试了 openGauss、PostgreSQL、Oracle、MySQL 的CPU 指令数
mysql·postgresql·oracle
CJi0NG38 分钟前
【自用】MySQL-多表查询
数据库·mysql
雨落在了我的手上44 分钟前
MySQL数据库基础(12):事务
数据库·mysql
D姐聊门店1 小时前
门店收银系统断网兜底怎么设计:本地账本、离线续传与日终对账的工程实现
数据库·saas
CCblandy1 小时前
Agent 工具调用怎么编排?本机实测:并行只到 2.63 倍,慢的先发省 30ms,CPU 活加 worker 无效
人工智能·后端·python·性能优化
用户8662978480081 小时前
抓包看 KingbaseES 内部协议?官方给了个现成工具
数据库
wefg11 小时前
【Redis】安装与启动
数据库·redis·缓存
大圣编蚕2 小时前
常见的 Redis 面试题(高频整理)
数据库·redis·缓存
派小心.2 小时前
网站统计采样数据的误差核对
大数据·服务器·前端·数据库·人工智能·小程序·数据分析
龙腾AI白云2 小时前
数字孪生、具身智能、世界模型三者交叉
数据库·人工智能·机器学习·flask·pygame