feat: 更新附录交互组件和文档

This commit is contained in:
sanbuphy
2026-02-24 00:18:09 +08:00
parent d45df3cda5
commit 94f9db0834
88 changed files with 11797 additions and 7634 deletions
@@ -1,84 +1,224 @@
# 计算机网络:从输入网址到返回结果的过程
::: tip 🎯 核心问题
**当你浏览器输入 www.google.com 并按下回车,到底发生了什么**
**当你舒服地靠在沙发上,在手机浏览器输入 `www.google.com` 并按下回车,为什么几百毫秒后,搜索结果就能准确无误地出现在你的屏幕上**
这个看似简单的动作,背后隐藏着一个庞大精密的跨国“快递系统”。从填写订单(URL解析)到查询地址簿(DNS解析),从建立运输通道(TCP握手)到快递员送货(HTTP请求与响应),最终在你屏幕上拆开包裹组装(浏览器渲染)。本章带你零基础、完整理解这个神奇的过程。
在上一章中,我们知道了数据是如何被编码成 0 和 1 并通过海底光缆传输的。但这还不够。互联网上的服务器浩如烟海,你的手机是怎么在茫茫机海中精准找到 Google 的服务器,商量好暗号,并成功把页面要回来的呢?
这个看似无比简单的"敲回车"动作,背后其实隐藏着一个精密到令人震撼的跨国"快递接力系统"。本章,我们不讲枯燥的八股文概念,而是顺着**"填写购物单 -> 查地址簿 -> 打电话确认 -> 寄包裹 -> 自己拆解组装"**这条主线,带你零基础看清网络世界的全貌。
:::
---
## 全景演示:网络世界的快递系统
## 第一步:填写购物单 (URL 解析)
你可以通过下方的交互组件,直观地体验从输入网址到看到网页的 5 个关键步骤。先自己点一点,然后再看底下的详细解释!
**目标**:把人类能看懂的网址,翻译成浏览器能理解的结构化信息。
<UrlToBrowserDemo />
当你在地址栏中输入 `https://www.google.com/search` 时,浏览器第一步必须先把你输入的这段"人类文字",仔细拆解成它能看懂的标准化字段。
这就像是你准备去商店买东西,首先要在**购物单**上写清楚:用什么交通工具去、去哪家店、拿什么货。
<UrlParserDemo />
**💡 核心原理解析:URL是怎么分工的?**
- **交通方式(Protocol/协议)**:比如开头写的 `https://`。这代表你要求坐安全级别最高的"运钞车"(加密通信)去。如果是老式的 `http://`,就相当于坐敞篷车,你一路上买什么都会被别人看光。
- **店铺名(Host/主机名)**:比如 `www.google.com`。这就是你要去哪家店(也就是服务器的域名)。
- **具体货架(Path/路径)**:比如后面的 `/search`。这代表进了店门之后,你要去哪个房间拿具体的哪份文件。
**这一步完成了什么?** 浏览器现在知道了:我要用 HTTPS 协议,去 `www.google.com` 这个域名对应的服务器,获取 `/search` 路径下的内容。
**但问题来了**:浏览器知道了域名,但网络世界只认数字 IP 地址。就像你知道"王府井大饭店",但司机需要 GPS 坐标。下一步,我们需要把域名转换成 IP 地址。
---
## 1. 填写购物单 (URL 解析)
## 第二步:查地址簿 (DNS 解析)
当你在浏览器的地址栏中输入 `https://www.google.com` 这样一段地址并按下回车,这就像是你准备去商店买东西,首先要在**购物单**上写清楚:
**上一步完成了**:浏览器拆解了 URL,知道了目标域名是 `www.google.com`
- **交通方式 (Protocol)**:例如 `https://`,代表你想坐安全级别的最高的“运钞车”(加密通信)去。如果是单纯的 `http://`,就相当于坐普通的“大巴”(明文传输),路上可能会被人偷看行李
- **店铺地址 (Host)**:例如 `www.google.com`,也就是你要去哪家店(域名)。
- **商品位置 (Path)**:例如 `/search`,意思是进了商店之后,你要去哪个货架找什么东西(即请求的具体资源路径)。
**这一步要实现**:把域名转换成 IP 地址,让浏览器知道服务器的精确位置
浏览器第一步要做的,就是把这段“人类语言”拆解开,看看你到底想要什么
**目的**:网络世界的底层路由器(负责指路的交警)根本不懂英文,它们**只认数字**,也就是所谓的 **IP 地址(如 142.250.80.46**
<DnsLookupDemo />
**💡 核心原理解析:找"114查号台"**
既然必须用 IP 地址,浏览器就会走一个叫做 **DNS (Domain Name System)** 的打听流程:
1. **翻自己的备忘录(本地缓存)**:浏览器会先翻翻自己的浏览历史,看看前几天是不是刚去过这家店,记没记过它的数字地址。如果记了,直接用。
2. **打电话给查号台(递归查询)**:如果实在没见过,它就会向互联网的"总查号台"(通常由你的宽带运营商提供,比如联通、电信的 DNS 服务器)发请求:"你好,请帮我查一下,google.com 对应的数字坐标是几?"
3. **拿到坐标**:查号台通过逐级查询,最终把一个准确的 IP 地址(如 `142.250.80.46`)发回给你的手机。
**这一步完成了什么?** 浏览器现在拿到了 Google 服务器的精确 IP 地址 `142.250.80.46`
**但问题来了**:有了 IP 地址就能直接发请求了吗?万一服务器宕机了呢?万一网线断了呢?如果直接发请求,对方没收到,就成了鸡同鸭讲。下一步,我们需要先确认双方能正常通信。
---
## 2. 查找店铺地址 (DNS 解析)
## 第三步:打电话确认 (TCP 三次握手)
网络世界的“快递员”(路由器设备)是不懂英文的,它们只认数字(也就是 **IP 地址**
**上一步完成了**:浏览器通过 DNS 查询,拿到了服务器的 IP 地址 `142.250.80.46`
它们需要知道对方的精确数字坐标!这就像快递员不知道“王府井百货”在哪,他必须先查地图,找到“北京市东城区王府井大街255号”这个确切的门牌号(比如 `142.250.66.4`
**这一步要实现**:建立一条可靠的通信通道,确保双方都能收发数据
- **本地缓存**:浏览器会先翻翻自己的备忘录(看之前有没有访问过该网站)。
- **DNS 系统**:如果在本地找不到,它就会向互联网的“查号台”(DNS 服务器)打电话询问:“请问 google.com 的数字地址是什么?”。一旦获得了对应的 IP 地址,浏览器的快递车就知道该往哪里开了。
**目的**:在正式传输数据之前,必须先确认"对方在线"且"双方收发通道都正常"。这就像打电话前要先确认"喂,能听到吗?"
<TcpHandshakeDemo />
**💡 核心原理解析:为什么非得是"三"次?**
不要被专业名词吓到,它完全可以在现实生活中还原。想象一下你给朋友打电话:
---
## 3. 建立通话 (TCP 握手)
### 第一次握手:SYN(同步请求)
拿到了地址,浏览器不能直接冲过去,万一店今天没开门呢?所以,要先进行一次**“电话确认”**(这叫建立 TCP 连接)。为了确保通话稳定可靠,会有三次非常严谨的“确认打招呼”机制,行业里叫**三次握手 (Three-way Handshake)**
**浏览器发送 SYN 包**
- **第一次握手 (浏览器)**:“喂,你好,我要来买东西,你在吗?” (SYN)
- **第二次握手 (服务器)**:“我在的,欢迎光临!你也听得到我说话吗?” (SYN-ACK)
- **第三次握手 (浏览器)**:“我也听到了!那我就要过来了!” (ACK)
就像你拨通朋友电话后说的第一句话:"喂,你好,能听到我说话吗?"
经过这三次确认,双方都知道了彼此的听力和表达能力都没问题,一条稳定可靠的通信通道就正式建立了。
- **SYN** 是 **Synchronize**(同步)的缩写
- 浏览器生成一个随机数字(比如 `Seq = 100`),告诉服务器:"我要开始建立连接了,我的初始序号是 100"
- 这个序号用来标记后续发送的数据顺序,防止乱序
**这一步确认了什么?** 服务器收到了浏览器的消息 → 浏览器的**发送通道**正常。
---
## 4. 购买商品 (HTTP 请求与响应)
### 第二次握手:SYN-ACK(同步+确认)
通道建好后,业务正式开始。
**服务器回复 SYN-ACK 包**
- **浏览器(买家)提交订单**:浏览器会打包一份极其规范的订单表格(**HTTP 请求报文**),里面写着:“老板,请给我拿一份你的主页 HTML 文件,我是用 Chrome 浏览器来访问的哦。”
- **服务器(卖家)根据订单发货**:位于地球另一端的 Google 服务器收到请求后,立刻开始在仓库里配货,生成网页的 HTML 代码,然后打包成包裹(**HTTP 响应报文**),发回给你的浏览器。包裹外面还会贴个标签“200 OK”,意思是“交易成功,你要的货全齐了”。
就像朋友回答:"喂喂,我能听到你!你也能听到我吗?"
- **SYN-ACK** = **Synchronize + Acknowledge**(同步+确认)
- 服务器做两件事:
1. **ACK**:确认收到浏览器的消息(`Ack = 101`,表示"我期待收到你序号为 101 的下一个包")
2. **SYN**:服务器也生成自己的随机序号(比如 `Seq = 200`),告诉浏览器:"我的初始序号是 200"
**这一步确认了什么?** 浏览器收到了服务器的回复 → 服务器的**发送通道**正常,浏览器的**接收通道**正常。
---
## 5. 拆盒组装 (浏览器渲染)
### 第三次握手:ACK(确认)
最后一步,货物送到了你的电脑。但发过来的只是一堆代码(HTML、CSS、JavaScript),这就好比你网购买了一箱乐高积木,还需要自己组装:
**浏览器回复 ACK 包**
1. **看说明书 (解析 HTML)**:浏览器先把 HTML 代码解读出来,拼装成网页的骨架(DOM 树)。
2. **涂抹颜色 (解析 CSS)**:然后检查 CSS 代码,看看字体要多大、按钮是什么颜色,给网页穿上漂亮的外衣(CSSOM 树)。
3. **计算布局并拼装 (Layout & Paint)**:浏览器计算好每个元素在屏幕上的确切位置,用画笔把它们画在你的显示器上。
4. **注入灵魂 (执行 JavaScript)**:最后,各种能点击、能滑动的交互效果都通过 JavaScript 激活。
就像你回答:"能听到!那我们开始聊正事吧!"
**只要短短的几百毫秒,所有的步骤就已全部完成,你也就看到了那个熟悉的页面!**
- **ACK** 是 **Acknowledge**(确认)的缩写
- 浏览器回复:`Ack = 201`,表示"我期待收到你序号为 201 的下一个包"
**这一步确认了什么?** 服务器收到了浏览器的确认 → 服务器的**接收通道**也正常。
---
## 总结:从微观到宏观
### 为什么必须是三次?两次行不行?
如果我们把目光再拉远一点,整个网络通讯的本质,就是在做**接力跑和翻译**
**假设只有两次握手:**
- 我们上面看到的这五步,大多是发生在你眼前的**应用程序**层面的事情。
- 在肉眼看不见的底层,刚才那个充满代码的 HTML 包裹,会被切分成无数块极小的碎片(数据包)。这些碎片顺着你家墙上的网线、海底的万兆光缆,像接力棒一样在各种路由器之间传递。
- 最终,这一切碎片完好无损地抵达,并在哪怕是几十个毫秒的时间里,化成你屏幕上的绚丽像素。
1. 浏览器:"喂,能听到吗?"
2. 服务器:"能听到!"
就是计算机网络的神奇魅力!
时候服务器以为连接建立了,开始发送数据。但如果服务器的回复在半路丢了,浏览器根本没收到,浏览器就不会认为连接建立成功,也不会处理服务器发来的数据。
**结果**:服务器单方面认为连接已建立,疯狂发数据,但浏览器全当垃圾丢弃。服务器资源被白白浪费。
**三次握手的精妙之处**
第三次握手的 ACK 包,**证明了浏览器确实收到了服务器的回复**。只有浏览器收到了,才会回复 ACK;服务器收到了这个 ACK,才能**100%确定**双方通道都是通的。
这就像打电话时的完整确认:
- 你:"喂,能听到吗?"SYN
- 朋友:"能听到,你呢?"SYN-ACK
- 你:"我也能听到!"ACK
**这一步完成了什么?** 浏览器和服务器都确认了:**我能发给你,我能收到你的,你也能发给我,你也能收到我的**。一条可靠的 TCP 通道正式建立!
**现在可以开始了吗?** 通道已建立,下一步就是正式发送请求,获取网页内容。
---
## 第四步:寄包裹 (HTTP 请求与响应)
**上一步完成了**:通过 TCP 三次握手,建立了可靠的通信通道。
**这一步要实现**:正式发送请求,获取网页内容。
**目的**:浏览器向服务器"下单",服务器返回"货物"(网页内容)。
<HttpExchangeDemo />
**💡 核心原理解析:HTTP 请求与响应的小纸条**
浏览器会把你刚才写好的购物单,按照一种极为规范的格式打包(这叫 **HTTP 请求头**),正式塞进刚才建立好的 TCP 通道里,发给服务器。
- **买方发纸条(HTTP Request**
浏览器发出的包裹里,写着大写的请求指令。如果是看网页就是 `GET`,如果是提交账号密码登录就是 `POST`。不仅如此,这张纸条里还附带了一些重要情报:"嗨,我是用 Mac 电脑的 Chrome 浏览器访问的哦,另外我只能听懂中文,请把给我的货也转换成中文。"(这些补充说明就被叫做 **请求 Headers**)。
- **卖方发纸条(HTTP Response**
位于千里之外的服务器收到这包东西后,看了一眼:"哦,他要 `GET` 这个页面啊"。于是服务器飞速在自己的硬盘里找到相应的 HTML 网页代码打包好,在包裹最外面贴上一个标签:`200 OK`(意思是交易非常成功,你要的货全齐了),然后借由同一个通道,原路寄回给你的电脑。
> **小科普**:如果是找不到你要找得页面,服务器就会贴个 `404 Not Found` 的悲伤标签给你退回来。如果是服务器自己代码写错了挂掉了,就会贴个 `500 Server Error` 的崩溃标签。
**这一步完成了什么?** 浏览器收到了服务器返回的 HTML、CSS、JavaScript 代码(也就是网页的"原材料")。
**但问题来了**:这些代码只是文本,还不是你能看到的网页画面。下一步,浏览器需要把这些代码"翻译"成屏幕上的像素。
---
## 第五步:拆解组装 (浏览器渲染)
**上一步完成了**:通过 HTTP 请求,浏览器获取了网页的源代码(HTML、CSS、JavaScript)。
**这一步要实现**:把代码转换成屏幕上可见的网页画面。
**目的**:将文本代码"翻译"成像素,让用户看到最终的网页。
<BrowserRenderingDemo />
**💡 核心原理解析:毫秒级的画家**
此时你电脑收到的,仅仅是一大串干瘪枯燥的文本代码(HTML 骨架、CSS 色彩图纸、JS 交互动效代码)。这就像你网购了一箱子乐高,它给你的只有几千个塑料零件和一本极度复杂的说明书。
浏览器的组装过程堪比惊心动魄的全自动工厂流水线:
1. **搭骨架 (DOM 解析)**:工人先把 HTML 文件通读一遍,理清楚网页的结构。比如"这里要有一个标题框,那里要有三个图片框"。这个骨架叫做 DOM 树。
2. **上颜色 (CSS 解析)**:紧接着看 CSS 文件,"哦,老王说标题框必须是红色的,图片框必须有圆角。"
3. **几何计算排版 (Layout)**:结合骨架和颜色后,开始拿尺子计算。因为每个人的屏幕大小不一样,同样是三个图片框,在手机上只能竖着放,在电脑上可以横着放。必须计算出每一个像素块极其精确的摆放坐标。
4. **上色绘制 (Paint)**:最后拿起了画笔,按照前面算出来的精确设计图,把真真切切的颜色和像素渲染到了你的显示器上!
**这一步完成了什么?** 浏览器把代码转换成了屏幕上的像素,用户终于看到了完整的网页!
---
## 完整流程回顾
让我们把整个过程串起来:
| 步骤 | 完成了什么 | 下一步需要什么 |
|------|-----------|---------------|
| **1. URL 解析** | 拆解网址,知道要去哪 | 需要把域名转成 IP |
| **2. DNS 解析** | 拿到服务器 IP 地址 | 需要确认服务器在线 |
| **3. TCP 握手** | 建立可靠通信通道 | 需要发送正式请求 |
| **4. HTTP 交换** | 获取网页源代码 | 需要把代码转成画面 |
| **5. 浏览器渲染** | 把代码渲染成像素 | ✅ 用户看到网页! |
---
## 结语:0.5 秒里发生了什么
敲下回车,等上半秒,页面就跳出来了——我们早就习惯了这个速度,甚至觉得慢。
但仔细想想,就在这眨眼的功夫里:
- **第一步**:浏览器把你输入的网址拆开看懂
- **第二步**:跑去问了好多台服务器才要到 IP 地址
- **第三步**:跟大洋彼岸的服务器来回确认了三次"能听见吗"
- **第四步**:把请求打包发过去,再等着收回来
- **第五步**:最后还要把成千上万行代码瞬间组装成你能看到的画面
这些步骤一环扣一环,**前一步的输出是后一步的输入**,中间哪个环节出问题,页面就打不开。而那些路由器、服务器、光缆,就默默在后台 24 小时运转,保证你每次滑动手机时,内容都能准时出现。
下次等网页加载的时候,或许可以想想:这 0.5 秒,其实挺忙的。
@@ -2,98 +2,90 @@
::: tip 🎯 核心问题
**有了完美的 CPU 和无限的内存,电脑就能直接用了吗?**
在上一章,我们见证了晶体管如何组合成强大的 CPU。但其实,如果直接使用这些冷冰冰的硬件,哪怕只是想在屏幕上打出一个字母,你都需要写几百行晦涩的机器指令。
在上一章,我们见证了晶体管如何组合成强大的 CPU。但即使你拥有最顶级的硬件,如果直接让它们工作,连在屏幕上显示一个字母都需要写几百行晦涩的机器指令。不仅麻烦,还极其危险——稍有差池,你的代码就可能把别人的数据覆盖掉。
为了不让大家在每次用电脑时都被逼疯,前辈们创造了一个夹在“硬件”和“你”之间的超级管家——**操作系统(Operating System, 简称 OS)**。本章我们不谈深奥的理论,只聊聊这个大管家是怎么通过三大“障眼法”,把复杂的硬件调教得服服帖帖的。
为了解决这些噩梦,**操作系统(Operating System, 简称 OS)**诞生了。它是挡在你和冰冷硬件之间的一层最伟大的"软件"。本章我们将抛开深奥的代码,用通俗的比喻,看看这个"超级管家"是如何把杂乱无章的硬件调教得服服帖帖的。
:::
---
## 0. 承上启下:如果没有操作系统会怎样?
## 0. 全景图:没有操作系统会怎样?
上一章我们提到,CPU 是一个不知疲倦的无情计算机器,通电后就会一行一行地执行指令
想象一下,你开了一家极具潜力的"计算工厂"(你的电脑),厂里有一个全能、不知疲倦的顶级干将(CPU),还有一片巨大的仓库(内存)和无数的集装箱(硬盘)
但这带来了几个现实的灾难
1. **CPU 独占危机**CPU 一次只能干一件事。如果你正在听歌,想切出去看个网页?抱歉,没有操作系统的调度,你的电脑必须停下音乐,才能去加载网页
2. **内存踩踏事故**:微信和游戏都在使用内存。如果没有保安管理,游戏一不小心把数据到了微信的内存地盘,微信当场崩溃。
3. **硬盘迷宫**:硬盘本质上只是一张密密麻麻刻满 0 和 1 的巨大光盘。要想找到昨天存的照片,你必须准确记住它存放在第 12345 圈磁道678 扇区。
如果你**不雇佣**一个厂长(操作系统)来管理
1. **CPU 独占危机**CPU 一次只能干一件事。如果有人在用它听歌,其他任何人想看网页?抱歉,大家必须排队等听歌的人主动把 CPU 让出来
2. **内存踩踏事故**:微信和游戏都在使用仓库(内存。如果没有保安规划区域,游戏一不小心把装备数据到了微信的盒子里,微信直接当场崩溃。
3. **硬盘迷宫**:硬盘硬件只是一张刻满 0 和 1 的巨大光盘。要想找到昨天存的照片,你必须准确记住它存放在"第 1 盘面、第 56 磁道第 8 扇区",没人能记住这种反人类的坐标
为了解决这些噩梦,操作系统诞生了。它对外提供了一套优雅的“幻觉”,这就是它的三大核心魔法:**进程(管理 CPU)**、**虚拟内存(管理内存)** 和 **文件系统(管理硬盘)**
<OSArchitectureDemo />
为了解决上述的三大噩梦,操作系统祭出了它的三板斧:**进程管理**、**内存管理**和**文件系统**。
---
## 1. 进程管理:制造“同时运行”的幻觉
## 1. 进程管理:CPU 的时分复用
你平时用电脑,常常是一边挂着微信,一边听着音乐,还能一边打字。但如果你买的电脑其实只有一个 CPU 核心,它是怎么同时做这三件事的?
答案是:**它并没有同时做**。是操作系统在进行疯狂的时间管理”。
答案是:**它并没有同时做。是操作系统在进行疯狂的"时间管理"。**
<ProcessDemo />
::: tip 💡 核心原理解析:时间片轮转(Time Slicing
操作系统把 CPU 的时间切成了极其微小的片段(比如 10 毫秒)
- 第 1-10 毫秒:让 CPU 去执行**微信**的接收消息逻辑。
- 第 11-20 毫秒:把微信强制暂停,让 CPU 去执行**音乐**的播放逻辑。
- 第 21-30 毫秒:把音乐暂停,让 CPU 去响应你的**键盘打字**。
### 1.1 什么是"进程"
每一个正在运行的程序,就被称为一个**进程**。你可以把它理解为一个"项目组",有自己的代码(做事清单)、自己的内存数据(项目资金),排着队等待 CPU 接见
因为切换的速度实在太快了(一秒钟切换成百上千次),在人类迟钝的感知中,就觉得这三个软件是“同时”在运行的。
在操作系统的术语里,运行中的程序就被称为**进程(Process)**。操作系统就是这群进程的冷酷无情的排班经理。
:::
### 1.2 时间片轮转
为了不让某个流氓软件一直霸占 CPU,操作系统把 CPU 的时间切成极小的片段(约 10 毫秒),轮流分配给各个进程。因为切换速度太快了,你感觉是"同时运行"。
---
## 2. 内存管理:给每个程序画个“海市蜃楼”
## 2. 内存管理:虚拟地址空间
解决了 CPU 轮流用的问题,接下来是存放数据的内存。如果所有的进程都挤在同一块物理内存里,很容易发生互相干扰和偷看数据的危险
操作系统的第二大魔法,叫作**虚拟内存(Virtual Memory**。
解决了 CPU 轮流用的问题,接下来是内存空间。如果不加管理,所有软件都直接往物理内存条写数据,必然会发生**互相覆盖**的踩踏惨剧
<MemoryDemo />
::: tip 💡 核心原理解析:内存映射
操作系统对每一个启动的进程撒了一个弥天大谎:嘿,你独占了整整 4GB 的纯净内存空间,随便用!”(这就是**虚拟内存**)。
### 2.1 虚拟内存(Virtual Memory
操作系统对每一个进程撒了一个大谎:"嘿,你独占了整台电脑所有的可用内存,随便用!"
但实际上,当进程往这个“虚拟空间”里放东西时,操作系统的底层会拿出一个**映射表(页表)**,偷偷把数据塞进**真实物理内存(Physical Memory)**中各种零碎、不连续的角落里
在进程眼里,自己的内存条永远是**连续**且**干净**的。它心安理得地往里面写数据
**这么做有两个巨大的好处:**
1. **绝对安全**:微信永远只能看到自己的虚拟空间,它根本不知道音乐的数据在物理内存的哪个角落,自然就不会发生“踩踏”。
2. **碎片利用**物理内存就算被用得像狗皮膏药一样稀碎,映射给进程的虚拟空间依然是连续且整齐的。
:::
### 2.2 页表映射(Page Table
实际上呢?操作系统偷偷把数据塞进**真实物理内存**中各种零碎的缝隙里。这么做有两个绝顶天才的好处:
1. **绝对安全**微信永远只能看到自己的空间,没法篡改别人的数据
2. **碎片利用**:不管物理内存多乱,映射给进程的虚拟空间依然是整齐的
---
## 3. 文件系统:把“荒地”变成“档案馆”
## 3. 文件系统:持久化存储的组织
如果你买了一块崭新的硬盘,它里面其实是一片荒芜的存储单元。如果你想存一张照片,硬盘硬件只会问你:请告诉我你要存在第几个字节地址?”这显然反人类。
操作系统的第三大魔法是**文件系统(File System)**,它为你构建了我们最熟悉的:文件夹(目录)和文件的概念。
如果你买了一块崭新的硬盘,它里面其实是一片荒芜的存储单元。如果你想存一张照片,硬盘只会问你:"请告诉我你要存在第几个字节"
<FilesystemDemo />
::: tip 💡 核心原理解析:从地址到路径
文件系统本质上是一个超级大型的“翻译官”加“账本”:
1. **账本功能**它悄悄地把硬盘切分成无数个小块(Block),然后用一个账本记录下来“哪几个小块现在是空的可以存数据,哪几个小块已经存了东西”。
2. **翻译功能**当你双击一层层文件夹,打开 `D盘/照片/宠物.jpg` 时,并不是硬盘真的长出了树枝一样的结构。而是文件系统在它的账本里疯狂翻阅,最终翻译出:哦,这个路径其实对应的是硬盘上的第 1056、1057 和 998 块小地方,然后把数据取出来交给你。
:::
### 3.1 文件系统做了什么?
1. **切割硬盘**:把硬盘切成无数个固定大小的**块**(通常是 4KB)
2. **建立账本**记录哪些块是满的,哪些是空的
3. **翻译路径** `D盘/照片/宠物.jpg` 翻译成"第 3、7、11 块"
这就是为什么你重命名文件瞬间就能完成(只改账本上的名字),而复制文件需要好久(要真实读写硬盘数据块)。
---
## 4. 总结:伟大的幕后英雄
## 4. 三者协同:程序启动的完整过程
我们通过一个你每天都在经历的场景,串联起今天学到的知识。当你**双击鼠标打开一个游戏**时,为了伺候你,大管家做了什么?
我们已经分别了解了操作系统的三大模块,下面看看当你**双击打开一个程序**时,它们是如何协同工作的:
1. **文件系统**:立刻从底层硬盘的杂乱数据块中,拼凑出游戏的执行文件和美术资产。
2. **内存管理**:为你分配一个巨大的虚拟内存空间,制造出“这台电脑只有这一个游戏”的幻觉,并把刚才找到的文件放进物理内存的空隙里。
3. **进程管理**:在它的名册上新建一个“游戏进程”,并在下一个瞬间,立刻剥夺其他正在运行软件的 CPU 权利,把 CPU 的计算力全盘移交给你的游戏。
<ProgramLaunchDemo />
我们之所以能那么轻松、优雅地在数字世界里冲浪,全都是因为底层的操作系统在替我们负重前行。
无论是你点击桌面图标,还是代码中的一句 `print("Hello World")`,都离不开这一套复杂的暗箱操作。我们之所以能那么轻松地在数字世界里冲浪,全都是因为底层的操作系统在替我们负重前行。
---
## 延伸阅读
如果你觉得操作系统的各种管理学十分有趣,你可以看看这些进阶话题:
- **进程与线程的区别**:除了进程,还有一种叫作“线程”的东西,它们是干什么用的?(为什么 Google Chrome 那么吃内存?)
- **页面置换算法**:当物理内存全都塞满了,但你又打开了一个新软件,操作系统该把谁的数据临时踢到硬盘里?(LRU 算法)
- **操作系统的多态**Windows 和 macOS 会在底层实现上有什么不同?为什么有些软件只能在特定系统上运行?
如果你觉得操作系统的各种"管理学和骗术"十分有趣,你可以看看这些进阶话题:
- **进程与线程**:如果进程是项目组,那"线程"就是组里干活的员工
- **并发与锁**:当两个进程同时竞争同一个资源时,如何防止死锁
- **系统调用**:操作系统给上层应用提供的"服务窗口"
@@ -112,23 +112,20 @@
如果刚才介绍的逻辑门只能做简单的条件判断,那计算机到底是如何做数学运算的呢?
我们先回想一下手算加法的方式:对应位相加,如果超出了限制(十进制是满十进一,二进制是满二进一),就向更高位“进位”。
在二进制中,只有 0 和 1。对于一位数的加法,可能的情况只有四种:
- `0 + 0 = 0` (本位是 0,不进位)
- `0 + 1 = 1` (本位是 1,不进位)
- `1 + 0 = 1` (本位是 1,不进位)
- `1 + 1 = 10` (本位是 0,进位 1
<BinaryAdditionRulesDemo />
仔细观察这四种情况,你会发现:
1. **本位的结果**,只有在两个输入**不同**时才为 1,这正是 **XOR 门(异或门)** 的逻辑。
2. **进位的结果**,只有在两个输入**都为 1** 时才为 1,这正是 **AND 门(与门)** 的逻辑。
因此,只要把一个 XOR 门和一个 AND 门组合起来,我们就得到了能计算一位数加法的电路,这也是最基础的**半加器(Half Adder**。
因此,只要把一个 XOR 门(负责算本位)和一个 AND 门(负责算进位)组合起来,我们就得到了能计算一位数加法的电路,这也是最基础的**半加器(Half Adder**。
<HalfAdderDemo />
但半加器有个致命缺陷:它无法处理来自低位的进位。在多位加法中,中间的每一位不仅要加 A 和 B,还要加上低位传来的进位。这就需要**全加器(Full Adder**
但半加器有个致命缺陷:它在物理结构上**只有两个输入端口(A 和 B**
想象我们在做十进制竖式加法(比如 `19 + 22`):
- **算个位**`9 + 2 = 11`。只需两个数相加,写 `1``1`。这刚好是两个输入,半加器能完美胜任。
- **算十位**:不仅要算 `1 + 2`,还要**加上刚才个位传过来的“进位 1”**(即 `1 + 2 + 1 = 4`)。这意味着在多位加法中,除了最低位,其他位实际上是在做**三个数字**的相加!
因为半加器没有接纳“低位传来的进位(Carry-in)”的第三个输入口,所以除了最右边的那一位,它在别的位全都没法用。为了解决这个问题,我们需要能接收三个信号的**全加器(Full Adder**
<FullAdderDemo />
@@ -185,6 +182,10 @@
当我们将 32 个抑或 64 个这种触发器整齐地编排成一列,施加同一种强劲的时钟频率信号(Clock)来号令它们统一行动时,**寄存器(Register)**便应运而生了。它身居 CPU 系统的心脏位置,被当做极速的“工作草稿纸”,默默捍卫着你每一个即时的关键变量。
:::
请通过下面的互动演示,亲自体验这个打破和恢复闭环的过程:
<FlipFlopDemo />
---
## 4. CPU 架构:从功能单元到处理器
@@ -66,6 +66,18 @@ result = response.choices[0].message.content
<ApiTypesComparison />
### 1.3 函数 API vs HTTP API 的区别
很多初学者会困惑:函数 API 和 HTTP API 到底有什么区别?看文档时该如何区分?
<ApiFunctionVsHttp />
### 1.4 不同类型的 API 文档怎么看
面对不同类型的 API 文档,关注重点各不相同:
<DocumentTypesComparison />
---
## 2. 一次完整的 API 调用
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-652
View File
@@ -1,652 +0,0 @@
# SQL:与数据库对话的语言
::: tip 核心问题
**如何高效地查询和操作数据?** 这就像问:图书馆的书怎么快速找到?仓库的货物怎么精准定位?银行的账目怎么安全转账?SQL 解决的就是"与数据对话"的问题。
:::
---
## 0. SQL 的核心价值
在现代软件开发中,数据是核心资产。无论是电商平台的商品信息、社交网络的用户关系,还是银行系统的交易记录,都需要一种高效的方式来管理和查询。
**SQL**Structured Query Language,结构化查询语言)就是这样一种"与数据库对话"的语言。它让我们能够:
- **精准查询**:从百万级数据中快速找到目标
- **高效操作**:批量增删改,一条语句搞定
- **安全保障**:事务机制保证数据一致性
- **标准通用**:学一次,所有数据库都能用
---
## 1. SQL vs NoSQL:如何选择?
在深入了解 SQL 之前,先了解一下它与 NoSQL 的区别。
### 1.1 用仓库来类比
| 特性 | SQL(关系型数据库) | NoSQL(非关系型数据库) |
| :--- | :--- | :--- |
| **数据结构** | 严格的表结构(像 Excel) | 灵活的文档/键值/图结构 |
| **典型代表** | MySQL、PostgreSQL、Oracle | MongoDB、Redis、Elasticsearch |
| **适用场景** | 金融系统、电商订单、用户管理 | 社交动态、日志分析、实时缓存 |
| **优势** | 数据一致性、事务支持(ACID) | 高并发、灵活扩展、高性能 |
| **劣势** | 扩展性差、schema 固定 | 数据一致性弱、查询功能有限 |
### 1.2 一个直观的对比
**SQL 数据库**就像一个**规范化的仓库**
- 每个货架有固定的编号、名称、容量
- 货物必须按照规则摆放
- 入库、出库有严格的流程和记录
- 适合需要严格管理的场景
**NoSQL 数据库**就像一个**灵活的杂物间**
- 想放哪里就放哪里
- 不需要预先规划空间
- 快速存取,但可能找不到东西
- 适合需要快速迭代的场景
::: tip 💡 实际应用
大多数企业会**同时使用 SQL 和 NoSQL**
- MySQL 存储用户信息、订单数据(核心业务)
- Redis 缓存热点数据(提高性能)
- MongoDB 存储日志、用户行为(数据分析)
:::
---
## 2. CRUD 操作:数据的增删改查
SQL 的核心操作就是 CRUDCreate, Read, Update, Delete)。
### 2.1 用 Excel 来类比
| Excel 操作 | SQL 关键字 | 说明 |
| :--- | :--- | :--- |
| 插入新行 | INSERT | 添加数据 |
| 筛选行 | SELECT | 查询数据 |
| 修改单元格 | UPDATE | 更新数据 |
| 删除行 | DELETE | 删除数据 |
### 2.2 实战演示
👇 **动手试试看**:在下方交互式演示中体验 CRUD 操作:
<SqlDemo />
### 2.3 常用查询语法
#### **SELECT:查询数据**
```sql
-- 查询所有列
SELECT * FROM users;
-- 查询指定列
SELECT name, email FROM users;
-- 带条件查询
SELECT * FROM users WHERE age > 18;
-- 排序
SELECT * FROM users ORDER BY age DESC;
-- 限制结果数量
SELECT * FROM users LIMIT 10;
```
#### **INSERT:插入数据**
```sql
-- 插入完整数据
INSERT INTO users (name, email, age)
VALUES ('张三', 'zhangsan@example.com', 25);
-- 批量插入
INSERT INTO users (name, email, age) VALUES
('李四', 'lisi@example.com', 30),
('王五', 'wangwu@example.com', 28);
```
#### **UPDATE:更新数据**
```sql
-- 更新单个字段
UPDATE users SET age = 26 WHERE id = 1;
-- 更新多个字段
UPDATE users
SET age = 27, email = 'newemail@example.com'
WHERE id = 1;
-- ⚠️ 危险操作:不带 WHERE 会更新所有行!
UPDATE users SET age = 0; -- 慎用!
```
#### **DELETE:删除数据**
```sql
-- 删除指定行
DELETE FROM users WHERE id = 1;
-- ⚠️ 危险操作:不带 WHERE 会删除所有数据!
DELETE FROM users; -- 慎用!
```
::: warning 💡 最佳实践
- 先用 `SELECT` 验证 WHERE 条件是否正确
- 再用 `UPDATE/DELETE` 执行操作
- 生产环境务必加 `LIMIT` 限制影响行数
:::
---
## 3. SELECT 进阶:JOIN、GROUP BY、子查询
当数据分布在多个表中时,我们需要更强大的查询能力。
### 3.1 JOIN:连接多个表
**场景**:一个电商系统有两个表:
- `users`(用户表):id, name, email
- `orders`(订单表):order_id, user_id, amount
如何查询"每个用户的订单总金额"?
#### **INNER JOIN:只返回匹配的行**
```sql
SELECT users.name, SUM(orders.amount) as total
FROM users
INNER JOIN orders ON users.id = orders.user_id
GROUP BY users.id;
```
**结果**:只显示有订单的用户
#### **LEFT JOIN:返回左表所有行**
```sql
SELECT users.name, SUM(orders.amount) as total
FROM users
LEFT JOIN orders ON users.id = orders.user_id
GROUP BY users.id;
```
**结果**:显示所有用户,没有订单的用户 total 为 NULL
::: tip 💡 如何选择 JOIN
- **INNER JOIN**:只要两边都有数据才需要(如:订单明细)
- **LEFT JOIN**:需要保留主表所有数据(如:用户列表 + 统计信息)
- **RIGHT JOIN**:需要保留从表所有数据(很少用)
- **FULL OUTER JOIN**:需要所有数据(MySQL 不支持,可用 UNION 实现)
:::
### 3.2 GROUP BY:分组统计
**场景**:统计每个部门的平均工资。
```sql
SELECT department, AVG(salary) as avg_salary, COUNT(*) as count
FROM employees
GROUP BY department
HAVING AVG(salary) > 10000; -- HAVING 过滤分组后的结果
```
**注意**
- `WHERE` 过滤行(在 GROUP BY 之前)
- `HAVING` 过滤分组(在 GROUP BY 之后)
### 3.3 子查询:查询嵌套查询
**场景**:查找工资高于平均工资的员工。
```sql
-- 方式一:WHERE 子查询
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
-- 方式二:FROM 子查询(派生表)
SELECT dept_name, avg_salary
FROM (
SELECT department, AVG(salary) as avg_salary
FROM employees
GROUP BY department
) as dept_avg
WHERE avg_salary > 10000;
```
::: tip 💡 子查询 vs JOIN
- **子查询**:逻辑清晰,但性能较差(每个子查询都会执行一次)
- **JOIN**:性能更好,但需要理解连接逻辑
- **最佳实践**:优先使用 JOIN,必要时用子查询
:::
---
## 4. 索引原理:让查询快起来
### 4.1 为什么需要索引?
**场景**:在一个 100 万行的用户表中,查找 `id = 123456` 的用户。
**没有索引**
- 数据库需要逐行扫描,最多比较 100 万次
- 时间复杂度:O(n)
**有索引**
- 数据库通过 B+ 树快速定位,只需比较 log₂(100万) ≈ 20 次
- 时间复杂度:O(log n)
### 4.2 用图书馆来类比
| 概念 | 图书馆 | 数据库 |
| :--- | :--- | :--- |
| **数据** | 书籍 | 表的行 |
| **索引** | 目录卡片 | B+ 树 |
| **查询** | 按书名找书 | 按 WHERE 条件找行 |
| **无索引** | 逐排书架找 | 全表扫描 |
| **有索引** | 查目录定位 | 索引查找 |
### 4.3 索引的可视化演示
👇 **动手试试看**:在 SqlDemo 组件的"索引"标签页查看无索引 vs 有索引的对比:
<SqlDemo />
### 4.4 索引的使用建议
| 场景 | 是否建索引 | 说明 |
| :--- | :--- | :--- |
| **WHERE 条件** | 是 | 如 `WHERE user_id = 1` |
| **JOIN 连接** | 是 | 如 `JOIN ON user_id` |
| **ORDER BY 排序** | 是 | 如 `ORDER BY created_at` |
| **低选择性列** | 否 | 如性别(只有男/女) |
| **频繁更新的列** | 谨慎 | 索引会降低写入性能 |
| **小表** | 否 | 数据量小不需要索引 |
**创建索引**
```sql
-- 单列索引
CREATE INDEX idx_user_id ON orders(user_id);
-- 复合索引(最左前缀原则)
CREATE INDEX idx_user_status ON orders(user_id, status);
-- 唯一索引
CREATE UNIQUE INDEX idx_email ON users(email);
```
::: tip 💡 索引的代价
- **空间**:每个索引都是额外的存储空间
- **时间**INSERT/UPDATE/DELETE 需要更新索引,降低写入速度
- **建议**:只在查询频繁、更新少的列上建索引
:::
---
## 5. 事务 ACID:保证数据一致性
### 5.1 什么是事务?
**事务**Transaction)是一组 SQL 操作,要么全部成功,要么全部失败。
**经典案例**:银行转账
```sql
BEGIN; -- 开始事务
-- 账户 A 扣款 100 元
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
-- 账户 B 加款 100 元
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
COMMIT; -- 提交事务(如果中间出错,自动 ROLLBACK)
```
如果第二步失败(比如账户 B 不存在),整个事务会回滚,账户 A 不会被扣款。
### 5.2 ACID 四大特性
👇 **动手试试看**:在 SqlDemo 组件的"事务"标签页查看 ACID 可视化:
<SqlDemo />
#### **A - Atomicity(原子性)**
- **含义**:事务中的操作要么全部成功,要么全部失败
- **类比**:转账要么同时成功,要么同时失败,不会出现"扣款了但没到账"的情况
- **实现**Undo Log(回滚日志)
#### **C - Consistency(一致性)**
- **含义**:事务前后数据库状态一致,满足所有约束
- **类比**:转账前后总金额不变(A 余额 + B 余额 = 总金额)
- **实现**:应用层约束 + 数据库约束
#### **I - Isolation(隔离性)**
- **含义**:并发事务之间互不干扰
- **类比**:两个用户同时转账,不会相互影响
- **实现**:锁机制 + MVCC(多版本并发控制)
#### **D - Durability(持久性)**
- **含义**:事务提交后,永久保存,即使系统故障
- **类比**:转账成功后,断电也不会丢失记录
- **实现**Redo Log(重做日志)
### 5.3 事务隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 | 适用场景 |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **READ UNCOMMITTED** | 是 | 是 | 是 | 高 | 几乎不用 |
| **READ COMMITTED** | 否 | 是 | 是 | 中 | 大多数数据库默认 |
| **REPEATABLE READ** | 否 | 否 | 是 | 低 | MySQL 默认 |
| **SERIALIZABLE** | 否 | 否 | 否 | 最低 | 金融级要求 |
**设置隔离级别**
```sql
-- 查看
SELECT @@transaction_isolation;
-- 设置
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
```
::: tip 💡 如何选择隔离级别?
- **默认使用 READ COMMITTED**:避免脏读,性能可接受
- **金融场景**:使用 SERIALIZABLE 或 REPEATABLE READ
- **分析场景**:可降低到 READ UNCOMMITTED 提高性能
:::
---
## 6. SQL 注入:安全的警惕性
### 6.1 什么是 SQL 注入?
**SQL 注入**是一种常见的安全漏洞,攻击者通过构造恶意的输入,篡改 SQL 语句。
**示例**:一个登录接口
```sql
-- 正常 SQL
SELECT * FROM users WHERE username = 'admin' AND password = '123456';
-- 攻击者输入用户名:admin' --
-- 拼接后的 SQL
SELECT * FROM users WHERE username = 'admin' --' AND password = '123456';
-- ↑ 注释掉后面的密码验证,直接登录成功!
```
**更危险的攻击**
```sql
-- 用户名输入:admin'; DROP TABLE users; --
-- 拼接后的 SQL
SELECT * FROM users WHERE username = 'admin'; DROP TABLE users; --'
```
### 6.2 如何防御?
#### **方法一:参数化查询(推荐)**
```python
# ❌ 错误:直接拼接字符串(危险!)
sql = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(sql)
# ✅ 正确:使用参数化查询(安全)
sql = "SELECT * FROM users WHERE username = %s"
cursor.execute(sql, (username,))
```
#### **方法二:ORM 框架**
```python
# Django ORM
user = User.objects.get(username=username)
# SQLAlchemy
user = session.query(User).filter(User.username == username).first()
```
#### **方法三:输入验证**
```python
# 限制用户名只能包含字母、数字、下划线
import re
if not re.match(r'^\w+$', username):
raise ValueError('Invalid username')
```
::: warning 💡 防御 SQL 注入的黄金法则
1. **永远不要相信用户输入**
2. **永远使用参数化查询或 ORM**
3. **永远不要拼接 SQL 字符串**
4. **最小权限原则**:数据库用户只给必要权限
:::
---
## 7. 最佳实践
### 7.1 查询优化
| 优化技巧 | 说明 | 示例 |
| :--- | :--- | :--- |
| **避免 SELECT \*** | 只查询需要的列 | `SELECT name, email FROM users` |
| **使用 LIMIT** | 限制结果数量 | `SELECT * FROM users LIMIT 10` |
| **索引覆盖** | 查询条件使用索引列 | `WHERE indexed_col = 1` |
| **避免子查询** | 用 JOIN 替代子查询 | 见上文对比 |
| **批量操作** | 减少数据库往返 | `INSERT INTO ... VALUES (...), (...), (...)` |
| **分页查询** | 大数据量分页 | `SELECT * FROM users LIMIT 10 OFFSET 20` |
### 7.2 命名规范
| 类型 | 规范 | 示例 |
| :--- | :--- | :--- |
| **表名** | 小写 + 下划线 | `user_profiles`, `order_items` |
| **列名** | 小写 + 下划线 | `created_at`, `user_id` |
| **索引名** | `idx_表名_列名` | `idx_users_email` |
| **外键名** | `fk_表名_列名` | `fk_orders_user_id` |
| **主键名** | 统一使用 `id` | 无 |
### 7.3 数据库设计
| 设计原则 | 说明 | 示例 |
| :--- | :--- | :--- |
| **规范化** | 消除数据冗余 | 第三范式(3NF) |
| **反规范化** | 适当冗余提高性能 | 在订单表冗余用户姓名 |
| **主键选择** | 优先使用自增 ID | `id BIGINT AUTO_INCREMENT` |
| **时间字段** | 统一使用 DATETIME | `created_at DATETIME` |
| **软删除** | 用 `is_deleted` 标记 | 不真删除,便于恢复 |
---
## 8. 用 AI 辅助编写 SQL
AI 可以帮助你快速编写复杂的 SQL 查询。关键在于提供清晰的表结构和业务需求。
### 8.1 提示词模板
```
你是一位资深的数据库工程师,精通 SQL 查询优化。请帮我编写 SQL 查询。
## 数据库表结构
[提供表的 CREATE TABLE 语句或字段说明]
## 业务需求
[描述你想要查询的数据,例如:
- 统计每个月的订单总金额
- 查找购买过商品 A 和商品 B 的用户
- 计算用户的留存率]
## 要求
1. 使用标准 SQL 语法(兼容 MySQL 8.0
2. 注释关键逻辑
3. 考虑性能优化(索引、JOIN 顺序)
4. 提供多种实现方案(如子查询 vs JOIN)
## 输出格式
### SQL 查询
\`\`\`sql
[SQL 语句]
\`\`\`
### 逻辑说明
[解释查询的逻辑和优化思路]
```
### 8.2 实战示例:复杂统计查询
**输入提示词**
```
你是一位资深的数据库工程师,精通 SQL 查询优化。请帮我编写 SQL 查询。
## 数据库表结构
```sql
-- 用户表
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
created_at DATETIME
);
-- 订单表
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT,
amount DECIMAL(10, 2),
status ENUM('pending', 'paid', 'shipped', 'completed', 'cancelled'),
created_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 订单详情表
CREATE TABLE order_items (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT,
product_id BIGINT,
quantity INT,
price DECIMAL(10, 2),
FOREIGN KEY (order_id) REFERENCES orders(id)
);
-- 商品表
CREATE TABLE products (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
category VARCHAR(50)
);
```
## 业务需求
1. 统计每个月的订单总金额和订单数量
2. 查找购买过商品 A 和商品 B 的用户
3. 计算用户复购率(有多少用户下了多个订单)
## 要求
1. 使用标准 SQL 语法(兼容 MySQL 8.0
2. 注释关键逻辑
3. 考虑性能优化
```
**AI 输出示例**
```sql
-- 需求 1:统计每个月的订单总金额和订单数量
SELECT
DATE_FORMAT(created_at, '%Y-%m') AS month,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
WHERE status IN ('paid', 'shipped', 'completed') -- 只统计有效订单
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
ORDER BY month DESC;
-- 需求 2:查找购买过商品 A 和商品 B 的用户
-- 方案一:INTERSECTMySQL 不支持,用 IN 替代)
SELECT DISTINCT user_id
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE p.name IN ('商品 A', '商品 B')
GROUP BY user_id
HAVING COUNT(DISTINCT p.name) = 2; -- 确保同时买了两个商品
-- 方案二:自连接(性能更好)
SELECT
oa.user_id
FROM orders oa
JOIN order_items oia ON oa.id = oia.order_id
JOIN products pa ON oia.product_id = pa.id
JOIN order_items oib ON oa.id = oib.order_id -- 同一个订单
JOIN products pb ON oib.product_id = pb.id
WHERE pa.name = '商品 A' AND pb.name = '商品 B';
-- 需求 3:计算用户复购率
WITH user_order_count AS (
SELECT
user_id,
COUNT(*) AS order_count
FROM orders
WHERE status IN ('paid', 'shipped', 'completed')
GROUP BY user_id
)
SELECT
COUNT(CASE WHEN order_count >= 2 THEN 1 END) AS repeat_users,
COUNT(*) AS total_users,
ROUND(COUNT(CASE WHEN order_count >= 2 THEN 1 END) * 100.0 / COUNT(*), 2) AS repeat_rate
FROM user_order_count;
```
### 8.3 AI 辅助的注意事项
| 注意点 | 说明 |
| :--- | :--- |
| **提供完整上下文** | 表结构、索引、数据量都要说清楚 |
| **明确性能要求** | 是否需要优化、数据量大小 |
| **验证 SQL 语法** | AI 生成的 SQL 可能有小错误,需要测试 |
| **理解执行计划** | 用 `EXPLAIN` 查看查询是否使用了索引 |
| **分步实现** | 复杂查询可以拆分成多个简单查询 |
::: tip 💡 追问技巧
- "请提供另一种实现方案(如用 JOIN 替代子查询)"
- "请分析这条查询的性能瓶颈"
- "请添加索引建议"
- "请解释每个步骤的逻辑"
:::
---
## 名词速查表
| 名词 | 英文 | 解释 |
| :--- | :--- | :--- |
| **SQL** | Structured Query Language | 结构化查询语言,与数据库对话的标准语言 |
| **数据库** | Database | 存储和管理数据的仓库 |
| **表** | Table | 数据的二维表格,类似 Excel |
| **行** | Row | 表中的一条记录 |
| **列** | Column | 表中的一个字段 |
| **主键** | Primary Key | 唯一标识一行的字段(如 id) |
| **外键** | Foreign Key | 关联其他表的字段 |
| **索引** | Index | 加速查询的数据结构(B+ 树) |
| **事务** | Transaction | 一组要么全成功、要么全失败的 SQL 操作 |
| **ACID** | Atomicity, Consistency, Isolation, Durability | 事务的四大特性 |
| **JOIN** | Join | 连接多个表的查询操作 |
| **子查询** | Subquery | 嵌套在另一个查询中的查询 |
| **聚合函数** | Aggregate Function | SUM, AVG, COUNT, MAX, MIN |
| **分组** | Group By | 按字段分组统计 |
| **SQL 注入** | SQL Injection | 通过输入篡改 SQL 语句的攻击方式 |
| **规范化** | Normalization | 消除数据冗余的设计原则 |
| **反规范化** | Denormalization | 适当冗余提高性能的设计 |
| **执行计划** | Execution Plan | 数据库执行 SQL 的详细步骤 |
| **B+ 树** | B+ Tree | 索引的底层数据结构 |
| **MVCC** | Multi-Version Concurrency Control | 多版本并发控制,实现事务隔离 |
| **脏读** | Dirty Read | 读取未提交的数据 |
| **不可重复读** | Non-Repeatable Read | 同一事务两次读取结果不同 |
| **幻读** | Phantom Read | 同一事务两次读取结果集不同 |
| **隔离级别** | Isolation Level | 事务隔离的程度(READ UNCOMMITTED/READ COMMITTED/REPEATABLE READ/SERIALIZABLE |