半夜两点被叫起来,应用报"数据库连接失败",所有报表白屏。这是每个后端和运维都经历过的场景。数据库问题排查这件事看起来很吓人,但大部分情况下,真正让你卡住的不是问题本身有多难,而是你根本不知道从哪下手,只能靠重启、重启、再重启碰运气。我整理这份"01-08-22 数据库问题排查完全指南",就是想把这些年处理Oracle、MySQL、PostgreSQL、SQLite以及国产数据库问题时积累的排查链路一次性讲清楚——从连接故障、慢查询、死锁,到数据迁移、安装配置,每个环节都给出可落地的操作步骤和判断依据。
这篇内容主要面向两类人:一是刚接手数据库运维的后端开发,数据库出问题的时候往往是最慌的;二是需要自己扛项目的全栈或独立开发者,没有专职DBA,出了问题只能自己上。不管你是哪种,看完以后至少能建立一套自己的排查思路,而不是遇到问题就百度一条命令试一条。
1. 排查数据库问题前,先把"现象"翻译成"故障层"
1.1 为什么要先分层
我见过太多人拿到一个数据库报错就开始瞎试:先重启数据库服务,不行就重启应用,再不行就重装客户端。这种碰运气式的排查最大的问题是,你根本没有定位问题的层面,只是在赌。
数据库访问链路其实是固定的:客户端/应用 -> 网络连接 -> 数据库实例 -> SQL解析执行 -> 存储引擎 -> 物理文件。任何一个环节出问题,应用层的表现往往都是"连接失败"或"查询超时"——这是因为上层应用把底层的多种异常统一包装成了几个常见错误码。所以排查的第一步,是把这个链路在脑子里过一遍,确认问题到底发生在哪一层。
我一般把故障划分为四个层面:
| 故障层 | 典型现象 | 常见原因 |
|---|---|---|
| 网络层 | 连接超时、TNS无法解析 | 防火墙、端口不通、DNS问题 |
| 服务层 | 连接被拒绝、认证失败 | 实例没起、监听挂了、密码错 |
| SQL层 | 查询慢、锁等待 | 索引缺失、执行计划差、并发冲突 |
| 数据/存储层 | 磁盘满、表损坏、数据文件丢失 | 空间耗尽、异常断电、误删 |
1.2 每一层最直接的验证动作
确定了怀疑的层面,就要用最快的动作去验证,而不是打开一堆工具慢慢地看。
网络层:先 ping 通不通,再测端口。很多数据库端口在云厂商的安全组、本机防火墙、甚至公司办公网的出口策略上被挡了。Windows 下用 telnet,Linux 下用 nc,一条命令就能确认端口是否可达。有些时候 ping 通了但端口不通,说明主机活着但监听进程没起来,或者安全组挡了端口。
服务层:查看数据库进程是否存活、监听状态是否正常、日志里有没有报错。Oracle 看 lsnrctl status,MySQL 看 systemctl status mysqld 或者直接看 error log,PostgreSQL 看 pg_ctl status。这里要特别注意,进程活着不代表数据库可用,很多数据库实例会陷入"假死"状态——进程在,但无法接受新连接。
SQL层:如果连接本身没问题,但某些业务操作很慢或卡住,就要看慢查询日志、执行计划、会话等待事件。这个层面的排查是性能问题的主战场,后面单独展开。
数据/存储层:检查磁盘空间、表空间使用率、文件系统权限。我遇到过不止一次数据库自己没问题,是挂载的数据盘满了,导致数据库无法写入。用 df -h 和 df -i 同时看,因为 inode 满了同样会出问题,而且更容易被忽略。
1.3 一个被忽略的原则:制造最小复现
排查问题的时候,不要直接在应用里试,那样变量太多。从数据库客户端(mysql、psql、sqlplus、DBeaver)去连一下,如果能连上并执行简单查询,说明数据库本身是好的,问题在应用侧;如果客户端也连不上,再按上面的层次逐层排查。这种最小复现的方法能帮你快速把排查范围缩小一半。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 连接故障排查:从"应用报错"倒推到"连接池打满"
2.1 常见连接报错的真实含义
连接类故障是数据库问题里出现频率最高的一类。Java 项目最常见的报错是 Communications link failure,Oracle 项目常见的是 ORA-12541: TNS:no listener,MySQL 常见的还有 Too many connections,PostgreSQL 则是 sorry, too many clients already。这些报错看着不同,但本质上对应的故障层完全不一样。
我整理了一个快速对照表:
| 报错信息 | 问题层 | 优先检查 |
|---|---|---|
| Communications link failure | 网络/服务 | 端口、实例状态 |
| ORA-12541 TNS:no listener | 服务层 | Oracle监听是否启动 |
| ORA-01034: ORACLE not available | 服务层 | 实例是否mount/open |
| Access denied for user | 认证 | 账号密码、权限 |
| Too many connections | 连接池/资源 | max_connections、活跃连接数 |
| Connection refused | 服务层 | 进程是否存活、防火墙 |
| 访问数据库时发生错误 | 应用层封装 | 看应用完整堆栈 |
2.2 一条标准的连接排查路径
我自己的排查顺序是固定的,按这个顺序走基本不会漏:
第一步,确认数据库实例是否活着。登录数据库所在主机,执行 ps -ef | grep 确认进程在不在。不在的话直接看启动日志,找出为什么起不来。这一步能排除掉服务层一半的问题。
第二步,确认网络和端口。从应用服务器上 telnet 数据库IP端口,不通就用 traceroute 或 nc -vz 看卡在哪一跳。很多情况是数据库升了级、换了端口,但应用的配置没跟着改。
第三步,检查账号权限。这一步最常见的问题是密码过期、账号被锁、IP白名单没放行。MySQL 的 account_locked 和 password_expired 字段经常被忽略,Oracle 的 PROFILE 里密码有效期默认 180 天,到期后应用就连不上了。
第四步,看连接数是否打满。连接数打满的情况最隐蔽,因为数据库本身是健康的,但你新连接就是进不去。MySQL 执行 SHOW STATUS LIKE 'Threads_connected'; 和 SHOW VARIABLES LIKE 'max_connections'; 对比一下,Threads_connected 长期贴近 max_connections 的话,说明连接没有及时释放或连接池配得太大。
2.3 连接池打满的深层原因
连接池打满,数据库只是受害者,真正的根因通常在应用侧。排查的时候先 SHOW FULL PROCESSLIST 看看大量连接是什么状态,如果一堆 Sleep 状态的连接挂在那边,说明应用拿到了连接但没释放。
我处理过最典型的一个案例:某个 Spring Boot 服务用了 HikariCP,maximumPoolSize 配了 50,但业务代码里有个事务方法在外层 catch 了异常导致事务没有正确提交或回滚,连接一直被占用,最终连接池耗尽,所有请求排队等连接。这里有两个排查技巧:一是启动参数里加 leakDetectionThreshold=30000,连接泄漏超过30秒会自动打印告警;二是配合 SHOW FULL PROCESSLIST 看某个连接的 Time 字段,如果同一个连接长时间不释放,基本就是代码里忘关了连接。
Tomcat 连接池的话,removeAbandoned=true 可以在连接被借出超过 removeAbandonedTimeout 后强制回收,但这是个兜底手段,不能依赖它,因为它可能把还在执行的事务连接给回收掉,造成数据不一致。
2.4 两个特殊连接问题的现场还原
一个是 ArcGIS 连接 Excel 表时报"连接到数据库失败。常规功能故障,外部表不是预期的格式"。这个报错很多人第一次见会懵,其实 ArcGIS 扫描 Excel 文件时对格式要求很严格:文件必须是 .xls 或 .xlsx 的标准格式,如果文件是从 CSV 直接改扩展名得到的,或者文件被 WPS 以兼容模式保存过但实际内容不规范,就会报这个错。解决方法是把 Excel 另存为标准格式,或者先导入到 Access/PostGIS 再连接。
另一个是 Multisim 访问数据库报错。这类专业软件的数据库连接错误,绝大多数是因为软件自带的 ODBC 数据源指向了 32 位/64 位不匹配的驱动。Multisim 是 32 位程序,在 64 位系统上必须用 C:\Windows\SysWOW64\odbcad32.exe 配置 ODBC,如果用默认的 64 位管理器配置,软件就找不到数据源。
3. 慢查询和锁冲突:性能问题三板斧
3.1 慢查询定位不需要猜
性能问题排查的核心是拿到"证据",而不是靠感觉。第一板斧是打开慢查询日志。MySQL 里设置 slow_query_log=ON 和 long_query_time=1,超过 1 秒的 SQL 全被记录下来。Oracle 用 AWR 报告,PostgreSQL 用 log_min_duration_statement=1000。有了慢 SQL 列表,你才算有了排查的起点。
拿到慢 SQL 之后,第二板斧是看执行计划。MySQL 在 SQL 前面加 EXPLAIN,Oracle 用 EXPLAIN PLAN FOR 或者直接 DBMS_XPLAN。看执行计划重点看四样东西:访问类型(type 字段是不是 ALL 全表扫描)、扫描行数(rows 字段)、是否用了临时表和文件排序(Using temporary、Using filesort)、索引使用情况。
这里有个容易被忽略的点:统计信息过期。MySQL 的优化器依赖 information_schema 里的统计信息决定是否走索引,如果表数据量变化很大但统计信息没更新,优化器可能选错执行计划,明明有索引却全表扫。遇到这种情况,执行 ANALYZE TABLE 刷新一下就解决了。
3.2 索引失效的六种高频场景
索引失效是慢查询的头号原因,也是面试里问烂了但在实际生产里依然反复出现的问题。我总结最常见的六种:
- 对索引列做了函数运算,比如
WHERE DATE(create_time) = '2024-01-01',改成create_time >= '2024-01-01 00:00:00' AND create_time < '2024-01-02 00:00:00'才能用到索引。 - 隐式类型转换,比如手机号字段是 varchar,查询时传了数字
WHERE phone = 13800138000,MySQL 会把字段转成数字再比较,索引直接失效。改成字符串'13800138000'就好。 - 前导模糊查询,
LIKE '%关键词'无法走索引,这是 B+ 树结构决定的。 - OR 条件连接了非索引列,
WHERE a = 1 OR b = 2,如果 b 没有索引,整个查询可能退化。 - 联合索引没有遵循最左前缀原则,建了
(a, b, c)却只查b或c。 - 在索引列上做加减乘除运算,
WHERE price * 1.1 > 100,改成WHERE price > 100 / 1.1。
这些场景的共同点是:优化器认为走索引比全表扫描还慢,或者根本无法使用索引结构。排查的时候用 EXPLAIN 看 possible_keys 为 NULL 或者 key 为 NULL,基本就是这几种原因。
3.3 锁等待和死锁:两码事,别混为一谈
锁等待和死锁是数据库并发问题里最容易混淆的两个概念,但处理思路完全不同。
锁等待是事务 A 持有某行/表的锁,事务 B 想操作同一行/表,B 只能等 A 提交或回滚。MySQL 里默认 innodb_lock_wait_timeout=50 秒,超过就报 Lock wait timeout exceeded。排查方法是查 information_schema.innodb_trx 看当前有哪些事务在跑,再用 sys.innodb_lock_waits 直接定位谁在等谁的锁。
死锁是事务 A 持有资源1等待资源2,事务 B 持有资源2等待资源1,两边都在等对方释放,只能靠数据库的死锁检测机制强制回滚一个事务。MySQL 查看死锁信息的方法是执行 SHOW ENGINE INNODB STATUS,在输出里找 LATEST DETECTED DEADLOCK,里面会明确告诉你两个事务各持有什么锁、在等什么锁。
实际排查死锁的经验是:死锁日志里显示的两条 SQL 往往不是根因,真正的根因是业务代码里多个事务获取锁的顺序不一致。比如下单流程先更新订单表再更新库存表,另一个流程先更新库存表再更新订单表,并发大了必然死锁。解决的思路是统一加锁顺序,或者把大事务拆小,缩短持有锁的时间。
3.4 一个容易翻车的点:开启审计引起索引争用
生产环境为了满足合规要求开启数据库审计,结果业务响应变慢,这类问题我见过不止一次。MySQL 开启 general_log 且输出到表,所有 SQL 都会写入 mysql.general_log 表,高峰期这个表有大量并发 insert,表上的索引页成为争用热点,严重拖垮整个实例的性能。
更隐蔽的是 Oracle 开启审计后,AUD$ 审计表不断膨胀,且审计写入和业务写入争用 undo 段和 redo 日志。我处理过一个案例:开启审计两周后,核心业务表的索引扫描从 10 毫秒涨到 500 毫秒,查了半天才定位到是审计表膨胀导致系统表空间碎片化加剧,连带影响到了所有对象的访问。
所以审计功能要开,但必须做好配套:一是审计日志尽量输出到文件而不是表,二是建立审计日志的定期清理任务,三是对审计表所在表空间做单独的存储规划,避免和业务数据抢 IO。
3.5 不同数据库的锁监控入口
锁问题虽然原理相通,但不同数据库的排查命令完全不同。我把常用的入口列出来方便大家直接抄:
| 数据库 | 查看锁等待 | 查看死锁 |
|---|---|---|
| MySQL | sys.innodb_lock_waits |
SHOW ENGINE INNODB STATUS |
| Oracle | v$session + v$lock |
alert log 或 trace 文件 |
| PostgreSQL | pg_locks + pg_stat_activity |
log_lock_waits 开启后看日志 |
| 达梦 | V$LOCK + V$TRX |
实例日志 dm_xxx.log |
4. 数据变更与迁移现场:改结构、导数据、搬库
4.1 加唯一约束前,先处理存量重复数据
"mysql设置唯一已经有重复数据库"这类问题几乎每周都有人问。需求是在某个字段上加唯一约束,但表里已经有重复数据了,ALTER TABLE 直接报错。这不是数据库的问题,是数据质量问题。
正确顺序是:先查重复,再清重,最后加约束。查找重复用一条 SQL 搞定:
sql复制SELECT phone, COUNT(*)
FROM user
GROUP BY phone
HAVING COUNT(*) > 1;
清重的时候要保留一条,推荐用保留最小 id 的方式:
sql复制DELETE FROM user
WHERE id NOT IN (
SELECT MIN(id) FROM user GROUP BY phone
);
注意 MySQL 不允许在子查询里直接引用要删除的表,需要再包一层临时表,否则会报 You can't specify target table for update in FROM clause。这是新手最容易踩的坑。
4.2 大表结构变更:别在生产环境直接 ALTER
给几百万行的表加字段、加索引,直接执行 ALTER TABLE 会导致锁表,业务停摆。虽然 MySQL 5.6 以后支持 Online DDL,但并不是所有操作都能全程在线。比如修改列类型、修改字符集这类操作,仍然需要拷贝整张表的数据,期间会有锁。
生产环境的做法是用工具。MySQL 生态里最常用的是 pt-online-schema-change 和 gh-ost,原理都是先建一张新表,通过触发器或 binlog 同步增量数据,最后切换表名。操作之前一定要确认磁盘空间够放一张全表数据,并且要在业务低峰期操作。
改完结构之后别忘了处理缓存。很多应用有 SQL 缓存或 ORM 层的元数据缓存,表结构变了,旧缓存可能拿到错误结果。稳妥的做法是改完结构后灰度重启应用。
4.3 Excel 导入数据库的经典翻车点
Excel 导入数据库看起来是个基础功能,但实际导入过程中的报错五花八门。最经典的是"外部表不是预期的格式",其次是时间字段变成数字串、长数字变成科学计数法、中文乱码。
时间字段变成数字串是因为 Excel 里日期本质是序列号,需要先把单元格格式设为文本再处理。长数字变科学计数法是 Excel 的显示问题,导入前要把单元格格式设为"文本",或者用 LEFT 函数补零。中文乱码绝大多数是编码问题,CSV 文件用 GBK 还是 UTF-8,必须和数据库连接串的字符集保持一致。
实用的做法是:导出 CSV 时选 UTF-8 编码,如果发现乱码再换成 GBK 试;导入前先用工具预览前 100 条数据做数据校验,避免导入到一半发现类型对不上回滚不了。
4.4 导出脚本和数据迁移:IDEA、DBeaver 的细节
IDEA 自带数据库工具,右键表选择 Dump Data To File 可以导出 SQL 脚本。导出时注意几个选项:是否包含 DROP TABLE、是否生成 CREATE TABLE、INSERT 语句是单条 INSERT 还是批量 INSERT。批量 INSERT 性能好很多,适合大数据量导入。
DBeaver 做数据库迁移也很方便,支持 MySQL、PostgreSQL、SQL Server、达梦等多种数据源。但迁移前要注意:源库和目标库的字符集必须一致,否则中文数据全变问号;迁移大表时先关掉外键约束,导入完成后再开启并校验。
Oracle 冷迁移也值得提一句。Oracle 11g 的冷迁移,核心是保证 datafile、controlfile、redo log 文件完整,用同样的目录结构和版本还原。迁移后如果启动报 ORA-00205(controlfile 不存在或无法读取),多半是控制文件路径变了但初始化参数没改。操作前先备份参数文件,避免改完回不去。
5. 安装配置与国产数据库的适配坑
5.1 Oracle 安装配置:环境检测和字符集是两大门槛
Oracle 安装最让人崩溃的不是过程,而是前置环境检测。Linux 上需要提前配好内核参数,比如 kernel.sem 信号量、kernel.shmmax 共享内存,还要创建 oracle 用户和 oinstall、dba 用户组。很多人的安装卡在 prereq 检查过不去,多半是这些系统参数没配。
字符集的选择更要提前想清楚。生产库建议用 AL32UTF8,如果选了 ZHS16GBK,以后想改 UTF-8 就非常麻烦,可能需要重建库。安装后配置监听时,listener.ora 和 tnsnames.ora 里最容易出错的是 HOST 写成了 localhost,导致远程客户端连不上。
5.2 达梦数据库:安装、兼容模式和 CDC
达梦数据库(DM8)在国产化替代项目里用得非常多,它的安装步骤和 Oracle 很像:创建用户、解压安装包、执行安装程序、初始化实例。初始化实例时有一个关键选项是"兼容模式",选兼容 Oracle 语法,可以让很多从 Oracle 迁移过来的应用少改代码。
达梦开启 CDC 是数据同步场景的常见需求,流程比 MySQL 的 binlog 复杂一些。基本步骤是:开启数据库归档模式,然后通过达梦提供的工具配置数据源和映射关系。我在实际操作中发现,最容易被漏掉的是归档参数没配好,CDC 同步会提示无法读取日志。另外达梦的 CDC 需要单独的同步组件,不能在数据库里直接一条命令搞定,这点和 Oracle GoldenGate 的思路类似,需要先规划好部署架构。
5.3 人大金仓 Docker 部署和 SSL 配置
人大金仓(KingbaseES)用 Docker 部署很方便,但默认端口是 54321,不是 PostgreSQL 的 5432。如果你用 PG 的客户端去连,端口不写对就会一直报连接超时。启动时要把数据目录挂载出来做持久化,否则容器删了数据全没。
"金仓数据库未启用ssl"这个问题,本质是金仓默认配置里 ssl = off。如果应用的连接串里强行要求 SSL,就会报错。处理方法有两个:一是把连接串的 sslmode 改为 disable,二是生成证书后在配置文件里启用 SSL。内网环境我一般直接 disable,简单省事,公网环境才建议认真配置证书。
5.4 华为 GaussDB 适配 Nacos
Nacos 默认存储用的是 MySQL,如果要把配置中心迁移到华为 GaussDB,需要替换数据源驱动和数据库类型配置。GaussDB 提供了兼容 PostgreSQL 的驱动,也有自己的 gaussdb 驱动。实际操作中要注意:Nacos 的 SQL 脚本需要先按 GaussDB 的语法适配,因为 GaussDB 和 MySQL 在分页、自增主键上的语法不一致。启动报错时优先看 nacos.log 里的数据库连接信息,一般错误就出在驱动类名或 URL 上。
GaussDB 的连接 URL 是 jdbc:gaussdb://host:port/dbname,和 PostgreSQL 的 jdbc:postgresql:// 不一样,这个细节能省你半小时的排查时间。
5.5 PostgreSQL 实例启动方式和 SQLite 的单文件特性
PostgreSQL 实例启动方式有三种:pg_ctl start、systemctl start postgresql、以及通过 service 命令。排查启动失败时,先在 pg_log 目录下看启动日志,重点看数据目录权限是不是 postgres 用户所有。用 root 启动 PG 是明确被禁止的,这个限制导致很多人第一次部署时反复失败。
SQLite 则完全是另一个世界,它是单文件数据库,整个库就是一个 .db 文件。它适合嵌入式场景和轻量应用,但不适合高并发。如果项目从 SQLite 迁移到 MySQL/PostgreSQL,要注意 SQL 方言的差异,比如 SQLite 没有 IF NOT EXISTS 的完整支持、自动递增的写法也不一样。
5.6 从问题看选型:时序数据库和向量数据库
熟悉数据库问题排查的人,对选型也会更敏感。时序数据库适合物联网、监控类的写入密集型场景,数据结构设计上和关系型数据库完全不同,通常以时间戳作为分区键,按 tag 建索引。如果拿 MySQL 做时序数据存储,几千万条监控数据就能把磁盘和查询拖垮。
向量数据库则是 AI 应用带火的新方向,它在数据模型、索引结构(HNSW、IVF)和查询方式上跟传统数据库差异更大。问题排查的思路也要跟着变,比如"查询慢"在向量数据库里可能不是索引失效,而是向量索引的 ef 参数配得太小。传统数据库的排查经验在这些新类型里只能参考,不能照搬。
6. 把一次排查变成一套机制:日志、监控与复盘清单
6.1 先固化一套排查清单
每次故障排查完,我都会把当天的排查过程整理成清单,下次遇到类似问题直接照着走,效率翻倍。数据库连接问题的清单大概是这样的:
- 数据库进程是否存活。
- 端口是否可达、防火墙是否放行。
- 用客户端直连数据库,确认是否认证失败。
- 检查连接数是否打满(
SHOW STATUS或v$session)。 - 查看应用完整的异常堆栈,定位是连接获取、事务执行还是资源释放环节。
- 检查连接池配置和代码里的事务管理逻辑。
这个清单看起来简单,但真正按顺序走一遍能过滤掉 80% 的无效操作。排查性能问题的清单则包含:慢查询日志、执行计划、索引使用情况、锁等待和死锁信息、数据库整体负载。
6.2 监控指标要提前设计,而不是出事才看
数据库监控最忌讳的是指标堆了一大堆,出事的时候根本不知道看哪个。我建议优先盯这几类指标:连接数、活跃会话数、慢查询数量、锁等待次数、死锁次数、磁盘空间使用率、数据库错误日志的高危报错。这些指标如果能做到告警,大部分问题能在用户感知之前被处理掉。
连接数和慢查询数是最核心的两个。连接数异常飙高,通常意味着应用侧出了问题,比如连接池泄漏、突发流量;慢查询数突然增加,往往对应着执行计划变化、数据量增长或者索引失效。把这两个指标的告警阈值调好,能覆盖大部分数据库故障场景。
6.3 复盘时多问几个"为什么"
故障处理完了,真正的价值在复盘环节。我每次复盘都会问自己四个问题:问题的直接原因是什么?我为什么没有更早发现?有没有办法在代码层面避免?监控层面能不能提前告警?
比如连接池打满这件事,直接原因可能是代码没释放连接,但深入追问就会发现问题不止一层:为什么代码 review 没发现?为什么连接池的泄漏检测没开?为什么没有连接数接近阈值的告警?顺着这几个问题往下推,你会把单个故障变成一个系统性的改进项,而不是每次都在同一个坑里爬出来又掉进去。
数据库问题排查永远是实践出真知。这篇指南里的方法,是我在真实故障现场反复验证过的,你拿去用的时候也可以根据自己的项目情况调整。最后分享一个我自己的习惯:每次排查完,把关键的 SQL 和命令、当时的现象、最终的根因,随手记到一个笔记里。半年以后回头看,你会发现很多当时觉得天大的问题,其实都是同一种套路。积累到一定程度,数据库问题对你来说就不再是"事故",而只是一道普通的判断题。
