当前位置:首页 > 问答 > 正文

SQL Server那些复杂又实用的技术细节,带你一步步搞懂数据库核心知识

整理自多本SQL Server技术书籍、微软官方文档及资深DBA的实践经验分享)

页与区:数据存储的“最小作战单元”

你得先知道,SQL Server不是直接把你的数据乱扔进硬盘的,它有一个非常精细的“仓库管理系统”,最基本的仓库货架就是“页”,一页的大小固定是8KB,你建的一张表、一条条记录,最终都是存放在这些页里面,一页存满了,就换下一页。

但操作系统管理硬盘的时候,不会一页一页地去要空间,那样效率太低了,所以SQL Server又搞了个更大的单位叫“区”,一个区是8个连续的页,也就是64KB,区是空间分配的基本单位,当你新建一张表并插入数据时,SQL Server会一次性分配一个区给它,哪怕你只用了其中一页。

这里有个关键细节(来源:Microsoft Docs - 表和索引组织):区分为“混合区”和“统一区”,混合区里的8个页可以属于8个不同的对象(比如8张不同的表),而统一区里的8个页全都属于同一个对象,为什么这么设计?想象一下,如果你的数据库里有很多小表,每个表都给它分配一个完整的区(64KB),但每张表可能只占几KB,那就太浪费空间了,对于刚创建的小对象,SQL Server会先使用混合区来存放,等这个对象长大到一定程度(通常超过8页),再转为使用统一区,这个细节能帮你理解为什么有时候表很小,但占用的磁盘空间看起来却不小。

索引的“双向链表”与B-Tree的深层逻辑

大家都知道索引像书的目录,能加快查找速度,但它的内部结构比目录复杂得多。

SQL Server那些复杂又实用的技术细节,带你一步步搞懂数据库核心知识

聚集索引的叶子节点就是数据页本身,数据真正按照索引键的顺序物理存储,非聚集索引的叶子节点则只包含索引键和指向数据行的“指针”(如果表有聚集索引,这个指针就是聚集索引键)。

这里有个非常实用但容易被忽略的细节(来源:SQL Server Internals书籍):索引的每一层(叶子层和中间层)的页,并不是孤立的,它们通过一个“双向链表”连接起来,这是什么意思?比如你有一个按时间排序的聚集索引,那么所有叶子页(也就是数据页)会像一个链条一样,按时间顺序首尾相连。

这个设计带来了巨大的好处:范围查询变得极其高效,比如你要查询“2023年1月1日到1月31日”的所有订单,数据库只需要通过B-Tree快速找到1月1日所在的页,然后沿着页之间的双向链表向后扫描就可以了,完全不需要再回到索引的根节点去重新查找下一页,这个“链式结构”是索引性能的关键之一。

锁与阻塞的“微观世界”:行锁、键范围锁和锁升级

当多个用户同时操作数据库时,锁机制保证了数据的一致性,但也带来了阻塞问题。

SQL Server那些复杂又实用的技术细节,带你一步步搞懂数据库核心知识

最基础的锁是行锁,即只锁住你要修改的那一行,但在可重复读或序列化隔离级别下,为了防止“幻读”(即两次查询之间插入了新数据),SQL Server会使用一种更精细的锁——键范围锁(来源:SQL Server技术内幕:事务管理与并发控制),这种锁不仅锁住索引中存在的键,还会锁住这些键之间的“范围”,比如你查询WHERE id BETWEEN 10 AND 20,数据库可能会在id=10和id=20这两个索引项上设置锁,并锁住10到20这个区间,阻止其他事务插入id=15这样的新数据,理解键范围锁是解决高并发环境下神秘阻塞问题的钥匙。

另一个重要细节是锁升级,SQL Server为了节省管理锁的开销,不会无休止地锁住成千上万行,当单个事务持有的锁数量超过一个阈值(例如5000个),或者锁占用的内存超过一定量时,数据库引擎会自动将大量细粒度的行锁或页锁,升级为一个更粗粒度的表锁,锁升级的本意是好的,是为了提升系统整体性能,但如果你有一个大批量更新数据的事务,突然在关键时刻被升级为表锁,就会瞬间阻塞其他所有对这个表的操作,导致应用超时,你可以通过跟踪标志或表选项来监控甚至控制锁升级行为,这是处理批量作业时必须考虑的点。

统计信息:查询优化器的“眼睛”

SQL Server如何决定用哪个索引?是全表扫描还是索引查找?它靠的不是猜,而是靠“统计信息”。

统计信息本质上是关于表中列值分布情况的一组直方图和数据摘要,你有一张用户表,有个“城市”字段,统计信息会告诉优化器,值“北京”大概有多少行,“上海”有多少行,总共有多少个不同的城市等。

SQL Server那些复杂又实用的技术细节,带你一步步搞懂数据库核心知识

一个复杂又关键的细节是(来源:微软PSS工程师博客):统计信息会“过期”,当你频繁地对表进行增删改操作,导致数据分布发生显著变化后,旧的统计信息就无法准确反映现状了,这时,优化器就像一个近视眼没戴眼镜,可能会制定出一个非常愚蠢的执行计划,它可能错误地估计某个查询只会返回10行,于是选择了嵌套循环连接,但实际上返回了10万行,导致查询性能灾难性地下降。

SQL Server有自动更新统计信息的机制,但在数据变化极快的OLTP系统或超大型数据仓库中,这个自动更新可能不及时或采样率不够高,资深DBA会定期检查统计信息的更新日期,并可能使用FULLSCAN选项手动更新,以确保优化器始终有最“明亮”的眼睛。

事务日志的“不可切割”与VLF碎片

事务日志(LDF文件)记录了对数据库的每一个修改操作,它最重要的特性是“预写日志”(WAL):任何数据修改必须先把记录写到日志文件里,才能写到数据文件里,这是保证数据恢复和事务ACID属性的基石。

一个非常技术化但影响巨大的细节是事务日志的物理结构(来源:SQL Server Central知名技术文章),日志文件在内部被分成多个称为“虚拟日志文件”的小段,当数据库运行时,日志记录会按顺序写入当前的VLF,当一个VLF写满后,就切换到下一个。

问题来了:如果日志文件设置得过大或过小,或者增长设置不合理(比如每次只增长1MB),会导致VLF数量过多且碎片化,这被称为“VLF碎片”,高度碎片化的VLF会显著拖慢所有依赖日志的操作,包括日志备份、数据库恢复、事务回滚甚至常规的日志写入操作,检查VLF数量并通过适当大小的日志文件增长或重建日志文件来减少VLF碎片,是数据库性能调优的一项高级但收效显著的工作。

这些细节只是SQL Server庞大冰山的一角,但每一个都直接关系到数据库的稳定性、性能和可维护性,理解它们,意味着你不再只是会写SQL语句的用户,而是开始真正洞察数据引擎内部运作的维护者。