知识

sql原理

时间:2010-12-5 17:23:32  作者:热点   来源:热点  查看:  评论:0
内容摘要:```html sql原理 body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvet

```html sql原理

写一条 SELECT * FROM users WHERE id = 1,数据库在几十毫秒内就把结果甩到你面前。它到底干了什么?这背后不是魔法,是一套相当死板的机械流程。理解这套流程,比背一百条优化技巧都管用。

SQL 的本质是声明式语言。你告诉数据库“我要什么”,而不是“怎么拿”。这个“怎么拿”的过程,全部由数据库的查询优化器和执行引擎接管。你写的那句 SQL,在数据库眼里只是一段文本,它得先被拆解、翻译、重新组装,最后才变成对磁盘上数据页的操作。

解析:把你的话拆成语法树

数据库收到 SQL 文本,第一件事是词法分析和语法分析。词法分析把字符串切成一个个 token:SELECT、*、FROM、users……语法分析再按 SQL 语法规则把这些 token 组合成一棵抽象语法树(AST)。

如果语法有错,比如少了个括号,这一步就报错了。但语法正确不代表语义正确——表名不存在、字段拼错、类型不匹配,这些要到语义分析阶段才查。语义分析会去查数据字典(系统表),确认 users 表真的存在,id 字段确实有,然后把这棵 AST 转换成逻辑执行计划。

逻辑执行计划是一棵关系代数树。比如 SELECT name FROM users WHERE age > 18,逻辑计划大致是:先对 users 做选择(σ age > 18),再对结果做投影(π name)。注意,这时候还没决定怎么扫表、用不用索引。

优化器:真正的智力战场

逻辑计划只是“逻辑上可行”的方案。同一个查询,可以有几十种物理执行方式。比如 WHERE age > 18,可以全表扫描,也可以走 age 上的 B+ 树索引;如果涉及两张表连接,可以用嵌套循环、哈希连接、排序合并连接。选哪个,直接决定查询是跑 0.1 秒还是 10 秒。

优化器的工作就是基于代价估算,从所有等价物理计划里挑一个它认为最便宜的。代价怎么算?靠统计信息:表有多少行、字段的基数(不同值个数)、数据分布直方图、索引的层级高度。这些统计信息通常由 ANALYZE 命令或自动任务更新。

问题在于,统计信息经常不准。数据倾斜、频繁更新、采样误差,都会让优化器做出错误判断。我见过一个案例:某张表 90% 的行 status = 'active',但统计信息还停留在半年前,优化器以为 status = 'active' 只返回几行,于是选了索引扫描,结果每次查询都要回表几百万次,慢得离谱。后来手动 ANALYZE 一下,计划立刻变成全表扫描,耗时从 8 秒降到 0.3 秒。

优化器不是神。它依赖的统计信息可能过期,代价模型也可能和实际硬件不匹配。所以 DBA 经常要干的事就是“骗”优化器——用 hint、改写 SQL、调整统计信息采样率。

执行:火山模型与流水线

物理计划确定后,执行引擎开始干活。大多数关系数据库采用火山模型(Volcano Model),也叫迭代器模型。每个算子(扫描、过滤、连接、聚合)都实现一个 next() 接口,每次调用返回一行数据。

比如一个简单的过滤算子,它的 next() 会不断调用子算子的 next(),拿到一行就检查 WHERE 条件,满足就返回,不满足就继续拉下一行。这种拉取式流水线的好处是内存占用小,不需要把中间结果全部物化。但坏处是函数调用开销大,CPU 缓存不友好。

现代数据库做了很多改进。向量化执行一次处理一批行(比如 1024 行),而不是一行一行拉,大幅减少虚函数调用。还有编译执行,把整个执行计划直接编译成机器码,消除算子间的解释开销。ClickHouse、DuckDB 在这方面走得比较靠前。

 -- 一个典型的执行计划(EXPLAIN 输出) Hash Join (cost=10.5..20.3 rows=100 width=64) Hash Cond: (orders.user_id = users.id) -> Seq Scan on orders (cost=0.00..8.0 rows=500 width=36) -> Hash (cost=1.2..1.2 rows=80 width=32) -> Index Scan on users (cost=0.0..1.2 rows=80 width=32) Index Cond: (age > 18) 

上面这个计划里,Index Scan 走索引拿到年龄大于 18 的用户,然后 Hash 算子把这些用户建哈希表,最后 Hash Join 拿订单表去探测。每一步的 cost 和 rows 都是优化器估算的,实际执行时可能差很远。

存储引擎:数据到底在哪

执行算子最终要落到存储引擎。InnoDB 里数据按聚簇索引组织,主键索引的叶子节点就是完整行数据。二级索引的叶子节点存的是主键值,所以通过二级索引查非索引字段,必须回表——拿主键再去聚簇索引里捞一次。

这就是为什么 SELECT * 往往比 SELECT id, name 慢。覆盖索引能避免回表:如果查询需要的字段全在索引里,存储引擎直接返回索引数据,不用碰聚簇索引。我习惯在频繁查询的字段组合上建覆盖索引,效果立竿见影。

B+ 树的高度通常只有 3 到 4 层。根节点常驻内存,所以一次索引查找最多两三次磁盘 I/O。但如果你用 LIKE '%abc' 这种前置模糊匹配,B+ 树的有序性直接废掉,只能全表扫描。这是原理层面的限制,加再多索引也没用。

事务与并发:看不见的锁

SQL 执行不是孤立的。多个事务同时跑,数据库得保证隔离性。InnoDB 用 MVCC(多版本并发控制)实现读不加锁。每行数据有隐藏的 trx_id 和 roll_pointer,读操作根据事务的 Read View 判断哪个版本可见。

写操作就复杂了。更新一行,先加行锁,写 undo log,再修改当前行,写 redo log。如果两个事务更新同一行,后到的会阻塞,直到前一个提交或回滚。间隙锁更麻烦,SELECT ... FOR UPDATE 在可重复读隔离级别下会锁住记录之间的间隙,防止幻读,但也容易引发死锁。

理解这些,你才能明白为什么“长事务”是灾难。一个事务开了十分钟没提交,undo log 膨胀,锁持有时间过长,其他事务全在排队。很多线上故障的根因就是某个业务代码忘了提交事务。

SQL 原理不复杂,但细节极多。从解析到优化到执行到存储,每一层都有取舍和坑。你写的每一句 SQL,背后都是数据库在替你做几百个决策。知道它在想什么,你才能写出它喜欢的查询。

```
最近更新
热门排行
copyright © 2026 powered by 力不从心网   sitemap