S41

数据库原理

点击正文段落选定收藏或划线位置;未选定时使用屏幕内第一段。

课内目录

1 数据库原理要回答什么

运维脚本每隔 10 秒采集进程状态,把主机、进程号、采样时间与常驻内存写进数据库。另有终端查询最近 5 分钟的异常进程,还有后台任务清理过期记录。它们访问的是同批数据,却希望得到不同保证:写入不能留下半条状态,查询不能把尚未确认的采样当成事实,清理不能让值班查询长期等待。数据库原理就是解释这些要求怎样相互影响,以及需要拿什么证据证明它们成立。

数据库不只是会搜索的文件。普通文件提供字节和路径,进程可以通过文件描述符读写;数据库在这些能力上组织记录、约束、事务与查询。记录描述对象,约束限制合法状态,事务规定哪些修改共同完成,查询表达想要的数据。数据库服务仍然是操作系统中的进程,需要 CPU、内存、文件系统和网络连接。数据库中的逻辑承诺与主机上的资源状况有关,但不能互相替代:磁盘空闲不代表事务已提交,端口可达也不代表查询能及时完成。

本课用 2 类记录贯穿讨论。proc_sample 保存进程采样,字段包括 host_id、pid、sampled_at、rss_kib、state;maintenance_task 保存维护任务,字段包括 id、status、owner。进程号会复用,因此实际识别进程时还应包含启动时间或其他实例标识。这个细节说明,数据库能够保证你写下的规则,却不能替你发现遗漏的业务身份。把错误的身份字段设成唯一,也只会稳定地执行错误规则。

讨论数据库时应分开 3 个问题。正确性关注合法状态与可见结果;性能关注完成查询耗费的时间和资源;可恢复性关注进程崩溃或主机故障后能恢复什么。事务提交通常代表数据库按当前配置接受了持久化责任,但具体故障保障仍取决于配置和存储条件。客户端遇到连接断开时,甚至可能不知道提交是否已经完成。此时简单重发写入可能产生重复记录,需要稳定的操作标识与结果查询接口。

数据库承诺与运行环境
数据库承诺与运行环境

不要把每种数据库都想成相同产品。关系数据库常用 SQL 表达访问意图,但 SQL 文本、默认隔离级别、执行计划格式和管理视图可能不同。本课先掌握可迁移的问题:事务边界在哪里,读取允许看到什么,访问路径是什么,等待发生在哪个资源上。随后再用具体实现的文档和实验确认答案。观察命令负责提供证据,不能代替数据库语义。

2 事务、隔离级别与并发异常

假设维护任务从 ready 变成 running 时,还要登记领取者。若脚本先写状态、再写领取者,第 2 条语句失败,其他进程就可能看到没有负责人的运行任务。事务把这组修改划进共同边界:成功时提交,失败时回滚。原子性意味着边界内的修改共同生效或共同撤销;一致性要求提交结果满足约束和业务规则;隔离性控制并发事务的相互影响;持久性描述提交后面对故障的保留承诺。这些词需要对应具体状态,不能只背缩写。

例如可以把领取操作写成条件更新:UPDATE maintenance_task SET status='running', owner='worker-a' WHERE id=42 AND status='ready';。应用检查受影响行数,只有更新到 1 行才认为领取成功。把条件留在数据库语句里,能够避免“先查到 ready,再无条件更新”造成的竞争。涉及更多记录或额外约束时,还需要事务与合适的锁定策略。事务包住代码并不自动使代码正确,尤其不能保护事务外的文件删除或 shell 命令。

并发异常可以用 2 个会话解释。会话 A 修改记录但尚未提交,会话 B 就读到这个值,称为脏读。如果 A 随后回滚,B 根据从未成为已提交事实的数据作出了决定。会话 B 在同个事务内重复读取同条记录,结果因 A 的提交发生变化,称为不可重复读。若重复查询某个条件范围时,符合条件的记录集合发生变化,就涉及幻读。还可能出现丢失更新:多个进程根据旧值计算,各自写回,后写者覆盖先写者的成果。

隔离级别描述允许的并发可见性。读未提交通常允许脏读;读已提交要求读取已提交数据,但不同语句可能看到不同提交状态;可重复读提供更稳定的读取视图,但不能据名称推断所有数据库都用相同方式处理范围读取或写入冲突;可串行化要求结果能够对应某种串行执行顺序,可能通过等待或拒绝事务来实现。某些数据库会把请求的级别映射到更强语义,例如 PostgreSQL 的读未提交按读已提交处理,因此实验前必须确认有效级别。

