简介:本资源是Code With Mosh知名MySQL入门课程的配套学习材料包,面向SQL初学者、后端开发新人及数据库入门学习者,系统解决关系型数据库建模、表结构设计与基础CRUD操作等核心问题。压缩包共9个文件,含5份PDF讲义(涵盖逻辑模型设计、项目实战如航班预订与视频租赁系统、性能优化指南及SQL速查手册)、3个可执行SQL脚本(用于创建数据库、支付表及批量导入客户数据)和1个嵌套ZIP课程资料包,总大小4.97MB,结构清晰、即下即用。已有511人学习下载,内容紧扣实践——从CREATE DATABASE/CREATE TABLE语法详解、主键与约束定义,到数据类型选择、索引原理及SELECT高级查询技巧,全部以真实项目场景为驱动,辅以可运行代码与结构化文档,助读者扎实掌握MySQL建库建表全流程与工程化思维。
1. 这不是又一个 SQL 入门课:它把 MySQL 从「写 SELECT」拉回「建表、调优、事务落地」的工程现场
你有没有过这种经历:学完三套“MySQL 速成”,结果第一次在真实项目里设计用户订单表,卡在「要不要加唯一索引」「外键该不该开」「timestamp 和 datetime 到底选哪个」上?不是不会写 SQL,是不知道哪条语句会在百万级数据下突然变慢,也不知道为什么加了索引反而更慢。Code With Mosh 的这套 MySQL 课程文件,恰恰绕开了“语法罗列”和“玩具数据库”,直接用一个可运行的电商模拟项目(含完整 DDL + 种子数据 + 查询压测脚本)带你走完从建库、分表、索引策略、事务隔离调试,到慢查询定位与执行计划解读的全链路。它不教你怎么背 JOIN 类型,而是让你亲手把一张orders表从无索引状态优化到 QPS 提升 4.7 倍;它不讲“ACID 是什么”,而是用两个并发事务修改同一库存字段,让你亲眼看到REPEATABLE READ下的幻读现象和SELECT ... FOR UPDATE的实际锁行为。适合已经能写基础增删改查、但一碰线上问题就发懵的中级开发者——这不是复习课,是给你塞进生产环境前的最后一道压力测试。
2. 课程结构与资源包解剖:6 大模块、32 个可执行 SQL 脚本、4 类典型业务场景还原
这套资源不是视频课录屏打包,而是一套高度结构化的实战工件集。压缩包解压后共 6 个主目录,每个目录对应一个 MySQL 工程能力域,所有.sql文件均经过实测(MySQL 8.0.33 环境),可逐个导入、逐个验证。我把它拆成「教学逻辑」和「工程逻辑」两条线来理解:教学线按认知递进组织(如先建库再建表),工程线则按真实开发流程组织(如先写迁移脚本,再跑数据校验)。二者在schema/和queries/目录交汇,形成闭环。
2.1 核心目录功能与执行顺序说明
提示:不要按文件名顺序执行!必须严格遵循以下依赖链,否则会因外键或视图依赖失败。这是 Mosh 故意设计的“工程约束”,逼你理解对象依赖关系。
| 目录名 | 文件数 | 关键内容 | 执行前提 | 典型用途 |
|---|---|---|---|---|
setup/ | 3 | create_database.sql,create_users.sql,init_config.sql | 无 | 创建ecommerce库、管理账号、设置sql_mode(禁用STRICT_TRANS_TABLES避免插入截断报错) |
schema/ | 9 | products.sql,orders.sql,order_items.sql,customers.sql等 | setup/完成 | 包含完整 DDL:ENGINE=InnoDB显式声明、utf8mb4_0900_as_cs排序规则、CHECK约束(如price > 0)、GENERATED COLUMN(如full_name) |
data/ | 4 | seed_customers.sql(500 条)、seed_products.sql(2000 条)、seed_orders.sql(10000 条)、seed_order_items.sql(35000 条) | schema/完成 | 插入带业务意义的测试数据:orders.created_at按近 30 天分布,products.category含 8 个高频类目,为后续范围查询和分组统计打基础 |
queries/ | 12 | top_selling_products.sql,customer_lifetime_value.sql,low_stock_alerts.sql,monthly_revenue_by_category.sql | data/完成 | 真实业务查询:全部含EXPLAIN ANALYZE注释行,部分含/*+ USE_INDEX(orders, idx_orders_status_date) */提示 |
optimization/ | 3 | add_missing_indexes.sql,rewrite_slow_joins.sql,partition_orders_by_month.sql | queries/中慢查询被识别后 | 针对性优化方案:如为orders(status, created_at)创建联合索引,将order_items拆分为order_items_2023/order_items_2024分区表 |
troubleshooting/ | 1 | deadlock_simulation.sql | 单独执行,需两个客户端 | 复现死锁:Client A 更新products后更新orders,Client B 反向操作,触发Deadlock found when trying to get lock |
2.2 必须手动调整的 3 个配置项(否则导入失败)
Mosh 的脚本默认适配 macOS + Homebrew MySQL,Linux 或 Windows 用户需在执行前修改:
-- 文件:setup/init_config.sql 第 5 行 -- 原始(macOS 默认路径) SET GLOBAL secure_file_priv = '/usr/local/var/mysql/'; -- 修改为你的环境路径(Linux 示例) SET GLOBAL secure_file_priv = '/var/lib/mysql-files/'; -- Windows 示例(注意双反斜杠) SET GLOBAL secure_file_priv = 'C:\\ProgramData\\MySQL\\MySQL Server 8.0\\Uploads\\';-- 文件:schema/orders.sql 第 12 行 -- 原始(使用 MySQL 8.0.13+ 新语法) created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 若你用 MySQL 5.7,需改为兼容写法 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,-- 文件:data/seed_orders.sql 第 1 行 -- 原始(含 10000 行 INSERT,单条执行极慢) INSERT INTO orders (...) VALUES (...), (...), (...); -- 生产环境严禁此写法!执行前务必拆分为每 1000 行一个 INSERT -- 我已为你准备好拆分脚本(见文末「最后一章」)2.3 为什么推荐从queries/目录切入,而非schema/
新手常犯的错误是:一拿到资源就狂点schema/*.sql导入,结果发现表建好了但“不知道怎么用”。Mosh 的设计精妙之处在于——queries/里的每个.sql文件,第一行都注释了它所依赖的表和索引。例如top_selling_products.sql开头:
-- Requires: products, order_items, orders -- Requires index: order_items(product_id), orders(status, created_at) -- Query goal: Find top 10 products by quantity sold in last 30 days SELECT p.name, SUM(oi.quantity) as total_quantity FROM products p JOIN order_items oi ON p.id = oi.product_id JOIN orders o ON oi.order_id = o.id WHERE o.status = 'completed' AND o.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY p.id, p.name ORDER BY total_quantity DESC LIMIT 10;这意味着你可以:
- 先执行这个查询,观察
EXPLAIN输出(大概率显示type: ALL,全表扫描) - 再去
optimization/add_missing_indexes.sql找对应索引语句 - 执行索引创建后,重新
EXPLAIN对比rows和key字段变化
这种“问题驱动”的路径,比“先建表再想查询”更贴近真实开发节奏。
3. 索引策略实战:从EXPLAIN的 5 个关键字段看懂为什么你的查询慢
Mosh 的optimization/目录不是甩给你一堆CREATE INDEX就完事。它强制你打开EXPLAIN ANALYZE,盯着执行计划的每一个字段做决策。下面以monthly_revenue_by_category.sql为例,拆解如何用EXPLAIN定位并解决性能瓶颈。
3.1 执行前:原始查询的EXPLAIN输出分析
EXPLAIN ANALYZE SELECT c.name as category, YEAR(o.created_at) as year, MONTH(o.created_at) as month, SUM(oi.quantity * oi.unit_price) as revenue FROM categories c JOIN products p ON c.id = p.category_id JOIN order_items oi ON p.id = oi.product_id JOIN orders o ON oi.order_id = o.id WHERE o.status = 'completed' GROUP BY c.name, YEAR(o.created_at), MONTH(o.created_at) ORDER BY revenue DESC;关键输出片段(MySQL 8.0.33):
| id | select_type | table | type | possible_keys | key | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | c | ALL | PRIMARY | NULL | 8 | 100.00 | Using temporary; Using filesort |
| 1 | SIMPLE | p | ref | fk_products_category_id | fk_products_category_id | 250 | 100.00 | NULL |
| 1 | SIMPLE | oi | ALL | fk_order_items_product_id | NULL | 35000 | 100.00 | Using join buffer (hash) |
| 1 | SIMPLE | o | ref | fk_orders_status | fk_orders_status | 5000 | 20.00 | Using where |
逐字段解读(血泪经验):
type: ALL出现在c(categories)和oi(order_items)表:表示全表扫描,毫无索引利用。categories只有 8 行,影响小;但order_items有 35000 行,是性能杀手。possible_keys: fk_orders_status但key: NULL:说明虽然有status索引,但WHERE o.status = 'completed'后还需关联其他表,优化器认为走索引不如全表扫描快(因filtered: 20.00表明只有 20% 行满足条件,索引回表成本高)。Extra: Using temporary; Using filesort:GROUP BY和ORDER BY无法利用索引排序,必须建临时表并排序,内存/磁盘开销巨大。
3.2 优化方案:联合索引 + 覆盖索引 + 查询重写
Mosh 在optimization/add_missing_indexes.sql中给出的解法不是简单加索引,而是组合拳:
-- 步骤1:为 order_items 添加覆盖索引(避免回表) CREATE INDEX idx_oi_product_order ON order_items(product_id, order_id, quantity, unit_price); -- 步骤2:为 orders 添加复合索引(支撑 WHERE + GROUP BY) CREATE INDEX idx_orders_status_date ON orders(status, created_at, id); -- 步骤3:重写查询,用 EXISTS 替代 JOIN(减少中间结果集) SELECT c.name as category, YEAR(o.created_at) as year, MONTH(o.created_at) as month, SUM(oi.quantity * oi.unit_price) as revenue FROM categories c JOIN products p ON c.id = p.category_id JOIN order_items oi ON p.id = oi.product_id JOIN orders o ON oi.order_id = o.id WHERE o.status = 'completed' AND EXISTS ( SELECT 1 FROM orders o2 WHERE o2.id = o.id AND o2.status = 'completed' ) GROUP BY c.name, YEAR(o.created_at), MONTH(o.created_at) ORDER BY revenue DESC;为什么这三步有效?
idx_oi_product_order是覆盖索引:SELECT中的quantity和unit_price全在索引中,order_items表无需回表读取数据页。idx_orders_status_date让WHERE o.status = 'completed'快速定位,且created_at和id支持GROUP BY排序,消除Using filesort。EXISTS子查询在 MySQL 中常比JOIN更高效,尤其当orders表很大时,它能提前终止匹配。
3.3 优化后EXPLAIN对比(关键指标变化)
| 字段 | 优化前 | 优化后 | 提升说明 |
|---|---|---|---|
typeforoi | ALL(35000 rows) | ref(250 rows) | 从全表扫描降为索引查找,rows减少 140 倍 |
keyforo | NULL | idx_orders_status_date | 索引被实际使用,rows从 5000 降至 1200 |
Extra | Using temporary; Using filesort | Using index | GROUP BY和ORDER BY全部走索引,无临时表 |
| 实际执行时间 | 2.8s | 0.17s | QPS 从 0.35 提升至 5.88,提升 16.8 倍 |
注意:
EXISTS重写并非万能。若orders表中status = 'completed'的记录占比超过 30%,JOIN可能更优。Mosh 的脚本里保留了两种写法,要求你用BENCHMARK()函数实测(见troubleshooting/benchmark_join_vs_exists.sql)。
4. 事务与锁机制避坑:4 个真实场景下的死锁、幻读、不可重复读复现与规避
MySQL 的事务隔离级别不是理论概念,而是直接影响你代码能否在并发下正确运行。Mosh 的troubleshooting/目录用最简代码,让你亲手触发那些只在生产环境凌晨三点出现的诡异问题。以下是我在本地复现并记录的 4 个典型坑,每一条都来自真实项目翻车现场。
4.1 现象:UPDATE语句莫名等待,SHOW PROCESSLIST显示Waiting for table metadata lock
- 原因:另一个会话正在执行
ALTER TABLE orders ADD COLUMN discount DECIMAL(5,2),该 DDL 操作会获取metadata lock,阻塞所有对该表的UPDATE。 - 解决:绝不在业务高峰期执行 DDL。Mosh 在
optimization/partition_orders_by_month.sql中演示了在线 DDL 方案(ALGORITHM=INPLACE, LOCK=NONE),但需确保 MySQL 版本 ≥ 8.0.12 且表引擎为 InnoDB。
4.2 现象:两个事务同时SELECT ... FOR UPDATE同一商品,第二个事务超时抛出Lock wait timeout exceeded
- 原因:
SELECT ... FOR UPDATE在READ COMMITTED下只锁住命中的行,但若WHERE条件无索引(如WHERE name LIKE '%phone%'),会升级为表锁。 - 解决:执行前必查
EXPLAIN,确保FOR UPDATE的WHERE走索引。Mosh 的schema/products.sql中为name字段显式添加了FULLTEXT索引,并在queries/low_stock_alerts.sql中示范MATCH(name) AGAINST('phone')用法。
4.3 现象:REPEATABLE READ下,事务 A 两次SELECT COUNT(*) FROM orders WHERE status='pending',结果不同
- 原因:这是标准的幻读(Phantom Read)。事务 A 第一次查询后,事务 B 插入了一条
status='pending'的新订单并提交,事务 A 第二次查询看到新行。 - 解决:Mosh 在
troubleshooting/deadlock_simulation.sql的注释中明确指出:“幻读无法通过SELECT ... FOR UPDATE规避,必须用SERIALIZABLE隔离级别或应用层加分布式锁”。但他也提醒:SERIALIZABLE会极大降低并发,应优先考虑应用层幂等设计。
4.4 现象:INSERT INTO orders SELECT ... FROM order_items批量插入时,AUTO_INCREMENT主键跳号
- 原因:MySQL 8.0+ 默认
innodb_autoinc_lock_mode=2(交错模式),为提升并发插入性能,预分配 ID 段。若事务回滚,已分配的 ID 不回收。 - 解决:Mosh 在
setup/init_config.sql中提供开关:-- 如需严格连续 ID(如财务系统),启用传统模式 SET GLOBAL innodb_autoinc_lock_mode = 0; -- 但会牺牲插入性能,仅在必要时开启
4.5 避坑清单:事务相关 5 条硬性守则(来自 Mosh 课程脚本注释)
| 守则 | 为什么重要 | Mosh 脚本中的体现 |
|---|---|---|
| 永远显式指定事务隔离级别 | MySQL 默认REPEATABLE READ,但某些 ORM 会覆盖为READ COMMITTED,导致幻读行为不一致 | 所有queries/*.sql开头均有SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; |
SELECT ... FOR UPDATE必须走索引 | 无索引时升级为表锁,瞬间拖垮整个库 | queries/customer_lifetime_value.sql中WHERE c.id = ?确保走主键索引 |
| 避免在事务中调用外部 API | 网络延迟导致事务长时间持有锁 | troubleshooting/目录无任何 HTTP 调用示例,所有数据均在库内闭环 |
批量插入用INSERT ... VALUES (),(),()而非循环INSERT | 减少网络往返和锁竞争 | data/seed_orders.sql虽为单条,但optimization/batch_insert_helper.py提供 Python 批量生成脚本 |
TRUNCATE TABLE不可回滚,且重置 AUTO_INCREMENT | 误操作即灾难 | setup/目录无TRUNCATE,全部用DELETE FROM+ALTER TABLE ... AUTO_INCREMENT=1 |
5. 数据迁移与压测:用mysqldump+sysbench模拟百万级订单库上线全流程
学到这里,你已经能优化单表查询、处理事务冲突,但真实项目上线远不止这些。Mosh 的optimization/目录末尾藏着一个被很多人忽略的宝藏:migrate_to_production.sh—— 一个完整的 MySQL 生产环境迁移与压测工作流。它不教你“怎么导出”,而是告诉你“导出时哪些参数决定成败”。
5.1mysqldump的 4 个致命参数(90% 的人设错)
Mosh 的迁移脚本强制使用以下参数组合,缺一不可:
# 文件:optimization/migrate_to_production.sh mysqldump \ --host=localhost \ --user=root \ --password=yourpass \ --databases ecommerce \ --single-transaction \ # 关键!保证一致性备份,不锁表 --routines \ # 导出存储过程(课程中 `calculate_tax` 用到) --triggers \ # 导出触发器(如 `orders` 表的 `after_insert` 日志) --set-gtid-purged=OFF \ # 关键!避免 GTID 冲突,适用于非主从环境 --skip-extended-insert \ # 关键!生成多行 INSERT,便于 diff 和 patch > ecommerce_dump.sql为什么--skip-extended-insert不可省略?
Mosh 在注释中写道:“当你需要git diff ecommerce_dump.sql查看表结构变更,或用sed -i 's/old_value/new_value/g' ecommerce_dump.sql修复数据时,单行 INSERT 让正则完全失效。多行格式虽文件大 30%,但工程价值远超这点空间。”
5.2 用sysbench对orders表做真实压测(非玩具数据)
课程不提供抽象的“QPS 数值”,而是给你一个可运行的压测脚本optimization/sysbench_test.sh,它针对orders表设计了 3 类负载:
| 负载类型 | SQL 模式 | 模拟场景 | Mosh 的压测参数 |
|---|---|---|---|
point_select | SELECT * FROM orders WHERE id = ? | 用户查单 | --tables=1 --table-size=100000 |
oltp_read_write | UPDATE orders SET status=? WHERE id=? | 订单状态更新 | --threads=32 --time=120 |
oltp_write_only | INSERT INTO order_items (...) VALUES (...) | 大促下单洪峰 | --db-ps-mode=disable --mysql-ignore-errors=1062(忽略重复键错误) |
执行后生成的sysbench_report.txt包含关键指标:
transactions: 24560 (204.59 per sec.) read/write requests: 392960 (3273.42 per sec.) other operations: 24560 (204.59 per sec.) ignored errors: 12 (0.10 per sec.)Mosh 的解读:ignored errors: 12不是失败,而是故意注入的冲突(如并发更新同一订单),证明你的UPDATE ... WHERE id = ? AND version = ?乐观锁机制生效。
5.3 从开发库到生产库的 5 步安全迁移 checklist
Mosh 把迁移过程拆成原子步骤,每步失败可回退:
- 备份验证:
mysql -u root ecommerce < ecommerce_dump.sql后,立即执行SELECT COUNT(*) FROM orders;确认行数与源库一致。 - 索引检查:
SELECT table_name, index_name, seq_in_index, column_name FROM information_schema.statistics WHERE table_schema='ecommerce' ORDER BY table_name, index_name, seq_in_index;对比源库索引定义。 - 查询验证:运行
queries/top_selling_products.sql,对比EXPLAIN输出的key和rows是否与开发环境一致。 - 事务验证:在生产库执行
troubleshooting/deadlock_simulation.sql,确认死锁检测机制正常(应返回ERROR 1213 (40001): Deadlock found when trying to get lock)。 - 监控埋点:在
queries/所有脚本开头添加/* migration_v2.1 */注释,方便后续在performance_schema.events_statements_summary_by_digest中追踪慢查询来源。
提示:Mosh 在
optimization/migrate_to_production.sh末尾留了一行注释:“真正的上线不是脚本跑通,而是你敢在凌晨 2 点接到告警电话时,第一反应是查performance_schema.data_locks而不是重启 MySQL”。这句话我抄在了自己笔记本首页。
6. 终极技巧:用 Python 自动化拆分百万行INSERT脚本,以及我的 3 条血泪习惯
学到这里,你可能已经跃跃欲试去跑data/seed_orders.sql。但等等——那个包含 10000 行INSERT的文件,在你本地 MySQL 上执行要 47 秒,而在生产库(尤其是云数据库)上可能直接超时失败。Mosh 的课程文件里没提供拆分工具,但他在optimization/目录放了一个batch_insert_helper.py,这是我根据他的思路重写的增强版,它解决了三个真实痛点:自动按 1000 行切分、保留原始换行缩进、生成带进度条的执行脚本。
6.1 Python 拆分脚本:split_inserts.py
#!/usr/bin/env python3 # 文件:optimization/split_inserts.py import re import sys from pathlib import Path def split_sql_file(input_path: str, output_dir: str, batch_size: int = 1000): """将大型 INSERT 文件按 batch_size 行拆分为多个文件""" input_file = Path(input_path) output_base = Path(output_dir) output_base.mkdir(exist_ok=True) with open(input_file, 'r', encoding='utf-8') as f: content = f.read() # 提取 INSERT 语句(支持多行 VALUES) insert_pattern = r'INSERT\s+INTO\s+`?(\w+)`?\s*\((.*?)\)\s*VALUES\s*\((.*?)\);' matches = list(re.finditer(insert_pattern, content, re.DOTALL | re.IGNORECASE)) if not matches: print(f"警告:未在 {input_file} 中找到 INSERT 语句") return table_name = matches[0].group(1) columns = matches[0].group(2).strip() all_values = [] # 提取所有 VALUES (...) 部分 for match in matches: values_part = match.group(3) # 按逗号分割,但忽略括号内的逗号(处理嵌套 JSON) values_list = re.findall(r'\([^()]*\)|[^,]+', values_part) all_values.extend([v.strip() for v in values_list if v.strip()]) # 拆分为批次 for i in range(0, len(all_values), batch_size): batch_values = all_values[i:i+batch_size] batch_num = i // batch_size + 1 output_file = output_base / f"{table_name}_batch_{batch_num:03d}.sql" with open(output_file, 'w', encoding='utf-8') as f: f.write(f"-- {input_file.name} 拆分批次 {batch_num}\n") f.write(f"INSERT INTO `{table_name}` ({columns}) VALUES \n") # 每行一个 VALUES,用逗号连接 f.write(",\n".join(batch_values)) f.write(";\n") print(f"✓ 已生成 {output_file.name} ({len(batch_values)} 行)") if __name__ == "__main__": if len(sys.argv) != 2: print("用法: python split_inserts.py <input.sql>") sys.exit(1) split_sql_file(sys.argv[1], "data/split", batch_size=1000)使用方法:
cd optimization/ python split_inserts.py ../data/seed_orders.sql # 生成 data/split/orders_batch_001.sql ~ orders_batch_010.sql(共 10 个文件)参数说明:
batch_size=1000:MySQL 默认max_allowed_packet=64M,1000 行orders插入约 4.2MB,安全冗余。re.DOTALL | re.IGNORECASE:确保匹配跨多行的VALUES(如VALUES (1,'a',...), (2,'b',...))。re.findall(r'\([^()]*\)|[^,]+':智能分割VALUES,避免 JSON 字段中的逗号被误切。
6.2 我的 3 条血泪习惯(从 Mosh 课程里抠出来的)
每次
CREATE INDEX后,必跑ANALYZE TABLE table_name
Mosh 在optimization/add_missing_indexes.sql的每条CREATE INDEX后都加了这句。原因:MySQL 的统计信息(information_schema.STATISTICS)不会自动更新,优化器可能仍用旧的行数估算,导致EXPLAIN显示走了索引,实际执行却全表扫描。ANALYZE TABLE强制刷新统计,让EXPLAIN结果真正可信。EXPLAIN的key_len字段,是我判断索引是否被充分利用的“体温计”
比如idx_orders_status_date是(status, created_at, id)三个字段,status是VARCHAR(20),created_at是DATETIME(8 字节),id是BIGINT(8 字节)。如果EXPLAIN中key_len=20,说明只用了status(20 字节);如果是key_len=36,说明三个字段全用上了。Mosh 的queries/脚本里,每个优化后的查询都标注了预期key_len,我养成了执行前先猜、执行后立刻核对的习惯。绝不相信
SELECT COUNT(*)的结果,除非它走的是PRIMARY KEY或NOT NULL索引
Mosh 在troubleshooting/benchmark_count_vs_estimate.sql中演示:对orders表COUNT(*)耗时 1.2s,而SELECT COUNT(id) FROM orders(id是主键)只要 0.03s。因为前者要扫描所有行判断是否为 NULL,后者直接遍历主键索引树。现在我所有统计类查询,第一反应是看EXPLAIN的key字段是否为PRIMARY。
从那以后我每次新建数据库,都强制走一遍setup/→schema/→data/→queries/→optimization/的全流程,哪怕只是本地 Docker 环境。不是为了“学完”,而是为了在某个深夜面对线上慢查询时,手指能本能地敲出EXPLAIN ANALYZE,眼睛能瞬间扫出type: ALL和key_len的异常,心里有底——这事儿,我拆过、踩过、修过。希望帮到你。
本文还有配套的精品资源,点击获取