资讯动态

数据库重学

发布时间:2026/10/29 13:20:09 来源:尧图企业网站定制
关系型数据库这个关系怎么理解关系就是一个二维表不是表与表之间的关系一个表就是一个关系关系的好处数据只存一份靠外键关联改一处处处更新不会出现「张三的班级在一班表里叫’一班’、在二班表里叫’1班’」这种数据不一致查询可以任意组合任意几张表用 JOIN 现场拼起来写什么条件都行。查询能力不依赖你提前设计的存储结构完整性有约束主键、外键、唯一、非空、检查约束由数据库保证而不是靠应用自觉核心价值可以概括成一句结构化 数据不冗余 查询自由MySQL 关系型CREATETABLEusers(idINTPRIMARYKEY,nameVARCHAR(50));CREATETABLEorders(idINTPRIMARYKEY,user_idINT,amountDECIMAL(10,2),FOREIGNKEY(user_id)REFERENCESusers(id));-- 用的时候现查现拼SELECTu.name,o.amountFROMusers uJOINorders oONo.user_idu.id;MongoDB 文档型{_id:u01,name:张三,orders:[{amount:99.00,date:2026-03-01},{amount:25.50,date:2026-04-12}]}Redis 键值型user:u01-Hash{name:张三}user:u01:orders-ZSet{order:99.00,order:25.50}counter:today-String10248关系型 反冗余数据存一份查的时候现拼写入一致、查询灵活文档型 冗余换读速把要一起读的数据放一起读一次拿全但更新时要同步多份键值型 结构换速度几乎没有查询能力靠 key 的设计直接定位快到极致咋选用 MySQL 当主存储当你有明确的业务实体和关联用户-订单-商品-支付需要跨表统计、报表、复杂筛选对数据一致性要求高钱、库存、状态流转加 MongoDB当你有结构不确定、字段经常变日志、埋点、内容、配置数据天然是嵌套树形一篇带评论的文章、一份设备遥测需要水平扩展到很大规模且读多写多加 Redis当你有高频读的热点数据缓存挡在 MySQL 前面需要特殊数据结构的场景排行榜 ZSet、计数器 String、队列 List/Stream、去重 Set、Session Hash对延迟极度敏感内存操作微秒级非关系型数据库里面的BASEB A— Basically Available基本可用不是「勉强能用」而是「可以降级但不能全挂」。当系统遇到压力或故障时它选择牺牲部分性能或功能而不是整体拒绝服务。典型例子是双十一淘宝会把推荐、评论、部分非核心接口降级甚至关闭保证下单、支付、查订单这条主链路还能跑。这就是「基本可用」——功能上打折但基本服务不中断。对比 ACID传统数据库在资源不够时可能直接锁等待、超时、报错——它宁可不可用也要保证正确。S — Soft State软状态允许系统存在「中间状态」「旧状态」不要求随时反映最新值。与之相对的是 Hard State硬状态任何时刻都必须严格满足约束比如银行账户余额在任何瞬间都必须等于流水累计值不能有模糊地带。软状态的例子你给微博点了赞自己界面立刻 1但你朋友手机上显示的数字可能还是旧值过几秒才更新。在这个「过几秒」的窗口里系统处于软状态——数据暂时是旧的但这是被允许的。E — Eventually Consistent最终一致性最终一致 ≠ 不一致。它是「延迟一致」。只要一段时间内没有新的写入所有副本最终都会达成一致。关键是「最终」没有硬性时间保证——可能是毫秒也可能是秒级取决于网络和实现。用一个比喻ATM 存钱和手机银行余额。你在 ATM 存了 500ATM 立刻显示新余额但手机银行可能几分钟后才刷新。中间这段时间不一致但过一会儿都会对上——最终一致。如果这时候你冲进银行说「手机上钱少了」柜员不会说「系统错了」而是「稍等刷新」数据库的三大范式第一范式1NF要求数据库表的每一列都是不可分割的原子数据项第二范式需要确保数据库表中的每一列都和主键相关而不能只与主键的某一部分相关第三范式需要确保数据表中的每一列数据都和主键直接相关而不能间接相关数据库的链表查询内连接INNER JOINSELECTemployees.name,departments.nameFROMemployeesINNERJOINdepartmentsONemployees.department_iddepartments.id;返回每一个员工及其所在的部门不包括没有分配部门的员工不包括没有分配员工的部门左外连接 (LEFT JOIN)SELECTemployees.name,departments.nameFROMemployeesLEFTJOINdepartmentsONemployees.department_iddepartments.id;这个查询返回所有员工及其部门名称包括那些没有分配部门的员工右外连接 (RIGHT JOIN)SELECTemployees.name,departments.nameFROMemployeesRIGHTJOINdepartmentsONemployees.department_iddepartments.id;这个查询返回所有部门及其员工包括那些没有分配员工的部门全外连接 (FULL JOIN)SELECTemployees.name,departments.nameFROMemployeesLEFTJOINdepartmentsONemployees.department_iddepartments.idUNIONALLSELECTemployees.name,departments.nameFROMemployeesRIGHTJOINdepartmentsONemployees.department_iddepartments.idWHEREemployees.idISNULL;第二部分只取右表没有匹配到员工的行再用 UNION ALL 合并这样既不会重复返回已经匹配的行也不会把姓名相同的不同员工误当成重复结果去掉数据库的存储引擎MySQL 的架构分两层Server 层负责 SQL 解析、优化、执行计划……所有引擎共用存储引擎层负责数据怎么落到磁盘上——怎么建索引、怎么加锁、崩溃了怎么恢复、支不支持事务。CREATETABLEstudents(idINTPRIMARYKEY,nameVARCHAR(50))ENGINEInnoDB;ENGINE InnoDB 就是在建表时声明这张表交给 InnoDB 这个引擎来存5.5 之后默认就是 InnoDB特性InnoDBMyISAMMemory事务✅❌❌行级锁✅表锁表锁外键✅❌❌崩溃安全✅❌❌数据在内存全文索引✅5.6✅❌典型用途几乎所有业务表读多写少的日志/统计表临时表、缓存表IN和EXISTS的区别IN 用于检查左边的表达式是否存在于右边的列表或子查询的结果集中。如果存在则IN 返回TRUE否则返回FALSEEXISTS 用于判断子查询是否至少能返回一行数据。它不关心子查询返回什么数据只关心是否有结果。如果子查询有结果则EXISTS 返回TRUE否则返回FALSE。返回的是NULL也是有数据IN的UNKNOW问题SQL 里的 IN / NOT IN 本质是 一串等值比较用 AND / OR 连起来而任何涉及 NULL 的比较结果都是 UNKNOWN未知不是 TRUE 也不是 FALSESELECT1NOTIN(2,NULL);-- 1 NOT IN (2, NULL) 等价于12AND1NULL表达式结果1 2TRUE1 NULLUNKNOWN任何与 NULL 的比较都是 UNKNOWNTRUE AND UNKNOWNUNKNOWN三值逻辑TRUE / FALSE / UNKNOWN的 AND 规则是TRUEANDUNKNOWNUNKNOWNFALSEANDUNKNOWNFALSENOT IN 特别容易踩坑-- IN只要有一个比较是 TRUE整个就是 TRUE1IN(2,NULL)-- 12 OR 1NULL → FALSE OR UNKNOWN → UNKNOWN1IN(1,NULL)-- 11 OR 1NULL → TRUE OR UNKNOWN → TRUE ✓ 没事-- NOT IN所有比较都必须是 TRUE一旦有 NULL 就整体 UNKNOWN1NOTIN(2,NULL)-- 12 AND 1NULL → TRUE AND UNKNOWN → UNKNOWN ✗ 挂了现实的问题和避免-- 想找出没有下过订单的客户SELECT*FROMcustomers cWHEREc.idNOTIN(SELECTcustomer_idFROMorders);SELECT*FROMcustomers cWHERENOTEXISTS(SELECT1FROMorders oWHEREo.customer_idc.id);WHEREc.idNOTIN(SELECTcustomer_idFROMordersWHEREcustomer_idISNOTNULL)只要 orders.customer_id 里有一行是 NULLNOT IN 子查询对每一行都会返回 UNKNOWN结果就是 一行都查不出来而且没有任何报错——静默地给你错误答案EXISTS 只关心有没有行不涉及 NULL 比较结果是确定的 TRUE/FALSE显式排除 NULL执行的顺序FROM/JOIN→ON→WHERE→GROUPBY→HAVING→SELECT→DISTINCT→ORDERBY→LIMIT已付款订单超过 10 笔的城市里销量最高的 5 个是哪些SELECTDISTINCTc.cityAS城市,COUNT(*)AS订单数FROMorders oJOINcustomers cONo.customer_idc.idWHEREo.statuspaidGROUPBYc.cityHAVINGCOUNT(*)10ORDERBY订单数DESCLIMIT5;| 顺序 | 子句顺序子句在这条语句里做什么1FROM / JOIN从 orders 出发连接 customers2ON用 customer_id id 决定两表怎么配对3WHERE先筛掉非 paid 的订单4GROUP BY按城市把剩余订单分组5HAVING只保留订单数 10 的城市6SELECT算出城市名和订单数7DISTINCT对结果行去重8ORDER BY按订单数从高到低排9LIMIT只留前 5 名WITH…AS我就知道不用写一大堆嵌套WITHStudentTotalScoresAS(-- 第一步算出每个学生的总分SELECTstu_id,SUM(score)AStotal_scoreFROMstudent_scoreGROUPBYstu_id),RankedStudentsAS(-- 第二步给总分排名次SELECTstu_id,total_score,RANK()OVER(ORDERBYtotal_scoreDESC)ASrankingFROMStudentTotalScores)SELECTstu_id,total_score-- 第三步取第 5~10 名FROMRankedStudentsWHERErankingBETWEEN5AND10ORDERBYranking,stu_id;WITH … AS … 是 SQL 里很常用的一个语法叫 CTECommon Table Expression公用表表达式WITH 就是先查一些数据然后把这个数据变成一个表然后在后面的主查询里可以像用一张真实的表一样 FROM 临时表名每一段结构名字 AS ( 查询 )多段之间用逗号 , 分隔最后一段后面不加逗号WITH 整体必须放在 SQL 的最开头锁、事务等事务要么全做要么全不做 └─ 并发问题两个人同时改同一行数据怎么办 └─ 锁让后来的人等一等 └─ 行锁 / 共享锁 / 排他锁 └─ SELECT ... FOR UPDATE手动加锁 └─ 隔离级别事务之间能看多新的数据 └─ 幻读问题 └─ 间隙锁 gap lock副作用 └─ 死锁 └─ 回到我们的锁表事务要么全做要么全不做假设要给 A 转 100 元给 B两条 SQLUPDATEaccountSETbalancebalance-100WHEREid1;UPDATEaccountSETbalancebalance100WHEREid2;UPDATEaccountSETbalancebalance-100WHEREid1;UPDATEaccountSETbalancebalance100WHEREid2;如果第一条执行完、第二条执行前断电了呢A 少了 100B 没多钱凭空消失。事务就是把多条 SQL 打包成一个不可分割的整体BEGIN;-- 事务开始UPDATEaccountSETbalancebalance-100WHEREid1;UPDATEaccountSETbalancebalance100WHEREid2;COMMIT;-- 提交两条一起生效BEGIN;-- 事务开始UPDATEaccountSETbalancebalance-100WHEREid1;UPDATEaccountSETbalancebalance100WHEREid2;COMMIT;-- 提交两条一起生效如果中途出错ROLLBACK回滚会把两条一起撤销恢复原样。关键点事务什么时候算结束 只有 COMMIT提交或 ROLLBACK回滚才结束。事务不结束它占着的东西包括锁就不还。并发问题两个程序同时改同一行假设库存是 1两个请求同时下单时刻 请求 A 请求 B t1 读库存1 t2 读库存1 t3 判断 1 0减 1写回 0 t4 判断 1 0减 1写回 0 结果卖出两件库存只减了 1。这叫丢失更新lost update。原因A 的读-改-写过程被 B 插进来了。数据库必须提供一种机制让 A 干活的时候 B 只能等着 —— 这就是锁。锁让后来的人等一等3.1 共享锁S 锁与排他锁X 锁共享锁SharedS 锁读锁。大家都能一起读但不能改。排他锁ExclusiveX 锁写锁。只有我能读写别人什么都不能做。兼容规则一行锁上的矩阵当前持有 ↓ \ 想申请 → S 锁 X 锁S 锁 兼容可以共存 冲突等待X 锁 冲突等待 冲突等待口诀读读可以共存读写、写写都要等。nnoDB 的一个反直觉设计普通的 SELECT 不加任何锁。它读的是快照MVCC多版本所以读的人不会阻塞写的人写的人也不会阻塞读的人。这对性能很好但意味着你 SELECT 出来的值可能在你下一句 SQL 执行前就被别人改掉了。所以我们需要手动加锁的场景得自己说。行锁锁是加在索引记录上的InnoDB 的锁不是加在整张表上而是加在一行一行的索引记录上。这一点后面很重要如果你的 WHERE 条件没走索引可能演变成锁全表。SELECT … FOR UPDATE 是什么语法SELECT*FROMstockWHEREsku_id100FORUPDATE;SELECT*FROMstockWHEREsku_id100FORUPDATE;意思是把查出来的每一行都加上 X 锁直到我这个事务结束COMMIT/ROLLBACK才释放。现在改写扣库存BEGIN;SELECTquantityFROMstockWHEREsku_id100FORUPDATE;-- 加 X 锁别人排队-- 读到 quantity 1UPDATEstockSETquantityquantity-1WHEREsku_id100;COMMIT;-- 这里才释放锁BEGIN;SELECTquantityFROMstockWHEREsku_id100FORUPDATE;-- 加 X 锁别人排队-- 读到 quantity 1UPDATEstockSETquantityquantity-1WHEREsku_id100;COMMIT;-- 这里才释放锁这时候请求 B 来执行同样的语句会在 SELECT … FOR UPDATE 那一步被卡住等 A 提交后才能继续。丢失更新就解决了。一个容易忽略的规则锁在事务结束才释放。所以如果你不开事务autocommit 模式下每句 SQL 自己一个事务SELECT … FOR UPDATE语句一执行完锁就放了等于没锁。这解释了为什么我们代码里要 BEGIN … COMMIT。幻读 → 间隙锁 gap lock问题的源头什么是幻读BEGIN;SELECT*FROMuserWHEREage30;-- 返回 5 行-- 这时另一个事务插入了一条 age35 的新用户并提交SELECT*FROMuserWHEREage30;-- 返回 6 行凭空多出来一行COMMIT;BEGIN;SELECT*FROMuserWHEREage30;-- 返回 5 行-- 这时另一个事务插入了一条 age35 的新用户并提交SELECT*FROMuserWHEREage30;-- 返回 6 行凭空多出来一行COMMIT;同一句查询两次结果不一样。多出来的那行像个幻影叫幻读phantom read。间隙锁把空隙也锁住要防幻读光锁住已存在的行不够 —— 鬼是新插入的行。于是 InnoDB 在 RR 级别下发明了间隙锁gap lock不光锁住记录本身还锁住记录与记录之间的空隙禁止在这个空隙里插入新行。MySQL查询缓存为什么被删除查询缓存用一把全局大锁保护。每次执行查询无论读写都要先抢这把锁去查询/写入缓存锁竞争激烈失效策略是只要表发生任何写操作该表的所有缓存条目全部清空不是按行、按 key 失效而是整表全删缓存 key 是SQL 文本的完全匹配条件苛刻索引索引的本质就是一种把数据按照某种规则组织好的、额外的数据结构用来避免从头到尾挨个找。占磁盘空间索引本身也是数据拖慢写操作——每次 INSERT / UPDATE / DELETEMySQL 都要同步维护索引结构数据库不是把整个索引读进内存而是每读一个节点就是一次磁盘 IO以下是索引按什么数据结构来组织B树索引[ 30 | 60 ] ← 非叶子节点只存 key不存数据索引页 / | \ [10|20] [40|50] [70|80] ← 非叶子节点只存 key / \ / \ / \ [1|5|9]...[35|45]...[65|75]... ← 叶子节点存数据/指针 ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ 所有实际数据都在叶子节点 叶子节点之间用**双向链表**连接 ←── 这是关键非叶子节点只存key不存数据。key比数据小多了所以一个节点就是一个16k的页可以存更多的key那么孩子分支就越多树更矮所有数据都在叶子节点 查询稳定性高任何一次查询都是 3~4 次 IO不会忽快忽慢叶子节点用双向链表相连 范围查询BETWEEN、、ORDER BY极其高效找到起点后顺着链表走即可例子SELECT*FROMusersWHEREageBETWEEN20AND30;先从根节点一直定位到叶子节点3次IO然后沿着叶子节点的链表顺序读直到 age 30 停止哈希索引Hash Index对索引列的值做哈希运算得到桶的位置hash(张三) → 桶 5 → [指向数据行的指针]但是不支持范围查询全文索引FULLTEXT上面的索引都是针对值相等/大小比较而 WHERE content LIKE ‘%数据库%’ 这种模糊文本搜索 B 树帮不上忙全文索引用倒排索引Inverted Index记录每个词出现在哪些文档里数据库 → 文档 3, 文档 7, 文档 15 索引 → 文档 1, 文档 3, 文档 99以下是索引行和索引之间的存放关系聚簇索引主键索引聚簇索引的叶子节点直接存的就是整行数据数据和主键索引是一体的表数据本身就是一棵按主键组织的 B 树主键索引 B 树 [30 | 60] / | \ [10|20] [40|50] [70|80] / \ [1|5] [10|15|20] ← 叶子节点 整行数据id, name, age, email...在磁盘上这张表的物理存储就是按主键排序的二级索引非聚簇索引SELECT*FROMusersWHEREname张三;执行过程走 name 索引 B 树 → 找到 ‘张三’得到主键 id 1024拿 id 1024 再去主键索引 B 树查一次 → 拿到整行数据联合索引为什么要用联合索引CREATETABLEusers(idINTPRIMARYKEY,nameVARCHAR(20),ageINT,cityVARCHAR(20));给nameagecity都建一个索引得不偿失优化器只能挑一个用且要维护三个索引建立联合索引联合索引 把多个列拼成一个值来排序和查找的索引INDEXidx_name_age_city(name,age,city)插入顺序索引排列顺序(李四, 25, 北京) ← id5 (李四, 28, 广州) ← id3 (王五, 25, 北京) ← id7 (王五, 30, 上海) ← id1 (张三, 20, 北京) ← id4 (张三, 25, 上海) ← id6 ← 注意city 变了但 nameage 相同所以排在一起 (张三, 25, 北京) ← id2 ← 这里顺序取决于实现但 nameage25 的都聚在一堆但是有一个最左前缀原则从最左边开始匹配聚簇索引和非聚簇索引什么是聚簇聚簇就是把东西聚在一起聚簇索引就是把索引和数据聚在一起了索引和数据聚在同一棵树的叶子里 name/id → 整行数据直接就在手边非聚簇索引就是索引的叶子节点存放的是主键值不是完整数据索引树一边 数据另一边 name → id id → 整行数据 李四 → 5 需要再跑去另一边找

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