读到已提交数据也不保证组合判断安全。2 个工作进程可能各自看到剩余配额充足,随后分别提交不同记录,使合计超过限制。应明确规则由唯一约束、条件更新、锁定还是更强隔离来保障,而不是把“没有脏读”当成所有并发问题的答案。初学阶段只需先识别读和写之间有没有被其他事务插入操作的空隙,不必提前研究内部并发控制算法。

隔离越强,应用通常越需要面对等待、冲突和重试,不过成本与访问模式有关,不能写成固定倍数。重试必须重做整个相关事务,重新读取依赖数据,并限制次数和总时长。事务中若已执行外部命令,重新执行可能重复产生副作用。将事务保持简短,把可重复的数据库工作与外部动作的完成确认设计清楚,比盲目增加重试次数更有效。

3 索引、缓冲区与执行计划边界

查询某台主机最近的异常进程,看似只是筛选几行,数据库却可能读取大量页面。表把记录组织成可存储的数据,索引提供额外的定位路径,缓冲区把部分数据页保留在内存。执行计划则说明优化器准备按什么路径取得结果:扫描哪些对象,怎样筛选、连接或排序。SQL 说明结果要求,计划说明本次选择的实现路径,它不是永久不变的承诺。

把索引想成按特定顺序编排的目录比较合适。若常用条件是 host_id=7,并限定 sampled_at 的时间范围,候选索引可以是 (host_id, sampled_at)。先定位主机,再沿时间范围寻找,可能减少访问量。索引列顺序影响可用方式;函数、隐式类型转换、排序要求以及返回字段也会影响计划。存在索引不等于查询必然使用,使用索引也不等于总代价较低。当查询需要大部分记录时,连续扫描可能更合理。

索引也有成本。每次插入采样,都可能维护多个索引,增加存储与写入工作。为每列建立索引既不能覆盖所有组合,也可能让采集进程的提交变慢。应从稳定且重要的访问模式出发,例如“按主机查询最近 5 分钟”“按时间清理过期数据”,分别评估过滤、排序和修改成本。唯一索引还能支撑某些唯一性约束,但普通性能索引不会自动证明维护任务只能被领取 1 次。

缓冲区命中表示所需页面能从相应缓存取得,减少部分存储读取。操作系统也可能缓存文件数据,因此数据库缓存未命中并不等同于物理磁盘必定读取。反过来,命中内存仍需要筛选、比较、排序和传输结果。扫描大量缓存页可能消耗 CPU 并挤占其他工作负载。判断瓶颈时要同时看访问量、等待和资源,不要把“在内存里”直接翻译成“足够快”。

查询从意图到结果的路径
查询从意图到结果的路径

计划中的行数往往是估计值,依赖统计信息与参数。实际扫描行数远多于估计,可能使原本合理的路径变得昂贵。计划显示的成本通常是实现内部的比较尺度,不能直接当毫秒。估计计划通常不执行查询,带实际分析的计划通常会执行,需要区分接口。更不能把数据库内执行耗时当作用户等待的全程:连接建立、网络传输、客户端取数和格式化仍可能占据时间。

滑块只改变示例中时间范围的选择性,让你观察候选行数和访问工作量。它不模拟真实优化器,也不预测生产延迟。正确用法是先形成假设:范围扩大后,索引访问优势可能降低;再用实际计划和运行指标验证。保持相同数据、参数和缓存条件,才能把变化归因到被调整的因素。

4 动手:用执行计划解释一次慢查询

准备可丢弃的练习数据库,选取已有或少量自建的进程采样记录。下面使用 PostgreSQL 风格的时间与计划语法;其他实现应替换对应写法。练习表可用 CREATE TABLE proc_sample (host_id integer, pid integer, sampled_at timestamp, rss_kib bigint, state varchar(16)); 建立。真实系统还应补齐非空、身份与时间语义约束,这里先保留与访问路径有关的字段。若表只有几10行,扫描全表很合理,不能为看到索引而人为判它错误。

先固定问题和时间范围,避免每轮“最近 5 分钟”移动边界造成结果不同。示例语句为 SELECT pid, sampled_at, rss_kib FROM proc_sample WHERE host_id=7 AND sampled_at >= TIMESTAMP '2026-10-01 10:00:00' AND sampled_at < TIMESTAMP '2026-10-01 10:05:00' ORDER BY sampled_at;。这里用左闭右开的区间,便于拼接相邻时间窗。应记录样本规模、目标主机记录占比、返回行数和已有索引,否则仅凭耗时无法判断优化是否来自访问路径变化。

