1. 先说点实在的:这份合集到底解决什么问题
做后端开发和数据库运维这几年,我最大的感受就是:PostgreSQL 的功能太强了,但常用的就那几十条 SQL,每次都去翻官方文档实在浪费生命。昨天刚写过的查询,过两周又要想半天语法;MySQL 转过来的同事,天天把 LIMIT 1 挂在嘴边,完全忘了 PostgreSQL 的 FETCH FIRST 才是标准写法。这种零散的记忆碎片,攒着攒着就丢了一大半。
所以我决定把日常工作中真正高频使用的 SQL 整理成一份合集,按照使用场景分类,每一条都附上适用场景和踩坑记录。这份合集不是教科书,不是让你背语法,而是给你一种"关键时刻能直接抄作业"的底气。适用对象包括三类人:
- 后端开发者:每天都在跟数据打交道,需要快速查询表结构、分析慢查询、处理数据更新。
- 数据库运维/ DBA:需要排查连接异常、检查锁冲突、监控数据库膨胀状态。
- 数据分析师或刚接触 PostgreSQL 的转岗同学:对系统表不熟悉,想快速找到"别人口中那条查询数据量的 SQL"。
从 PostgreSQL 9.6 到 15、16 我都实际用过,这套合集里的绝大多数 SQL 都能跨版本运行,个别新特性我会单独标出最低版本要求。文章会持续更新,每一批都在原有的分类下补充新的实战语句。
2. 查信息、看家底:连接库表的基础查询集合
2.1 版本、库与连接数检查
拿到一套陌生的环境,第一件事永远是搞清楚"我在哪、库有多大、连接有没有打满"。我刚接手一套业务系统的时候,第一步就是跑这些查询:
-- 当前 PostgreSQL 版本 SELECT version(); -- 当前连接数 SELECT count(*) FROM pg_stat_activity; -- 各库大小(单位 MB) SELECT datname, pg_size_pretty(pg_database_size(datname)) AS db_size FROM pg_database ORDER BY pg_database_size(datname) DESC;这里有个容易踩的坑:连接数不要只看count(*),很多连接可能是 idle 状态,完全不影响性能。要判断连接是否够用,得看state的分布:
SELECT state, count(*) AS conn_count FROM pg_stat_activity GROUP BY state ORDER BY conn_count DESC;正常情况下大部分连接应该是idle或active。如果出现大量idle in transaction,那基本就是应用代码里事务没及时提交,这种问题常见于使用连接池忘记关闭事务的情况。
pg_size_pretty()这个函数是 PostgreSQL 独有的,能把字节数自动转成 KB、MB、GB 这种可读格式,比我之前用 MySQL 时自己写除法换算方便太多了。库大小的查询在做迁移评估、容量规划时特别有用。
2.2 表大小、行数与膨胀判断
业务表到底有多大、多少行、膨胀了多少,这是 DBA 和开发者都关心的问题。下面这条 SQL 会把当前模式下的表按总大小排序,一次性看清楚所有表的"体重":
SELECT schemaname AS schema_name, tablename AS table_name, pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) AS total_size, pg_size_pretty(pg_relation_size(schemaname || '.' || tablename)) AS table_size, pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename) - pg_relation_size(schemaname || '.' || tablename)) AS index_size FROM pg_tables WHERE schemaname NOT IN ('pg_catalog', 'information_schema') ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC;注意pg_total_relation_size包含表数据加索引加 TOAST,pg_relation_size只算表本身的堆文件。两者之差基本就是索引占用的空间。这是判断"到底是表大还是索引大"的最快手段。
关于行数:count(*)在大表上会全表扫描,生产环境不建议频繁执行。PostgreSQL 的统计信息里维护了一个行数估算值,可以这样拿:
SELECT relname AS table_name, n_live_tup AS estimate_rows FROM pg_stat_user_tables WHERE relname = 'your_table_name';这个数字不是精确值,可能有百分之几甚至更多的偏差,但用来做容量评估足够。如果要精确行数,只能count(*),建议在业务低峰期执行。我在一次大促前评估订单表容量时,就是用这个统计信息快速判断出了哪个分区表膨胀最严重,省去了扫表的耗时。
2.3 表结构与字段信息
开发同学最常问的问题是:"这张表的字段都有啥?"PostgreSQL 里查字段信息靠的是information_schema.columns或者系统表pg_attribute。日常我用后者更多,因为可以顺便拿到注释:
SELECT a.attname AS column_name, format_type(a.atttypid, a.atttypmod) AS data_type, col_description(a.attrelid, a.attnum) AS column_comment FROM pg_attribute a WHERE a.attrelid = 'your_table_name'::regclass AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum;这里有个细节:a.attnum > 0是为了排除系统隐藏列,NOT a.attisdropped排除被删除的列。如果你写过 ALTER TABLE DROP COLUMN,就知道 PostgreSQL 其实不是物理删除列,而是标记一下,所以这个过滤条件不能少。
查外键关系的 SQL 也经常用,特别是拆表、拆库的时候:
SELECT tc.constraint_name, kcu.column_name AS source_column, ccu.table_name AS target_table, ccu.column_name AS target_column FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = tc.constraint_name WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = 'your_table_name';在数据迁移时,外键关系是必须提前梳理的,不然导入顺序一错就会报主键冲突。
3. 索引与性能诊断的黄金搭配
3.1 查看索引与索引使用率
索引是 PostgreSQL 性能的核心。我见过不少线上表,索引建得密密麻麻,但实际查询根本没走到。一条 SQL 看出一张表的所有索引:
SELECT indexname, pg_size_pretty(pg_relation_size(indexname::regclass)) AS index_size, indexdef FROM pg_indexes WHERE tablename = 'your_table_name';indexdef列会输出完整的 CREATE INDEX 语句,这对排查"索引走了没走"非常有帮助,因为你可以看到她到底是 B-tree、GIN 还是 BRIN。
比看索引更重要的,是看索引的使用率:
SELECT schemaname, relname AS table_name, indexrelname AS index_name, idx_scan AS index_scan_times, idx_tup_read AS rows_read_by_index, idx_tup_fetch AS rows_fetched_by_index FROM pg_stat_user_indexes ORDER BY idx_scan ASC;执行频率排在最后的索引,大概率是没用的索引。我处理过一次线上磁盘占用告警,排查下来发现某张订单历史表有 6 个索引,其中 3 个的idx_scan是 0。跟业务方确认后直接 DROP,磁盘压力瞬间缓解。当然,删除索引前一定要在测试环境复现查询计划,确认没有 SQL 依赖这个索引。
3.2 慢查询与正在执行的 SQL
PostgreSQL 排查慢查询,第一站永远是pg_stat_statements。这个模块默认没开启,需要配置shared_preload_libraries = 'pg_stat_statements'并重启库。开启后查询累计耗时最长的 SQL:
SELECT queryid, calls AS exec_count, round(total_exec_time::numeric, 2) AS total_ms, round(mean_exec_time::numeric, 2) AS avg_ms, round(max_exec_time::numeric, 2) AS max_ms, query FROM pg_stat_statements WHERE query NOT LIKE '%pg_stat_statements%' ORDER BY total_exec_time DESC LIMIT 20;注意版本差异:PostgreSQL 13 之前的时间字段叫total_time、mean_time,13 之后改成了total_exec_time、mean_exec_time。我在 11 升 14 的时候被这个变化坑过一次,脚本直接报字段不存在。
如果不想等统计模块,也可以直接抓正在执行的慢 SQL:
SELECT pid, now() - query_start AS duration, state, wait_event_type || ': ' || wait_event AS wait_info, query FROM pg_stat_activity WHERE state = 'active' AND query NOT LIKE '%pg_stat_activity%' ORDER BY duration DESC;这个查询非常实用。生产环境突然 CPU 飙升,十有八九是某条 SQL 在裸跑大表,用这条 SQL 一看就知道是谁在捣乱。duration字段用now() - query_start计算,比query_start裸看直观得多。
3.3 锁冲突与死锁快速定位
业务卡住了,大概率不是慢查询,而是锁冲突。PostgreSQL 查锁的经典视图是pg_locks,但直接看pg_locks太碎,我习惯用下面的关联查询,直接找出"谁堵了谁":
SELECT blocked.pid AS blocked_pid, blocked.query AS blocked_query, blocking.pid AS blocking_pid, blocking.query AS blocking_query, blocked.wait_event_type || ': ' || blocked.wait_event AS wait_info FROM pg_stat_activity blocked JOIN pg_stat_activity blocking ON blocked.pid = ANY( SELECT pid FROM pg_stat_activity WHERE pg_blocking_pids(blocked.pid) @> ARRAY[blocked.pid] ) WHERE blocked.wait_event_type = 'Lock';这段 SQL 写起来有一点绕,我解释一下思路:pg_blocking_pids(blocked.pid)是 PostgreSQL 自带的函数,返回阻塞指定 pid 的进程数组。通过@>数组包含操作符找到阻塞源,再 self join 拿到两边各自的完整 SQL。
拿到阻塞源后,对症下手。state = 'idle in transaction'的会话往往是只开了事务没提交,可以直接pg_terminate_backend(blocking_pid)。但话放在前面:终止连接是外科手术式操作,先通知业务方,确认不是正在跑的关键任务再动手。
3.4 表膨胀与 VACUUM 状态
表膨胀是 PostgreSQL 特有的运维问题。更新和删除都会产生死元组,autovacuum 干活不及时,表就会越来越肿。查膨胀状态:
SELECT relname AS table_name, n_live_tup AS live_rows, n_dead_tup AS dead_rows, CASE WHEN n_live_tup > 0 THEN round(n_dead_tup::numeric / n_live_tup, 2) ELSE 0 END AS dead_ratio FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 20;dead_ratio超过 0.2 就该关注了,超过 0.5 说明 autovacuum 已经跟不上了。这时可以手动执行VACUUM (VERBOSE, ANALYZE) your_table_name;来兜底。注意生产环境执行 VACUUM 会消耗 IO,尽量在业务低峰期做。
4. 高频 DML:更新、删除、去重的最佳实践
4.1 INSERT ... ON CONFLICT 做 upsert 的妙用
PostgreSQL 的ON CONFLICT语法是我从 MySQL 转过来后觉得最好用的功能之一。一条 SQL 搞定"存在就更新,不存在就插入",再也不用先 SELECT 判断再决定走 INSERT 还是 UPDATE:
INSERT INTO user_points (user_id, points, updated_at) VALUES (1001, 50, now()) ON CONFLICT (user_id) DO UPDATE SET points = user_points.points + EXCLUDED.points, updated_at = now();EXCLUDED是 PostgreSQL 里的特殊行引用,表示那些"因为冲突而被丢弃的候选插入行"。上面的写法可以用在积分累加场景:同一用户有新积分进来,直接叠加。如果只想忽略冲突、不做更新,就写成DO NOTHING。
这里有一个隐藏的坑:ON CONFLICT需要唯一索引或主键支撑。冲突判断依据就是你在括号里写的那个字段上的唯一约束。如果没建唯一索引,执行时直接报there is no unique or exclusion constraint matching the ON CONFLICT specification。所以建表时一定要把业务上"唯一"的字段声明成唯一约束。
4.2 用 CTE 串联更新步骤,告别嵌套子查询
复杂业务逻辑里,经常需要"先算出一个结果集,再根据结果更新主表"。用 WITH 子句能大幅提高可读性:
WITH recent_orders AS ( SELECT customer_id, sum(amount) AS total_amount FROM orders WHERE created_at >= now() - interval '7 days' GROUP BY customer_id ) UPDATE customers c SET last_7d_order_amount = r.total_amount FROM recent_orders r WHERE c.id = r.customer_id;PostgreSQL 的 UPDATE 支持FROM子句,可以直接跟另一个表或 CTE 做关联更新。这在 MySQL 里写起来要麻烦得多。同类写法还可以用在删除上:DELETE FROM ... USING ... WHERE ...。
4.3 删除重复数据,只保留一条
业务表缺唯一约束导致重复数据,这种场景我见过太多次了。CTID 是 PostgreSQL 里的物理行标识,每一行都有独立的 ctid,利用它删除重复记录特别巧妙:
DELETE FROM user_emails e USING user_emails e2 WHERE e.email = e2.email AND e.ctid < e2.ctid;原理:e.ctid < e2.ctid表示在同一个 email 分组下,保留 ctid 最大的那条,删除其余所有。这比先查出来再逐条 DELETE 效率高一个数量级。执行前务必先跑一遍 SELECT 确认要删的数量:
SELECT count(*) FROM user_emails e JOIN user_emails e2 ON e.email = e2.email WHERE e.ctid < e2.ctid;4.4 批量删除历史数据,控制单次删除大小
一次性 DELETE 几百万行数据,通常会锁表锁很久,还把 WAL 日志撑爆。稳妥的方案是分批删除。用ctid范围或者时间窗口都可以:
-- 每批删除 1 万条,循环执行,直到影响行数为 0 DELETE FROM audit_logs WHERE created_at < now() - interval '90 days' AND ctid IN ( SELECT ctid FROM audit_logs WHERE created_at < now() - interval '90 days' LIMIT 10000 );分批删除为什么有效?因为每次 DELETE 事务都很短,锁持有时间就短,其他业务查询的阻塞时间会显著下降。ctid子查询的方式能保证每次删除的行分散在物理存储上,避免连续删除造成页面热点。
我自己在大促后清理过期订单流水时,会配合pg_sleep在每批之间暂停几秒,给主从同步留出缓冲时间。循环脚本可以用 shell 或任何语言写,核心就是"一批删完看影响行数,不为 0 就继续"。
4.5 RETURNING 拿到操作后的实时数据
UPDATE 或 INSERT 之后需要立刻用新数据继续业务逻辑,很多人会选择重新 SELECT 一次。其实 PostgreSQL 的RETURNING可以直接把变更后的行返回:
UPDATE inventory SET quantity = quantity - 5 WHERE product_id = 99 RETURNING product_id, quantity AS new_quantity;应用层代码里就可以省掉一次查询。比如扣库存后要返回剩余数量给前端,不用再查库,代码路径短了一半。
5. 函数与特殊类型:日期、JSON、数组的高频玩法
5.1 日期时间处理:date_trunc 与 interval
PostgreSQL 的日期时间函数非常强大,date_trunc用来按天、按小时、按月分组统计特别方便:
SELECT date_trunc('day', created_at) AS day, count(*) AS order_count FROM orders GROUP BY date_trunc('day', created_at) ORDER BY day DESC LIMIT 30;date_trunc支持'minute'、'hour'、'day'、'week'、'month'、'quarter'、'year'等粒度,做报表时几乎取代了繁琐的DATE_FORMAT+GROUP BY组合。注意它返回的是 timestamp 类型,不是字符串,所以排序和后续计算都更自然。
算两个时间点之间的天数差:
SELECT extract(epoch FROM (now() - created_at)) / 86400 AS days_ago FROM orders ORDER BY created_at DESC LIMIT 1;extract(epoch FROM interval)会把时间间隔转成秒数,再除以 86400 换算成天。这个方法比直接减时间再转天数要精确得多,不受夏令时等时区变化影响。
5.2 JSONB 操作:取值、更新、查询一次说清
PostgreSQL 的 JSONB 类型几乎是半个文档数据库。日常操作主要集中在四个运算符:
->返回 JSONB 类型,用于继续链路操作。->>返回文本类型,用于普通字符串比较。#> 或 #>>根据路径取深层值。@>判断 JSONB 是否包含某个结构。
实际场景,从字段里取用户信息:
SELECT >UPDATE user_events SET data = jsonb_set(data, '{address, city}', '"新城市"', true) WHERE id = 1;这里有个 version 注意点:jsonb_set的第四个参数create_missing控制路径不存在时是否自动创建。PostgreSQL 14 之前必须显式传,14 之后默认值变更为true,不传也能自动创建。这是升级后行为变化的典型案例,代码在 13 上能跑,到 14 上行为突然不一样了。
5.3 数组与聚合:unnest、array_agg、string_agg
unnest是处理数组和 JSON 数组的利器。比如某张 tags 字段存的是逗号分隔的字符串,想拆成多行查统计:
SELECT tag, count(*) AS tag_count FROM user_articles, unnest(string_to_array(tags, ',')) AS tag GROUP BY tag ORDER BY tag_count DESC;LATERAL是隐式发生的,unnest在 FROM 中展开后自然与主表形成关联。string_to_array负责把 CSV 转数组,再由unnest展开成行。
反过来,把多行聚合成一行用array_agg或string_agg:
SELECT order_id, string_agg(product_name, '、' ORDER BY product_name) AS product_list FROM order_items GROUP BY order_id;string_agg的排序参数是可选的,但建议总是带上。不指定顺序时,字符串拼接顺序不可控,常见于多并发执行时结果不稳定。
6. 权限、备份与日常维护的必背 SQL
6.1 用户与权限快速查询
新同学接手系统,搞不清谁对哪个库有什么权限?下面这条直接列出所有非系统用户的角色和归属:
SELECT r.rolname AS role_name, r.rolsuper AS is_super, r.rolcreatedb AS can_create_db, r.rolcanlogin AS can_login, ARRAY(SELECT b.rolname FROM pg_auth_members m JOIN pg_roles b ON b.oid = m.roleid WHERE m.member = r.oid ) AS member_of FROM pg_roles r WHERE r.rolname NOT LIKE 'pg_%' ORDER BY r.rolname;给业务账号只读权限是标配操作:
GRANT CONNECT ON DATABASE your_db TO readonly_user; GRANT USAGE ON SCHEMA public TO readonly_user; GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_user; -- 对将来新建的表也自动授予权限 ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly_user;ALTER DEFAULT PRIVILEGES这行特别关键。如果只执行前三条,新表创建后只读账号依然看不到。我在一个数据分析平台上吃过这个亏:报表账号头一天还能查,第二天开发建了新表,查询直接报权限不足。找了一圈原因才发现漏了默认权限。
6.2 VACUUM 与 ANALYZE 不只是命令
很多人把 VACUUM 和 ANALYZE 当成同一个概念,其实分工不同。VACUUM 回收死元组占用的存储空间,ANALYZE 更新统计信息供查询计划器使用。二者经常连用,但触发时机不一样:
-- 只更新统计信息,不加锁,适合频繁执行 ANALYZE your_table_name; -- 回收空间并更新统计信息,依旧在线,不会锁表阻塞读写 VACUUM (ANALYZE, VERBOSE) your_table_name;关于 VACUUM 有个经典误解:VACUUM 不回收空间给操作系统,而是交还给 PostgreSQL 内部。所以执行完成后用ls -lh看文件大小可能没变化,只有pg_size_pretty(pg_total_relation_size(...))变小才说明真正回收了。真要缩文件给操作系统,只能VACUUM FULL,但这会拿到 ACCESS EXCLUSIVE 锁,生产环境必须极其谨慎,我通常只在维护窗口使用。
Tracking autovacuum 是否卡住,看这个视图:
SELECT schemaname, relname, last_autovacuum, last_autoanalyze FROM pg_stat_user_tables WHERE last_autovacuum IS NULL OR last_autoanalyze IS NULL;长时间没有 autovacuum 记录的表,要么是大表因长事务被跳过,要么是配置问题导致 worker 不足。这是排查"表越来越肥"的起点。
6.3 快速生成批量清理脚本的思路
维护环境里经常要批量对几十张表执行同样操作,比如把某模式下所有表的所有者改掉:
SELECT format('ALTER TABLE %I.%I OWNER TO new_owner;', schemaname, tablename) FROM pg_tables WHERE schemaname = 'public';把查询结果导出成 SQL 脚本再执行即可。format函数里的%I是安全的标识符占位符,会自动处理表名中的特殊字符,防止注入问题。同理,批量授权、批量开启 RLS、批量检查膨胀都能用这种"查询生成 SQL"的模式。
批量给所有表添加外键约束,也可以先把候选语句查出来,人工审一遍再执行,比手写几十条 UPDATE 安全得多。
7. 生产环境高频报错的排查实录
7.1 连接数打满:remaining connection slots
报错信息一般是FATAL: sorry, too many clients already或者remaining connection slots are reserved for non-replication superuser connections。这是生产环境最常见的故障之一。
排查思路是固定的:
- 先看当前连接数分布,找出占用大头:
SELECT state, count(*) FROM pg_stat_activity GROUP BY state; - 重点找
idle in transaction会话:SELECT pid, state, query_start, xact_start FROM pg_stat_activity WHERE state = 'idle in transaction'; - 找到问题会话后,跟业务方确认再终止:
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle in transaction' AND now() - xact_start > interval '10 minutes';
这里谈一个经验点:与其等连接满了再清理,不如在应用层设置连接池最大连接数,并给 PostgreSQL 的max_connections留出 20% 的冗余量。连接池打满和数据库打满,处理难度完全是两个级别。
7.2 主键冲突背后的序列断层
插入数据时报duplicate key value violates unique constraint ...,很多新人第一反应是数据重复,但还有一种隐蔽原因:表主键用了序列,但序列的当前值小于表内已有最大 id。
这种问题多发生在手工导数据、跨环境迁移数据之后。排查语句:
SELECT nextval('your_table_id_seq'); -- 当前序列值 SELECT max(id) FROM your_table_name; -- 表内实际最大 id如果序列值小于等于表内最大 id,修复方式:
SELECT setval('your_table_id_seq', (SELECT max(id) FROM your_table_name));注意 setval 的第二个参数是最小目标值,执行后下一次 nextval 会返回 max+1。这种坑在迁移数据时特别常见,我在恢复备份后就踩过,之后养成了迁移后必查序列的习惯。
7.3 慢查询与执行计划异常的处理路径
某条 SQL 昨天还快,今天突然慢了?第一反应不是加索引,而是看执行计划变没变:
EXPLAIN ANALYZE SELECT ...;关键看 Seq Scan vs Index Scan 的占比。统计信息过期导致错误选择全表扫描的情况,十有八九是 ANALYZE 没跟上。这时先执行一遍ANALYZE your_table_name;,再重查执行计划,很多时候问题直接消失。
如果 ANALYZE 之后依然走全表扫描,再思考:是不是条件列的数据分布倾斜,或者索引本身选择性不足?用pg_stats查看字段分布:
SELECT attname, n_distinct, most_common_vals, most_common_freqs FROM pg_stats WHERE tablename = 'your_table_name' AND attname = 'the_column';一张表的数据只有几百行时,索引扫描反而不如全表扫描快,这是优化器基于代价做出的合理选择,无需强行干预。
7.4 字符集与编码相关的隐性问题
PostgreSQL 的库是在初始化时确定编码的,最常见的默认值是 UTF8。乱码问题大多发生在客户端连接编码不对,而不是数据库本身出问题。遇到乱码,先看:
SHOW client_encoding; SHOW server_encoding;如果二者不一致,执行SET client_encoding TO 'UTF8';再重新查询。另一种情况是locale和排序规则不一致导致的索引失效,常见于从旧库"克隆"数据而不是逻辑导入。这类问题排查起来特别隐性,页面端显示正常,排序顺序却是乱的。
稳妥做法:同构迁移时保持 LC_COLLATE / LC_CTYPE 一致,异构迁移完成立即抽查中文字段排序和索引走没走。
8. 这份合集的使用方式与维护计划
我个人在使用和维护这份合集时的几条真实体会,或许对你有参考价值。
体会一:SQL 合集不是越全越好,而是越"场景化"越好。这份文档里我坚持每条 SQL 都配上"什么时候用、为什么不这么写就会出问题"。有了场景锚点,记忆效率会大幅提升。你在收藏别人整理的大段文档时,也可以尝试用自己的业务场景重新注释一遍。
体会二:系统表和视图的记忆有规律可循。pg_stat_*系列全是运行时统计,比如pg_stat_activity当前活跃会话、pg_stat_user_tables表的统计信息、pg_stat_user_indexes索引使用统计。pg_*不带stat的是元数据。information_schema是标准 SQL 视图,跨数据库兼容性好。我用一个本地笔记本把这些视图的关系画出来之后,查任何系统信息都能快速定位正确视图。
体会三:这些 SQL 可以落成监控脚本。我目前是把关键几条,比如连接数分布、死锁阻塞链、表膨胀比例,做成了定时巡检任务,每天凌晨跑一遍,异常就推告警。效果比事后处理强很多。
这份合集我不打算一次性写完,而是按专题持续分批补充。下一批计划补以下方向:
- 分区表维护相关的管理 SQL,包括分区拆分的流程和查各分区数据占比。
- 逻辑复制和订阅状态监控,排查复制延迟的专用语句。
- 更细的锁等待分析,区分行锁、页锁、咨询锁,以及锁超时配置建议。
- SQL 调优时使用
EXPLAIN各类选项的实际解读案例。
你可以把这篇文章当作一个长期维护的手册来用,每次遇到新问题、沉淀出可复用的语句,我都会同步更新进来。如果有你觉得特别实用但这里没有的语句,也欢迎在评论区留言,下一批更新我会优先补上。