第 1 步给语句加 EXPLAIN,看扫描类型、过滤条件、估计行数和排序节点。第 2 步在允许执行的练习环境运行 EXPLAIN (ANALYZE, BUFFERS),比较实际行数、循环次数和缓冲访问。父节点耗时可能包含子节点工作,不能直接把整棵树每行时间相加。实际分析有观测开销,结果也受缓存与并发影响。对于修改语句,实际分析会执行修改;本练习只分析读取语句。

第 3 步提出明确假设:“此查询扫描了许多其他主机的数据,按主机与时间建立组合索引可能减少访问。”执行 CREATE INDEX proc_sample_host_time_idx ON proc_sample (host_id, sampled_at);,再按练习数据库的要求更新统计信息,例如 PostgreSQL 的 ANALYZE proc_sample;。在相同参数下重新取计划,检查候选记录、过滤丢弃量和缓冲访问是否下降。创建索引本身也消耗资源,生产环境需要另行评估构建方式和窗口。

第 4 步把数据库内耗时与客户端耗时并列。shell 中可用 /usr/bin/time -p psql -X -d training -c 'SELECT 1' 粗看启动客户端、连接和执行的总时间,但它包含进程启动,不能当作数据库纯执行基准。用 ss -tn 查看连接状态,用 ps -o pid,stat,etime,pcpu,pmem,cmd -p <PID> 查看已确认的服务进程。命令中的占位符需要替换,连接口令不要写进可被进程列表或历史记录暴露的位置。

若计划已快速返回少量记录,客户端仍慢,就继续检查连接、结果读取与网络路径。若计划读取量大而 CPU 忙,优先检验过滤与索引假设;若查询主要在等待锁,先找阻塞事务。最终实验记录应写明原计划的问题、改动、相同条件下的证据与代价。不要只留下“加索引后快了”这种无法复查的结论,也不要为了固定计划而关闭优化器能力。

5 锁、等待与死锁证据链

锁用于协调互不兼容的并发操作。维护进程更新任务 42 时,另个进程也想修改该任务,可能需要等待前者结束。等待本身不是故障,它说明系统正在执行某种协调规则;等待过长才需要结合事务时长、访问范围和服务要求判断。不同实现中,普通读取是否等待写入、锁覆盖什么范围都可能不同,应查询当前数据库的活动会话和锁视图。

先建立请求到事务的关联。应用日志保留请求标识、开始时间、数据库连接或会话标识、事务开始和结束,以及语句模板。不要为排障把敏感参数全部写入日志。PostgreSQL 可从 pg_stat_activity 的 pid、state、xact_start、wait_event_type、wait_event 入手,再用 pg_blocking_pids(pid) 查阻塞者;权限和可见字段依部署而定。pg_locks 提供更详细的锁信息,但不能把每条锁记录都当作阻塞证据,必须确认等待关系。

从用户等待关联到数据库与主机证据
从用户等待关联到数据库与主机证据

例如会话 B 的更新迟迟不返回,活动视图显示它在等待锁;阻塞者 A 的事务已持续 120 秒,状态却是事务中空闲。此时 CPU 使用率可能很低,新增 CPU 无法结束 A 的事务。继续查 A 对应脚本是否在开启事务后等待人工输入,或把远程命令执行放在提交前。解决方向是缩短事务并修复边界,终止阻塞会话只是应急动作,可能引发回滚和客户端错误,需要先明确影响。

死锁是等待形成环。会话 A 先锁任务 42,再请求任务 43;会话 B 先锁任务 43,再请求任务 42。双方都等待对方释放,数据库通常会检测并中止某个事务。死锁与单向长等待不同,不能从“超时了”直接断定死锁。应保留错误码、数据库死锁报告、涉及语句和访问顺序,才能还原环路。把相关资源按稳定顺序访问,常能减少这种风险,但仍要保留正确的失败处理。

锁等待超时、死锁中止和连接中断也有不同恢复方式。某些错误只影响当前语句,某些会使事务必须整体回滚,具体行为需查实现。应用不能在失败后继续假定事务仍然有效。对于可重试冲突,先回滚,再从事务起点重新执行,限制重试次数,并加入适度间隔。连接在提交附近断开则还涉及结果未知,应先按操作标识确认状态,避免把网络重连当作无条件重放授权。

6 常见坑:把缓存命中率当正确性证明

监控面板显示缓存命中率 99%,维护脚本仍然误判任务完成,这并不矛盾。命中率衡量某种页面访问的获取位置,不描述事务允许看见哪些版本,也不验证约束是否完整。脏读、丢失更新和读取过期副本,都不能靠提高缓存命中率修复。先问错误属于数据语义还是资源效率,再选择指标,能够避免把几张绿色图表拼成错误的安全感。

高命中率也不保证请求延迟低。查询可能在内存中扫描大量页面,等待其他事务,或向客户端发送大量结果。平均耗时较低还可能掩盖少量持续很久的请求。因此观察时至少把返回量、扫描量、等待类型和延迟分布放在同个时间窗内。命中率的分母、统计范围和重置时间同样重要;数据库级累计值可能掩盖某张表或某类语句的问题。

另个常见误区是把客户端超时理解成数据库已经撤销。客户端停止等待后,服务端语句可能继续运行,事务也可能保持打开,具体取决于驱动、取消机制和连接处理。shell 使用 timeout 结束客户端进程,只能直接说明本地进程被终止;还需查询数据库会话确认服务端状态。重试前必须处理旧事务与结果未知问题,否则同个维护动作可能执行多次。

文件系统指标也有边界。df -h 能提示空间不足,df -i 能提示 inode 耗尽,数据库日志中的写入失败可与其对应;但文件系统仍有空间不意味着数据库必定能够成功提交。权限、配额、只读挂载或底层错误都可能影响写入。不要手动删除数据库目录下看似过期的文件来腾空间,数据库文件之间存在自身管理关系,应使用支持的维护接口并根据恢复要求操作。

最后,参数名也不是承诺。连接池可能复用前次会话留下的隔离设置,应用配置中的期望值未必等于当前事务实际值。测试要从真实访问入口获取连接,并在事务开始处确认有效设置。把配置、观测和结果对应起来,才能解释“测试没问题,定时脚本却出错”的差异。性能参数应在正确性约束成立后调整,不能通过放宽隔离来掩盖错误的数据访问方式。

7 案例:隔离级别过低导致脏读事故

某主机巡检系统有张 host_check 表,保存主机编号、检查批次和状态。采集进程每轮先把状态改成 healthy,随后检查进程与挂载点,全部通过才提交;失败则回滚。另个发布脚本读取状态,只有健康才继续。这个设计已埋下问题:状态在检查完成前就被写成成功,系统把安全性寄托在其他会话看不到中间状态上,却没有把可见性要求写进方案。

为了降低读等待,有人把发布脚本连接设为读未提交。在支持真实脏读的关系数据库中,故障顺序如下:10:00:00,采集事务 A 写入健康状态但未提交;10:00:01,发布事务 B 读到健康并启动外部部署命令;10:00:02,A 发现文件系统只读,回滚修改。数据库最终状态恢复为异常,但外部部署已经发生。回滚可以撤销事务内修改,无法自动撤销 B 已执行的 shell 动作。

这里的故障不是“缓存旧了”。B 读到的是尚未提交的新值。证据应包括 A 的事务起止与回滚记录、B 的有效隔离设置、读取时间及外部动作时间。如果只保存最终表状态,事故后看到异常状态反而可能误判为发布脚本没有查询数据库。实验必须选用确实支持脏读的实现;在把读未提交映射为读已提交的系统上,不能声称复现了同样的时间线。

直接修复是禁止该路径读未提交,并把“通过检查”作为完成后的结果写入。进一步为结果加入 check_id、checked_at 和明确状态,发布脚本要求已提交、未过期且对应目标主机的结果。读已提交可以排除本例中的脏读,却不保证读取之后环境不会变化。若检查到发布之间必须保持某种条件,需要额外的协作协议、再次校验或能够识别状态变更的机制,不能把更强隔离当成主机环境冻结器。

按症状分流并保留可复查证据
按症状分流并保留可复查证据

回归验证安排 2 个真实数据库会话,控制 A 暂停在提交前,让 B 发起读取,再分别测试 A 提交与回滚。断言不只是“B 没报错”,而是“B 不把未提交结果作为通过依据”。再检查发布动作是否可按批次去重、失败后是否能够确认已经执行的阶段。这样既验证数据库边界,也验证数据库之外的行为,避免事故换成网络中断后再次出现。

8 把一致性假设写进数据访问方案

方案应从不变量开始写。对维护任务,可以规定“同个任务仅允许 1 个有效领取者”“状态为 running 时必须存在 owner”“任务完成记录必须对应明确的执行批次”。接着说明每条规则由什么保障:字段约束限制空值,条件更新竞争领取权,唯一约束避免重复批次,事务让相关记录共同提交。不能用“采用数据库保证一致性”省略这些对应关系,数据库无法推断没有表达出来的规则。

随后描述每条访问路径。采样写入按批次提交,查询读取已提交结果;任务领取通过带旧状态条件的更新完成,并检查行数;批量维护按稳定顺序访问资源。写明数据库实现、有效隔离级别、事务覆盖的语句和提交位置。对于长查询,明确是否要求整次读取保持同个视图;对于外部命令,说明读取到执行期间发生变化时如何处理。读已提交可能足够,也可能不够,选择应来自可接受结果。

失败处理要按可观察状态编写。约束冲突应返回可解释结果;死锁或可重试冲突回滚后有限重试;锁等待超时应记录阻塞关系;提交响应丢失时按操作标识查询结果。把事务错误、网络错误与业务拒绝分开,便于调用者决定是否重试。重试次数、总时长和重复动作处理应是接口契约的一部分,不能藏在驱动默认配置里。

最后列出容量与观测假设。预计每台主机多久产生多少记录、保留多久、最常见查询返回多少行,决定索引与清理方案的起点。保留语句模板、计划、会话等待和主机资源的关联方式,并约定敏感字段的处理。验证材料应同时包含并发场景和性能场景:前者检查允许看到什么,后者检查为此付出多少资源。上线后的指标阈值来自测量与目标,而非某个通用命中率数字。

9 从数据库原理走向MySQL/PostgreSQL 实战(S42)

进入 S42 时,需要把本课的问题映射到具体接口。先确认连接到哪个实例、哪个数据库、哪个用户,再确认事务是否自动提交、隔离设置在哪个范围生效。客户端、驱动与连接池都可能影响边界。相同 SQL 在不同实现中的行为不能仅凭语法相似判断,应查当前版本文档,并用 2 个会话做最小并发实验。

接着把执行计划和活动视图变成可复用的排查入口。认识估计与实际数据的区别,能找到正在等待的会话及其阻塞者,能把应用请求与数据库连接关联,已经足以支撑许多入门排障。索引先从明确查询开始,事务先从明确不变量开始。暂时不需要展开存储页格式、日志内部布局或复杂优化器算法,这些细节应在能解释基本证据后再学。

准备练习时保留固定数据、明确时间范围和可重放步骤。每次只改变能够解释的条件,记录正确结果与资源变化。遇到慢查询,先分清连接、执行、等待还是取数;遇到错误结果,先还原提交顺序、有效隔离和访问条件。带着这套问题进入具体数据库实战,比记住大量无法判断适用范围的参数更可靠。

放进方案里

在主机巡检与维护任务方案中,先列明数据身份:进程记录使用主机、启动时间与进程号共同识别,检查结果附带批次和完成时间,任务操作附带可去重的标识。明确“检查完成后才可发布结果”“同个任务仅有 1 个有效领取者”等不变量,逐条关联到约束、条件更新或事务,避免把正确性寄托在脚本执行速度上。

画出访问边界:哪些语句属于同个事务,读取使用什么有效隔离级别,提交发生在哪里,外部 shell 动作何时执行。巡检状态必须来自已提交且未过期的检查结果;主机条件在读取后仍可能变化,方案应说明再次校验的时机和变化后的处理。事务中不要等待人工输入或长时间网络调用,领取任务后也要能够识别工作进程退出留下的未完成状态。

列出失败分支:锁等待超时如何定位阻塞者,死锁中止后如何整体回滚并有限重试,连接断开导致提交结果未知时如何按操作标识确认。每条分支都写出调用者能够获得的结果,以及是否允许再次执行。对文件修改、远程命令等外部动作,保留执行阶段和结果确认方式,使数据库重试不会无声地重复动作。

为常用查询附上固定参数的执行计划、返回量与扫描量,说明候选索引的用途和写入成本。观测同时覆盖事务时长、等待类型、查询延迟、连接状态和文件系统空间,缓存命中率只作为性能背景。验收时用 2 个会话分别验证提交、回滚与竞争领取,再用代表性数据测量查询;保留预期结果、实际证据和适用版本,作为后续调整方案的依据。

10 小结:下一步MySQL/PostgreSQL 实战(S42)

数据库原理把数据规则、并发可见性、访问路径和故障恢复联系起来。事务需要明确边界,隔离需要对应允许的结果,索引需要由计划与访问量证明价值,等待需要关联到具体会话和资源。主机指标能解释运行条件,不能单独证明数据正确。

进入 S42 前,可用本课场景自查:任务领取怎样防止竞争覆盖?客户端超时后怎样确认提交结果?高缓存命中率为什么仍可能慢?脏读事故需要哪几段时间证据?能够把这些问题落实到语句、会话和日志,就具备继续学习 MySQL/PostgreSQL 实战的起点。

同阶段相关课

AI 答疑

默认关闭。启用后,只发送本课标题、最近阅读的屏幕内正文摘录(最多 6000 字)与本次问题。

配置自己的服务