[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article:1l631ZXg:guest":3,"article:1l631ZXg:related":30},{"id":4,"created_at":5,"updated_at":6,"code":7,"author_id":8,"title":9,"slug":10,"content":11,"content_html":12,"summary":13,"category_slug":14,"visibility":15,"is_pinned":16,"is_recommended":16,"is_approved":17,"view_count":18,"like_count":18,"favorite_count":18,"comment_count":18,"hot_score":18,"recommended_score":18,"status":19,"publish_at":6,"author":20,"is_liked":16,"is_favorite":16},1196,"2026-10-20T20:39:07.779+08:00","2026-10-20T20:40:23.442+08:00","1l631ZXg",4,"SQL 优化实战：从慢查询定位到执行计划精读的完整指南","sql-优化实战：从慢查询定位到执行计划精读的完整指南","# SQL 优化实战：从慢查询定位到执行计划精读的完整指南\n\nSQL 优化是后端工程师性价比最高的技能之一：改对一条索引，可能让查询从 10 秒降到 10 毫秒。本文以\"定位慢 SQL → 读懂执行计划 → 对症下药\"为主线，给出可复用的优化方法论。\n\n## 一、第一步：把慢 SQL 找出来\n\n### 1.1 MySQL 慢查询日志\n\n```ini\n# my.cnf\nslow_query_log = ON\nslow_query_log_file = \u002Fvar\u002Flog\u002Fmysql\u002Fslow.log\nlong_query_time = 1\nlog_queries_not_using_indexes = ON\n```\n\n```sql\n-- 查看当前慢查询\nSHOW GLOBAL STATUS LIKE 'Slow_queries';\n-- 或直接看日志\ntail -100 \u002Fvar\u002Flog\u002Fmysql\u002Fslow.log\n```\n\n### 1.2 实时抓取\n\n```sql\n-- MySQL 8.0 performance_schema\nSELECT * FROM performance_schema.events_statements_summary_by_digest\nORDER BY SUM_TIMER_WAIT DESC LIMIT 10;\n\n-- 通用：processlist 抓正在跑的慢查询\nSELECT * FROM information_schema.PROCESSLIST\nWHERE TIME > 5 AND COMMAND != 'Sleep';\n```\n\n## 二、第二步：读懂执行计划\n\n对目标 SQL 执行 `EXPLAIN`，重点看五列：\n\n```sql\nEXPLAIN SELECT o.order_id, u.nickname\nFROM orders o\nJOIN users u ON u.id = o.user_id\nWHERE o.status = 1 AND o.created_at > '2026-01-01'\nORDER BY o.created_at DESC\nLIMIT 20;\n```\n\n| 列 | 本例表现 | 解读 |\n|----|----------|------|\n| type | orders: ALL, users: eq_ref | orders 全表扫描，users 走主键 |\n| possible_keys | idx_status, idx_created | 可用的索引 |\n| key | NULL | 一个都没用上！ |\n| rows | 1200000 | 预计扫 120 万行 |\n| Extra | Using filesort | 排序未用索引 |\n\n**诊断**：orders 表 WHERE 与 ORDER BY 各自有单列索引，但**联合索引缺失**导致只能全扫 + 文件排序。\n\n## 三、第三步：对症下药\n\n### 3.1 补联合索引\n\n```sql\n-- 等值条件 status 在前，范围\u002F排序 created_at 在后\nALTER TABLE orders ADD INDEX idx_status_created (status, created_at);\n\n-- 验证\nEXPLAIN SELECT ...;  -- type: ref, rows: 2万, Extra: 无 filesort\n```\n\n### 3.2 改写 SQL 的常见手法\n\n**避免函数与隐式转换**：\n\n```sql\n-- 反例\nWHERE DATE(created_at) = '2026-10-20'\n-- 正解\nWHERE created_at >= '2026-10-20 00:00:00'\n  AND created_at \u003C  '2026-10-21 00:00:00'\n```\n\n**NOT IN 改 LEFT JOIN**：\n\n```sql\n-- 反例（子查询物化、扫描大）\nSELECT * FROM products WHERE id NOT IN (SELECT product_id FROM blacklist);\n\n-- 正解\nSELECT p.* FROM products p\nLEFT JOIN blacklist b ON b.product_id = p.id\nWHERE b.id IS NULL;\n```\n\n**OR 改 UNION \u002F IN**：\n\n```sql\n-- 反例：OR 导致索引失效\nWHERE category = 'a' OR category = 'b'\n-- 正解（或直接用 IN）\nWHERE category IN ('a', 'b')\n```\n\n**LIMIT 深翻页**：\n\n```sql\n-- 反例：OFFSET 大\nSELECT * FROM orders ORDER BY id LIMIT 100000, 20;\n-- 正解：键集分页\nSELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20;\n```\n\n## 四、JOIN 优化的执行顺序认知\n\nMySQL 的 JOIN 执行是**驱动表 + 被驱动表**的模式（小表驱动大表）：\n\n```sql\n-- 小表 categories（几千行）驱动大表 articles（百万行）\nSELECT a.* FROM articles a\nINNER JOIN categories c ON c.id = a.category_id\nWHERE c.slug = 'backend';\n```\n\n- **驱动表**（外层）：尽量是小表\u002F高过滤率表；\n- **被驱动表**（内层）：join 字段必须有索引（否则每行内层全扫）；\n- 通过 `EXPLAIN` 观察谁先执行，必要时用 `STRAIGHT_JOIN` 或调整 JOIN 顺序。\n\n## 五、聚合与排序的优化\n\n### 5.1 GROUP BY 走索引\n\n```sql\n-- 反例：Using temporary + Using filesort\nSELECT category, COUNT(*) FROM articles GROUP BY category;\n\n-- 正解：category 建索引后，GROUP BY 直接走索引有序聚合\nALTER TABLE articles ADD INDEX idx_category (category);\n```\n\n### 5.2 大表 COUNT 优化\n\n```sql\n-- 条件 COUNT 尽量走覆盖索引\nSELECT COUNT(*) FROM orders WHERE status = 1;\n-- (status) 单列索引即可覆盖，无需回表\n```\n\n### 5.3 避免 SELECT *\n\n只取需要的列，不仅减少网络传输，还能让**覆盖索引**生效（查询直接命中索引树，不回表）。\n\n## 六、执行计划进阶：EXPLAIN ANALYZE\n\nMySQL 8.0.18+ 的 `EXPLAIN ANALYZE` 直接执行并给出真实耗时：\n\n```sql\nEXPLAIN ANALYZE\nSELECT ... FROM orders WHERE ...;\n\n-- 输出包含每步的实际行数、耗时，比估算更真实\n```\n\n配合 `EXPLAIN FORMAT=JSON`，能看到成本估算与索引命中细节。\n\n## 七、优化闭环与禁忌\n\n```\n慢查询日志 → 定位 TOP SQL → EXPLAIN → 诊断 → 加索引\u002F改写 → 复测耗时 → 回归\n```\n\n**禁忌清单**：\n1. 生产环境直接加索引要看锁表影响（MySQL 8.0 用 `INSTANT` 算法或在线 DDL 工具）；\n2. 不要为每个 WHERE 条件都建单列索引（索引过多拖垮写入）；\n3. 不要迷信\"索引越多越快\"，覆盖索引要精确设计；\n4. 先优化影响面大的（高频 × 慢），别在一年跑两次的报表 SQL 上恋战。\n\n## 结语\n\nSQL 优化是有章可循的工程方法：**日志找慢、计划诊断、索引对症、复测验证**。绝大多数慢查询的根因不外乎三类——缺索引、错误写法、深翻页，对应的解药也相对固定。把本文的检查清单固化为每次发布前的 review 步骤，SQL 性能问题就能在进生产之前被拦截，而不是在线上告警之后才手忙脚乱。\n","\u003Ch1>SQL 优化实战：从慢查询定位到执行计划精读的完整指南\u003C\u002Fh1>\n\u003Cp>SQL 优化是后端工程师性价比最高的技能之一：改对一条索引，可能让查询从 10 秒降到 10 毫秒。本文以&quot;定位慢 SQL → 读懂执行计划 → 对症下药&quot;为主线，给出可复用的优化方法论。\u003C\u002Fp>\n\u003Ch2>一、第一步：把慢 SQL 找出来\u003C\u002Fh2>\n\u003Ch3>1.1 MySQL 慢查询日志\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-ini\"># my.cnf\nslow_query_log = ON\nslow_query_log_file = \u002Fvar\u002Flog\u002Fmysql\u002Fslow.log\nlong_query_time = 1\nlog_queries_not_using_indexes = ON\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 查看当前慢查询\nSHOW GLOBAL STATUS LIKE 'Slow_queries';\n-- 或直接看日志\ntail -100 \u002Fvar\u002Flog\u002Fmysql\u002Fslow.log\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>1.2 实时抓取\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- MySQL 8.0 performance_schema\nSELECT * FROM performance_schema.events_statements_summary_by_digest\nORDER BY SUM_TIMER_WAIT DESC LIMIT 10;\n\n-- 通用：processlist 抓正在跑的慢查询\nSELECT * FROM information_schema.PROCESSLIST\nWHERE TIME &gt; 5 AND COMMAND != 'Sleep';\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>二、第二步：读懂执行计划\u003C\u002Fh2>\n\u003Cp>对目标 SQL 执行 \u003Ccode>EXPLAIN\u003C\u002Fcode>，重点看五列：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">EXPLAIN SELECT o.order_id, u.nickname\nFROM orders o\nJOIN users u ON u.id = o.user_id\nWHERE o.status = 1 AND o.created_at &gt; '2026-01-01'\nORDER BY o.created_at DESC\nLIMIT 20;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>| 列 | 本例表现 | 解读 |\n|----|----------|------|\n| type | orders: ALL, users: eq_ref | orders 全表扫描，users 走主键 |\n| possible_keys | idx_status, idx_created | 可用的索引 |\n| key | NULL | 一个都没用上！ |\n| rows | 1200000 | 预计扫 120 万行 |\n| Extra | Using filesort | 排序未用索引 |\u003C\u002Fp>\n\u003Cp>\u003Cstrong>诊断\u003C\u002Fstrong>：orders 表 WHERE 与 ORDER BY 各自有单列索引，但\u003Cstrong>联合索引缺失\u003C\u002Fstrong>导致只能全扫 + 文件排序。\u003C\u002Fp>\n\u003Ch2>三、第三步：对症下药\u003C\u002Fh2>\n\u003Ch3>3.1 补联合索引\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 等值条件 status 在前，范围\u002F排序 created_at 在后\nALTER TABLE orders ADD INDEX idx_status_created (status, created_at);\n\n-- 验证\nEXPLAIN SELECT ...;  -- type: ref, rows: 2万, Extra: 无 filesort\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>3.2 改写 SQL 的常见手法\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>避免函数与隐式转换\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 反例\nWHERE DATE(created_at) = '2026-10-20'\n-- 正解\nWHERE created_at &gt;= '2026-10-20 00:00:00'\n  AND created_at &lt;  '2026-10-21 00:00:00'\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>NOT IN 改 LEFT JOIN\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 反例（子查询物化、扫描大）\nSELECT * FROM products WHERE id NOT IN (SELECT product_id FROM blacklist);\n\n-- 正解\nSELECT p.* FROM products p\nLEFT JOIN blacklist b ON b.product_id = p.id\nWHERE b.id IS NULL;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>OR 改 UNION \u002F IN\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 反例：OR 导致索引失效\nWHERE category = 'a' OR category = 'b'\n-- 正解（或直接用 IN）\nWHERE category IN ('a', 'b')\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>LIMIT 深翻页\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 反例：OFFSET 大\nSELECT * FROM orders ORDER BY id LIMIT 100000, 20;\n-- 正解：键集分页\nSELECT * FROM orders WHERE id &gt; 100000 ORDER BY id LIMIT 20;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>四、JOIN 优化的执行顺序认知\u003C\u002Fh2>\n\u003Cp>MySQL 的 JOIN 执行是\u003Cstrong>驱动表 + 被驱动表\u003C\u002Fstrong>的模式（小表驱动大表）：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 小表 categories（几千行）驱动大表 articles（百万行）\nSELECT a.* FROM articles a\nINNER JOIN categories c ON c.id = a.category_id\nWHERE c.slug = 'backend';\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cul>\n\u003Cli>\u003Cstrong>驱动表\u003C\u002Fstrong>（外层）：尽量是小表\u002F高过滤率表；\u003C\u002Fli>\n\u003Cli>\u003Cstrong>被驱动表\u003C\u002Fstrong>（内层）：join 字段必须有索引（否则每行内层全扫）；\u003C\u002Fli>\n\u003Cli>通过 \u003Ccode>EXPLAIN\u003C\u002Fcode> 观察谁先执行，必要时用 \u003Ccode>STRAIGHT_JOIN\u003C\u002Fcode> 或调整 JOIN 顺序。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>五、聚合与排序的优化\u003C\u002Fh2>\n\u003Ch3>5.1 GROUP BY 走索引\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 反例：Using temporary + Using filesort\nSELECT category, COUNT(*) FROM articles GROUP BY category;\n\n-- 正解：category 建索引后，GROUP BY 直接走索引有序聚合\nALTER TABLE articles ADD INDEX idx_category (category);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>5.2 大表 COUNT 优化\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 条件 COUNT 尽量走覆盖索引\nSELECT COUNT(*) FROM orders WHERE status = 1;\n-- (status) 单列索引即可覆盖，无需回表\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>5.3 避免 SELECT *\u003C\u002Fh3>\n\u003Cp>只取需要的列，不仅减少网络传输，还能让\u003Cstrong>覆盖索引\u003C\u002Fstrong>生效（查询直接命中索引树，不回表）。\u003C\u002Fp>\n\u003Ch2>六、执行计划进阶：EXPLAIN ANALYZE\u003C\u002Fh2>\n\u003Cp>MySQL 8.0.18+ 的 \u003Ccode>EXPLAIN ANALYZE\u003C\u002Fcode> 直接执行并给出真实耗时：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">EXPLAIN ANALYZE\nSELECT ... FROM orders WHERE ...;\n\n-- 输出包含每步的实际行数、耗时，比估算更真实\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>配合 \u003Ccode>EXPLAIN FORMAT=JSON\u003C\u002Fcode>，能看到成本估算与索引命中细节。\u003C\u002Fp>\n\u003Ch2>七、优化闭环与禁忌\u003C\u002Fh2>\n\u003Cpre>\u003Ccode>慢查询日志 → 定位 TOP SQL → EXPLAIN → 诊断 → 加索引\u002F改写 → 复测耗时 → 回归\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>禁忌清单\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Col>\n\u003Cli>生产环境直接加索引要看锁表影响（MySQL 8.0 用 \u003Ccode>INSTANT\u003C\u002Fcode> 算法或在线 DDL 工具）；\u003C\u002Fli>\n\u003Cli>不要为每个 WHERE 条件都建单列索引（索引过多拖垮写入）；\u003C\u002Fli>\n\u003Cli>不要迷信&quot;索引越多越快&quot;，覆盖索引要精确设计；\u003C\u002Fli>\n\u003Cli>先优化影响面大的（高频 × 慢），别在一年跑两次的报表 SQL 上恋战。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>结语\u003C\u002Fh2>\n\u003Cp>SQL 优化是有章可循的工程方法：\u003Cstrong>日志找慢、计划诊断、索引对症、复测验证\u003C\u002Fstrong>。绝大多数慢查询的根因不外乎三类——缺索引、错误写法、深翻页，对应的解药也相对固定。把本文的检查清单固化为每次发布前的 review 步骤，SQL 性能问题就能在进生产之前被拦截，而不是在线上告警之后才手忙脚乱。\u003C\u002Fp>\n","从慢查询日志定位、EXPLAIN 执行计划精读到索引失效场景与改写技巧，系统掌握 SQL 优化的实战方法论。","数据库","public",false,true,0,"published",{"id":8,"created_at":21,"updated_at":22,"code":23,"username":24,"display_name":25,"role":26,"avatar_url":27,"is_active":17,"is_banned":16,"follower_count":28,"following_count":18,"last_login_at":29},"2023-12-07T23:03:24.891+08:00","2026-10-20T20:39:07.491+08:00","zJXMbKwb","1701961404829","今晚打老虎","user","\u002Fcuts\u002F1702121810380island.jpg",1,"2026-10-20T20:39:07+08:00",[31,52,71],{"id":32,"created_at":33,"updated_at":34,"code":35,"author_id":36,"title":37,"slug":38,"content":39,"content_html":40,"summary":41,"category_slug":14,"visibility":15,"is_pinned":16,"is_recommended":16,"is_approved":17,"view_count":42,"like_count":28,"favorite_count":28,"comment_count":18,"hot_score":43,"recommended_score":18,"status":19,"publish_at":34,"author":44,"is_liked":16,"is_favorite":16},1191,"2026-10-20T20:39:07.253+08:00","2026-10-20T20:40:34.248+08:00","r2Xr32KO",11,"MySQL 备份与容灾实战：从逻辑备份到主从复制的完整体系","mysql-备份与容灾实战：从逻辑备份到主从复制的完整体系","# MySQL 备份与容灾实战：从逻辑备份到主从复制的完整体系\n\n数据库备份是运维的最后一道防线。99% 的数据事故都不可怕，可怕的是\"没有备份\"或\"备份不可用\"。本文从备份类型讲起，到自动化备份、恢复演练与主从容灾，给出生产可用的完整方案。\n\n## 一、备份类型的选择\n\n| 类型 | 工具 | 特点 | 适用 |\n|------|------|------|------|\n| 逻辑备份 | mysqldump | 跨版本\u002F跨平台恢复，慢 | 小库（\u003C10GB）、迁移 |\n| 物理备份 | Percona XtraBackup | 快、在线热备 | 大库、生产环境 |\n| 二进制日志 | binlog | 增量、按时间点恢复 | 所有生产库 |\n\n**黄金组合**：定期全量（XtraBackup）+ 持续增量（binlog）+ 定期演练恢复。\n\n## 二、逻辑备份：mysqldump 的正确姿势\n\n```bash\n# 单库备份（带表结构）\nmysqldump -u backup -p \\\n  --single-transaction --set-gtid-purged=OFF \\\n  --databases app_db > app_db_$(date +%F).sql\n\n# 只备份数据（不含建库语句）\nmysqldump -u backup -p --no-create-info \\\n  --single-transaction app_db > app_db_data_$(date +%F).sql\n```\n\n关键参数解读：\n\n- `--single-transaction`：InnoDB 下基于事务快照，**备份期间不锁表**，是热备的核心；\n- `--set-gtid-purged=OFF`：避免恢复时 GTID 冲突；\n- 备份账号只授予 `SELECT, LOCK TABLES, SHOW VIEW` 等最小权限。\n\n## 三、物理备份：Percona XtraBackup\n\n```bash\n# 全量备份\nxtrabackup --backup --target-dir=\u002Fbackup\u002Ffull \\\n  --host=127.0.0.1 --user=backup --password=xxx\n\n# 准备阶段（应用日志，使备份一致）\nxtrabackup --prepare --target-dir=\u002Fbackup\u002Ffull\n\n# 恢复\nxtrabackup --copy-back --target-dir=\u002Fbackup\u002Ffull\n```\n\n**为什么大库用物理备份**：mysqldump 对 100GB 库可能要跑几小时，XtraBackup 通过物理复制数据文件 + redo log 增量，速度提升一个数量级。\n\n## 四、增量与时间点恢复：binlog 的妙用\n\n### 4.1 开启 binlog\n\n```ini\n# my.cnf\n[mysqld]\nserver-id = 1\nlog-bin = \u002Fvar\u002Flib\u002Fmysql\u002Fmysql-bin\nbinlog_format = ROW\nexpire_logs_days = 7\n```\n\n### 4.2 按时间点恢复\n\n```bash\n# 1. 恢复最近一次全量备份\nmysql -u root -p \u003C full_backup.sql\n\n# 2. 应用备份点之后的 binlog（到误操作前的时刻）\nmysqlbinlog --stop-datetime=\"2026-10-20 03:00:00\" \\\n  mysql-bin.000012 mysql-bin.000013 | mysql -u root -p app_db\n```\n\n**演练价值**：这一整套命令只有在定期演练时才能确保手到擒来。\n\n## 五、自动化备份脚本\n\n```bash\n#!\u002Fusr\u002Fbin\u002Fenv bash\nset -euo pipefail\n\nBACKUP_DIR=\"\u002Fbackup\u002Fdb\"\nRETENTION_DAYS=14\nDB_NAME=\"app_db\"\nDATE=$(date +%F)\n\n# 全量备份\nmysqldump --single-transaction --databases \"$DB_NAME\" \\\n  > \"$BACKUP_DIR\u002F${DB_NAME}_${DATE}.sql\"\n\n# 压缩（gzip 或 zstd）\ngzip -9 \"$BACKUP_DIR\u002F${DB_NAME}_${DATE}.sql\"\n\n# 清理过期备份\nfind \"$BACKUP_DIR\" -name \"*.sql.gz\" -mtime +\"$RETENTION_DAYS\" -delete\n\n# 上传异地（对象存储\u002FSFTP），防本地磁盘一起损坏\nrclone copy \"$BACKUP_DIR\u002F${DB_NAME}_${DATE}.sql.gz\" remote:backup\u002Fdb\u002F\n```\n\n```bash\n# crontab 每日凌晨 2 点执行\n0 2 * * * \u002Fopt\u002Fscripts\u002Fbackup_db.sh >> \u002Fvar\u002Flog\u002Fbackup_db.log 2>&1\n```\n\n**异地备份是底线**：本地磁盘故障、机房火灾都会同时摧毁主库与备份，必须异地\u002F异机一份。\n\n## 六、主从复制：读写分离与容灾\n\n### 6.1 搭建一主一从\n\n```sql\n-- 主库：创建复制账号并记录位点\nCREATE USER 'repl'@'%' IDENTIFIED BY 'repl_pass';\nGRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';\nFLUSH TABLES WITH READ LOCK;\nSHOW MASTER STATUS;   -- 记录 File 与 Position\n```\n\n```sql\n-- 从库：先导入主库备份，再启动复制\nCHANGE MASTER TO\n  MASTER_HOST='10.0.1.11',\n  MASTER_USER='repl',\n  MASTER_PASSWORD='repl_pass',\n  MASTER_LOG_FILE='mysql-bin.000012',\n  MASTER_LOG_POS=123456;\nSTART SLAVE;\nSHOW SLAVE STATUS\\G   -- Slave_IO_Running \u002F Slave_SQL_Running 均为 Yes\n```\n\n### 6.2 主从延迟的监控\n\n```sql\n-- 从库执行，Seconds_Behind_Master 是延迟秒数\nSHOW SLAVE STATUS\\G\n```\n\n延迟持续增长时排查：从库 IO 压力、大事务、DDL 锁、单线程应用（可用 MTS 并行复制优化）。\n\n### 6.3 故障切换（MHA \u002F Orchestrator）\n\n生产环境用 Orchestrator 等工具管理拓扑：主库宕机自动提升从库、迁移 VIP、通知告警。**注意**：自动切换只解决\"可用性\"，数据丢失（半同步复制可缓解）取决于复制延迟。\n\n## 七、恢复演练：备份的灵魂\n\n```\n演练场景：误删 app_db 核心表，恢复到 30 分钟前。\n演练步骤：\n1. 恢复最近全量备份到临时实例\n2. 应用 binlog 到误删前时刻\n3. 导出误删表数据，导回生产\n4. 对比行数\u002F关键业务校验\n```\n\n**结论**：没有演练过的备份等于没有备份。建议每季度全流程演练一次，并把演练时长计入 SLO。\n\n## 结语\n\n备份体系的完备度不取决于工具多先进，而取决于**恢复路径是否被验证过**。全量 + binlog 增量 + 异地 + 定期演练，这套组合能覆盖绝大多数数据事故。把备份脚本、恢复手册、演练记录沉淀为团队的运维资产，你才能真正在深夜收到\"误删数据\"的电话时，保持冷静、按章操作、快速恢复。\n","\u003Ch1>MySQL 备份与容灾实战：从逻辑备份到主从复制的完整体系\u003C\u002Fh1>\n\u003Cp>数据库备份是运维的最后一道防线。99% 的数据事故都不可怕，可怕的是&quot;没有备份&quot;或&quot;备份不可用&quot;。本文从备份类型讲起，到自动化备份、恢复演练与主从容灾，给出生产可用的完整方案。\u003C\u002Fp>\n\u003Ch2>一、备份类型的选择\u003C\u002Fh2>\n\u003Cp>| 类型 | 工具 | 特点 | 适用 |\n|------|------|------|------|\n| 逻辑备份 | mysqldump | 跨版本\u002F跨平台恢复，慢 | 小库（&lt;10GB）、迁移 |\n| 物理备份 | Percona XtraBackup | 快、在线热备 | 大库、生产环境 |\n| 二进制日志 | binlog | 增量、按时间点恢复 | 所有生产库 |\u003C\u002Fp>\n\u003Cp>\u003Cstrong>黄金组合\u003C\u002Fstrong>：定期全量（XtraBackup）+ 持续增量（binlog）+ 定期演练恢复。\u003C\u002Fp>\n\u003Ch2>二、逻辑备份：mysqldump 的正确姿势\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-bash\"># 单库备份（带表结构）\nmysqldump -u backup -p \\\n  --single-transaction --set-gtid-purged=OFF \\\n  --databases app_db &gt; app_db_$(date +%F).sql\n\n# 只备份数据（不含建库语句）\nmysqldump -u backup -p --no-create-info \\\n  --single-transaction app_db &gt; app_db_data_$(date +%F).sql\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>关键参数解读：\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>--single-transaction\u003C\u002Fcode>：InnoDB 下基于事务快照，\u003Cstrong>备份期间不锁表\u003C\u002Fstrong>，是热备的核心；\u003C\u002Fli>\n\u003Cli>\u003Ccode>--set-gtid-purged=OFF\u003C\u002Fcode>：避免恢复时 GTID 冲突；\u003C\u002Fli>\n\u003Cli>备份账号只授予 \u003Ccode>SELECT, LOCK TABLES, SHOW VIEW\u003C\u002Fcode> 等最小权限。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>三、物理备份：Percona XtraBackup\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-bash\"># 全量备份\nxtrabackup --backup --target-dir=\u002Fbackup\u002Ffull \\\n  --host=127.0.0.1 --user=backup --password=xxx\n\n# 准备阶段（应用日志，使备份一致）\nxtrabackup --prepare --target-dir=\u002Fbackup\u002Ffull\n\n# 恢复\nxtrabackup --copy-back --target-dir=\u002Fbackup\u002Ffull\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>为什么大库用物理备份\u003C\u002Fstrong>：mysqldump 对 100GB 库可能要跑几小时，XtraBackup 通过物理复制数据文件 + redo log 增量，速度提升一个数量级。\u003C\u002Fp>\n\u003Ch2>四、增量与时间点恢复：binlog 的妙用\u003C\u002Fh2>\n\u003Ch3>4.1 开启 binlog\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-ini\"># my.cnf\n[mysqld]\nserver-id = 1\nlog-bin = \u002Fvar\u002Flib\u002Fmysql\u002Fmysql-bin\nbinlog_format = ROW\nexpire_logs_days = 7\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>4.2 按时间点恢复\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-bash\"># 1. 恢复最近一次全量备份\nmysql -u root -p &lt; full_backup.sql\n\n# 2. 应用备份点之后的 binlog（到误操作前的时刻）\nmysqlbinlog --stop-datetime=&quot;2026-10-20 03:00:00&quot; \\\n  mysql-bin.000012 mysql-bin.000013 | mysql -u root -p app_db\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>演练价值\u003C\u002Fstrong>：这一整套命令只有在定期演练时才能确保手到擒来。\u003C\u002Fp>\n\u003Ch2>五、自动化备份脚本\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-bash\">#!\u002Fusr\u002Fbin\u002Fenv bash\nset -euo pipefail\n\nBACKUP_DIR=&quot;\u002Fbackup\u002Fdb&quot;\nRETENTION_DAYS=14\nDB_NAME=&quot;app_db&quot;\nDATE=$(date +%F)\n\n# 全量备份\nmysqldump --single-transaction --databases &quot;$DB_NAME&quot; \\\n  &gt; &quot;$BACKUP_DIR\u002F${DB_NAME}_${DATE}.sql&quot;\n\n# 压缩（gzip 或 zstd）\ngzip -9 &quot;$BACKUP_DIR\u002F${DB_NAME}_${DATE}.sql&quot;\n\n# 清理过期备份\nfind &quot;$BACKUP_DIR&quot; -name &quot;*.sql.gz&quot; -mtime +&quot;$RETENTION_DAYS&quot; -delete\n\n# 上传异地（对象存储\u002FSFTP），防本地磁盘一起损坏\nrclone copy &quot;$BACKUP_DIR\u002F${DB_NAME}_${DATE}.sql.gz&quot; remote:backup\u002Fdb\u002F\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cpre>\u003Ccode class=\"language-bash\"># crontab 每日凌晨 2 点执行\n0 2 * * * \u002Fopt\u002Fscripts\u002Fbackup_db.sh &gt;&gt; \u002Fvar\u002Flog\u002Fbackup_db.log 2&gt;&amp;1\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>异地备份是底线\u003C\u002Fstrong>：本地磁盘故障、机房火灾都会同时摧毁主库与备份，必须异地\u002F异机一份。\u003C\u002Fp>\n\u003Ch2>六、主从复制：读写分离与容灾\u003C\u002Fh2>\n\u003Ch3>6.1 搭建一主一从\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 主库：创建复制账号并记录位点\nCREATE USER 'repl'@'%' IDENTIFIED BY 'repl_pass';\nGRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';\nFLUSH TABLES WITH READ LOCK;\nSHOW MASTER STATUS;   -- 记录 File 与 Position\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 从库：先导入主库备份，再启动复制\nCHANGE MASTER TO\n  MASTER_HOST='10.0.1.11',\n  MASTER_USER='repl',\n  MASTER_PASSWORD='repl_pass',\n  MASTER_LOG_FILE='mysql-bin.000012',\n  MASTER_LOG_POS=123456;\nSTART SLAVE;\nSHOW SLAVE STATUS\\G   -- Slave_IO_Running \u002F Slave_SQL_Running 均为 Yes\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>6.2 主从延迟的监控\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 从库执行，Seconds_Behind_Master 是延迟秒数\nSHOW SLAVE STATUS\\G\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>延迟持续增长时排查：从库 IO 压力、大事务、DDL 锁、单线程应用（可用 MTS 并行复制优化）。\u003C\u002Fp>\n\u003Ch3>6.3 故障切换（MHA \u002F Orchestrator）\u003C\u002Fh3>\n\u003Cp>生产环境用 Orchestrator 等工具管理拓扑：主库宕机自动提升从库、迁移 VIP、通知告警。\u003Cstrong>注意\u003C\u002Fstrong>：自动切换只解决&quot;可用性&quot;，数据丢失（半同步复制可缓解）取决于复制延迟。\u003C\u002Fp>\n\u003Ch2>七、恢复演练：备份的灵魂\u003C\u002Fh2>\n\u003Cpre>\u003Ccode>演练场景：误删 app_db 核心表，恢复到 30 分钟前。\n演练步骤：\n1. 恢复最近全量备份到临时实例\n2. 应用 binlog 到误删前时刻\n3. 导出误删表数据，导回生产\n4. 对比行数\u002F关键业务校验\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>结论\u003C\u002Fstrong>：没有演练过的备份等于没有备份。建议每季度全流程演练一次，并把演练时长计入 SLO。\u003C\u002Fp>\n\u003Ch2>结语\u003C\u002Fh2>\n\u003Cp>备份体系的完备度不取决于工具多先进，而取决于\u003Cstrong>恢复路径是否被验证过\u003C\u002Fstrong>。全量 + binlog 增量 + 异地 + 定期演练，这套组合能覆盖绝大多数数据事故。把备份脚本、恢复手册、演练记录沉淀为团队的运维资产，你才能真正在深夜收到&quot;误删数据&quot;的电话时，保持冷静、按章操作、快速恢复。\u003C\u002Fp>\n","从逻辑备份、物理备份、Binlog 增量备份到主从复制与半同步，构建 MySQL 可恢复、可验证的容灾备份体系。",3,39841.53820887155,{"id":36,"created_at":45,"updated_at":46,"code":47,"username":48,"display_name":49,"role":26,"avatar_url":50,"is_active":17,"is_banned":16,"follower_count":28,"following_count":18,"last_login_at":51},"2024-09-12T11:58:24.365+08:00","2026-10-20T20:39:06.889+08:00","kbXvOKMJ","1726113504301","打工人","\u002Fcuts\u002F1726370661979da4b1661617b527c821952001.jpg","2026-10-20T20:39:06+08:00",{"id":53,"created_at":54,"updated_at":55,"code":56,"author_id":42,"title":57,"slug":58,"content":59,"content_html":60,"summary":61,"category_slug":14,"visibility":15,"is_pinned":16,"is_recommended":16,"is_approved":17,"view_count":8,"like_count":28,"favorite_count":18,"comment_count":18,"hot_score":62,"recommended_score":18,"status":19,"publish_at":55,"author":63,"is_liked":16,"is_favorite":16},1175,"2026-10-20T18:08:48.9+08:00","2026-10-20T18:09:32.69+08:00","xD81zVXg","MySQL 索引优化实战：从 EXPLAIN 到查询提速的完整指南","mysql-索引优化实战：从-explain-到查询提速的完整指南","# MySQL 索引优化实战：从 EXPLAIN 到查询提速的完整指南\n\n索引是关系型数据库性能的基石。同样一条 SQL，在索引缺失与索引合理的情况下，查询耗时可能相差几个数量级。本文从执行计划入手，系统梳理 MySQL 索引的设计原则、失效场景与调优方法论。\n\n## 一、先看懂 EXPLAIN\n\n任何索引优化都从读懂执行计划开始。`EXPLAIN SELECT ...` 输出每一行代表一个执行步骤，核心列的含义如下：\n\n| 列名 | 含义 | 需要警惕的值 |\n|------|------|--------------|\n| type | 访问类型，从好到差依次为 | system > const > eq_ref > ref > range > index > ALL |\n| key | 实际使用的索引 | NULL 表示没走索引 |\n| rows | 预估扫描行数 | 与实际行数偏差过大说明统计信息陈旧 |\n| Extra | 附加信息 | Using filesort \u002F Using temporary 是需要优化的信号 |\n\n最需要记住的一条经验：**type 为 ALL（全表扫描）或 index（扫描整个索引）时，先停下来分析**。只要命中 range 及以上，SQL 通常还有救。\n\n## 二、索引失效的十个高频场景\n\n### 2.1 对索引列做函数运算\n\n```sql\n-- 反例：对 created_at 取函数，索引失效\nSELECT * FROM orders WHERE DATE(created_at) = '2026-10-01';\n\n-- 正解：改成范围查询，走索引\nSELECT * FROM orders\nWHERE created_at >= '2026-10-01 00:00:00'\n  AND created_at \u003C '2026-10-02 00:00:00';\n```\n\n### 2.2 隐式类型转换\n\n```sql\n-- 反例：phone 是 varchar，传入数字导致隐式转换，索引失效\nSELECT * FROM users WHERE phone = 13800138000;\n\n-- 正解：加引号，保持类型一致\nSELECT * FROM users WHERE phone = '13800138000';\n```\n\n### 2.3 前导模糊查询\n\n```sql\n-- '%xx' 开头无法利用索引，但 'xx%' 可以\nSELECT * FROM users WHERE name LIKE '%小明%';  -- 索引失效\nSELECT * FROM users WHERE name LIKE '小明%';    -- 走 range\n```\n\n### 2.4 联合索引违反最左前缀\n\n联合索引 `(a, b, c)` 遵循最左前缀原则：`WHERE b=1 AND c=2` 用不上，`WHERE a=1 AND c=2` 只能用到 a。\n\n### 2.5 索引列参与计算或比较\n\n```sql\n-- 反例\nSELECT * FROM products WHERE price * 0.9 \u003C 100;\n-- 正解：把计算移到常量侧\nSELECT * FROM products WHERE price \u003C 100 \u002F 0.9;\n```\n\n## 三、索引设计五原则\n\n1. **区分度优先**：选择性 = 去重值数 \u002F 总行数，低于 20% 的列单独建索引意义不大。\n2. **联合索引按\"等值在前、排序在后\"排列**：`WHERE a=? AND b>? ORDER BY c`，索引设计为 `(a, b, c)` 可同时满足过滤与排序。\n3. **覆盖索引是查询提速神器**：`(a, b, c)` 索引让 `SELECT a,b,c` 直接从索引树返回，避免回表。\n4. **控制索引数量**：单表索引控制在 5 个以内，每个索引都要考虑写入成本。\n5. **字符串前缀索引**：大字段用 `INDEX idx_name(name(20))`，用长度换空间。\n\n## 四、order by \u002F group by 的索引优化\n\n```sql\n-- 反例：产生 Using filesort\nSELECT id, title FROM articles WHERE category_id = 5 ORDER BY created_at DESC;\n\n-- 正解：联合索引 (category_id, created_at)，排序免 filesort\nALTER TABLE articles ADD INDEX idx_cat_time (category_id, created_at);\n```\n\n`GROUP BY` 同理，本质是先排序再分组。给 group by 列建联合索引能显著提升性能。\n\n## 五、分页深翻页的优化\n\n`LIMIT 100000, 20` 需要扫描 100020 行再丢弃前 10 万行，这是经典性能陷阱：\n\n```sql\n-- 反例：深翻页\nSELECT * FROM orders ORDER BY id LIMIT 100000, 20;\n\n-- 方案一：延迟关联（先取主键，再回表）\nSELECT o.* FROM orders o\nINNER JOIN (SELECT id FROM orders ORDER BY id LIMIT 100000, 20) t\nON o.id = t.id;\n\n-- 方案二：游标\u002F键集分页（推荐）\nSELECT * FROM orders\nWHERE id > :last_seen_id\nORDER BY id LIMIT 20;\n```\n\n## 六、count 的优化思路\n\n- `COUNT(*)` 和 `COUNT(1)` 在 MySQL 8 中性能无差异，别纠结；\n- 大表精确 count 无法避免全表扫描（InnoDB 不缓存行数），业务上可用缓存计数、抽样估算替代；\n- 条件 count 走覆盖索引：`SELECT COUNT(*) FROM t WHERE status=1`，若 `(status)` 有索引则走 Index Only Scan。\n\n## 七、调优方法论：先测量再动手\n\n1. `EXPLAIN ANALYZE`（MySQL 8.0+）给出真实执行耗时与行数，比估算更可靠；\n2. 开启慢查询日志，把 `long_query_time` 设为 1 秒，找到真正需要优化的 SQL；\n3. 每次只改一个变量（索引\u002F写法\u002F参数），用同样的数据集对比；\n4. 上线前用 `EXPLAIN` 复查，确认 type 和 rows 符合预期。\n\n## 结语\n\n索引优化的本质是**减少磁盘随机 IO**。理解 B+ 树、覆盖索引、最左前缀这三件事，90% 的慢查询都能自己分析出原因。记住：先 EXPLAIN，再动手，用数据说话，而不是靠感觉。\n","\u003Ch1>MySQL 索引优化实战：从 EXPLAIN 到查询提速的完整指南\u003C\u002Fh1>\n\u003Cp>索引是关系型数据库性能的基石。同样一条 SQL，在索引缺失与索引合理的情况下，查询耗时可能相差几个数量级。本文从执行计划入手，系统梳理 MySQL 索引的设计原则、失效场景与调优方法论。\u003C\u002Fp>\n\u003Ch2>一、先看懂 EXPLAIN\u003C\u002Fh2>\n\u003Cp>任何索引优化都从读懂执行计划开始。\u003Ccode>EXPLAIN SELECT ...\u003C\u002Fcode> 输出每一行代表一个执行步骤，核心列的含义如下：\u003C\u002Fp>\n\u003Cp>| 列名 | 含义 | 需要警惕的值 |\n|------|------|--------------|\n| type | 访问类型，从好到差依次为 | system &gt; const &gt; eq_ref &gt; ref &gt; range &gt; index &gt; ALL |\n| key | 实际使用的索引 | NULL 表示没走索引 |\n| rows | 预估扫描行数 | 与实际行数偏差过大说明统计信息陈旧 |\n| Extra | 附加信息 | Using filesort \u002F Using temporary 是需要优化的信号 |\u003C\u002Fp>\n\u003Cp>最需要记住的一条经验：\u003Cstrong>type 为 ALL（全表扫描）或 index（扫描整个索引）时，先停下来分析\u003C\u002Fstrong>。只要命中 range 及以上，SQL 通常还有救。\u003C\u002Fp>\n\u003Ch2>二、索引失效的十个高频场景\u003C\u002Fh2>\n\u003Ch3>2.1 对索引列做函数运算\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 反例：对 created_at 取函数，索引失效\nSELECT * FROM orders WHERE DATE(created_at) = '2026-10-01';\n\n-- 正解：改成范围查询，走索引\nSELECT * FROM orders\nWHERE created_at &gt;= '2026-10-01 00:00:00'\n  AND created_at &lt; '2026-10-02 00:00:00';\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>2.2 隐式类型转换\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 反例：phone 是 varchar，传入数字导致隐式转换，索引失效\nSELECT * FROM users WHERE phone = 13800138000;\n\n-- 正解：加引号，保持类型一致\nSELECT * FROM users WHERE phone = '13800138000';\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>2.3 前导模糊查询\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- '%xx' 开头无法利用索引，但 'xx%' 可以\nSELECT * FROM users WHERE name LIKE '%小明%';  -- 索引失效\nSELECT * FROM users WHERE name LIKE '小明%';    -- 走 range\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>2.4 联合索引违反最左前缀\u003C\u002Fh3>\n\u003Cp>联合索引 \u003Ccode>(a, b, c)\u003C\u002Fcode> 遵循最左前缀原则：\u003Ccode>WHERE b=1 AND c=2\u003C\u002Fcode> 用不上，\u003Ccode>WHERE a=1 AND c=2\u003C\u002Fcode> 只能用到 a。\u003C\u002Fp>\n\u003Ch3>2.5 索引列参与计算或比较\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 反例\nSELECT * FROM products WHERE price * 0.9 &lt; 100;\n-- 正解：把计算移到常量侧\nSELECT * FROM products WHERE price &lt; 100 \u002F 0.9;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>三、索引设计五原则\u003C\u002Fh2>\n\u003Col>\n\u003Cli>\u003Cstrong>区分度优先\u003C\u002Fstrong>：选择性 = 去重值数 \u002F 总行数，低于 20% 的列单独建索引意义不大。\u003C\u002Fli>\n\u003Cli>\u003Cstrong>联合索引按&quot;等值在前、排序在后&quot;排列\u003C\u002Fstrong>：\u003Ccode>WHERE a=? AND b&gt;? ORDER BY c\u003C\u002Fcode>，索引设计为 \u003Ccode>(a, b, c)\u003C\u002Fcode> 可同时满足过滤与排序。\u003C\u002Fli>\n\u003Cli>\u003Cstrong>覆盖索引是查询提速神器\u003C\u002Fstrong>：\u003Ccode>(a, b, c)\u003C\u002Fcode> 索引让 \u003Ccode>SELECT a,b,c\u003C\u002Fcode> 直接从索引树返回，避免回表。\u003C\u002Fli>\n\u003Cli>\u003Cstrong>控制索引数量\u003C\u002Fstrong>：单表索引控制在 5 个以内，每个索引都要考虑写入成本。\u003C\u002Fli>\n\u003Cli>\u003Cstrong>字符串前缀索引\u003C\u002Fstrong>：大字段用 \u003Ccode>INDEX idx_name(name(20))\u003C\u002Fcode>，用长度换空间。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>四、order by \u002F group by 的索引优化\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 反例：产生 Using filesort\nSELECT id, title FROM articles WHERE category_id = 5 ORDER BY created_at DESC;\n\n-- 正解：联合索引 (category_id, created_at)，排序免 filesort\nALTER TABLE articles ADD INDEX idx_cat_time (category_id, created_at);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>GROUP BY\u003C\u002Fcode> 同理，本质是先排序再分组。给 group by 列建联合索引能显著提升性能。\u003C\u002Fp>\n\u003Ch2>五、分页深翻页的优化\u003C\u002Fh2>\n\u003Cp>\u003Ccode>LIMIT 100000, 20\u003C\u002Fcode> 需要扫描 100020 行再丢弃前 10 万行，这是经典性能陷阱：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 反例：深翻页\nSELECT * FROM orders ORDER BY id LIMIT 100000, 20;\n\n-- 方案一：延迟关联（先取主键，再回表）\nSELECT o.* FROM orders o\nINNER JOIN (SELECT id FROM orders ORDER BY id LIMIT 100000, 20) t\nON o.id = t.id;\n\n-- 方案二：游标\u002F键集分页（推荐）\nSELECT * FROM orders\nWHERE id &gt; :last_seen_id\nORDER BY id LIMIT 20;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>六、count 的优化思路\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Ccode>COUNT(*)\u003C\u002Fcode> 和 \u003Ccode>COUNT(1)\u003C\u002Fcode> 在 MySQL 8 中性能无差异，别纠结；\u003C\u002Fli>\n\u003Cli>大表精确 count 无法避免全表扫描（InnoDB 不缓存行数），业务上可用缓存计数、抽样估算替代；\u003C\u002Fli>\n\u003Cli>条件 count 走覆盖索引：\u003Ccode>SELECT COUNT(*) FROM t WHERE status=1\u003C\u002Fcode>，若 \u003Ccode>(status)\u003C\u002Fcode> 有索引则走 Index Only Scan。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>七、调优方法论：先测量再动手\u003C\u002Fh2>\n\u003Col>\n\u003Cli>\u003Ccode>EXPLAIN ANALYZE\u003C\u002Fcode>（MySQL 8.0+）给出真实执行耗时与行数，比估算更可靠；\u003C\u002Fli>\n\u003Cli>开启慢查询日志，把 \u003Ccode>long_query_time\u003C\u002Fcode> 设为 1 秒，找到真正需要优化的 SQL；\u003C\u002Fli>\n\u003Cli>每次只改一个变量（索引\u002F写法\u002F参数），用同样的数据集对比；\u003C\u002Fli>\n\u003Cli>上线前用 \u003Ccode>EXPLAIN\u003C\u002Fcode> 复查，确认 type 和 rows 符合预期。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>结语\u003C\u002Fh2>\n\u003Cp>索引优化的本质是\u003Cstrong>减少磁盘随机 IO\u003C\u002Fstrong>。理解 B+ 树、覆盖索引、最左前缀这三件事，90% 的慢查询都能自己分析出原因。记住：先 EXPLAIN，再动手，用数据说话，而不是靠感觉。\u003C\u002Fp>\n","从执行计划 EXPLAIN 入手，系统梳理 MySQL 索引的设计原则、十个失效场景与调优方法论，含 order by、深翻页、count 优化等实战技巧。",39837.530651119785,{"id":42,"created_at":64,"updated_at":65,"code":66,"username":67,"display_name":68,"role":26,"avatar_url":69,"is_active":17,"is_banned":16,"follower_count":28,"following_count":18,"last_login_at":70},"2023-12-05T17:38:27.845+08:00","2026-10-21T00:01:41.406+08:00","r2Xr2XOR","1701769107783","小卒子","\u002Fcuts\u002F17021221850442010-06-29_233540.jpg","2026-10-21T00:01:41+08:00",{"id":72,"created_at":73,"updated_at":74,"code":75,"author_id":8,"title":76,"slug":77,"content":78,"content_html":79,"summary":80,"category_slug":14,"visibility":15,"is_pinned":16,"is_recommended":16,"is_approved":17,"view_count":81,"like_count":18,"favorite_count":18,"comment_count":18,"hot_score":82,"recommended_score":18,"status":19,"publish_at":74,"author":83,"is_liked":16,"is_favorite":16},1198,"2026-10-20T20:39:07.914+08:00","2026-10-20T20:40:30.191+08:00","Q18bNJ8x","ClickHouse 实战：从列式存储到海量数据分析的完整指南","clickhouse-实战：从列式存储到海量数据分析的完整指南","# ClickHouse 实战：从列式存储到海量数据分析的完整指南\n\n当业务数据量达到亿级、查询要求秒级返回时，传统 OLTP 数据库开始力不从心。ClickHouse 以列式存储 + 向量化执行引擎，专为 OLAP 分析场景而生，单机即可支撑每秒数亿行的扫描吞吐。本文从核心原理讲起，完整走一遍落地路径。\n\n## 一、为什么 ClickHouse 这么快\n\n### 1.1 列式存储\n\n传统行式存储按行读写，分析查询（只取几列）也要读整行；ClickHouse 按列存储，查询只读取涉及的列，IO 量降低一个数量级：\n\n```\n行式: [id, name, time, value] [id, name, time, value] ...\n列式: [id,id,id] [name,name,name] [time,time,time] [value,value,value]\n```\n\n### 1.2 向量化执行\n\nCPU 一次指令处理一整个数据块（SIMD），而非逐行循环，配合极致的压缩率（列同质性高），磁盘 IO 与 CPU 双双受益。\n\n### 1.3 数据压缩\n\n列式存储让同列数据高度相似，压缩比可达 10:1 甚至更高，进一步减少 IO。\n\n## 二、核心表引擎：MergeTree 家族\n\nMergeTree 是 ClickHouse 的基石，理解它的机制就理解了 CH 的用法：\n\n```sql\nCREATE TABLE events (\n    event_date Date,\n    event_type String,\n    user_id    UInt64,\n    amount     Float64\n) ENGINE = MergeTree\nPARTITION BY toYYYYMM(event_date)   -- 按月分区\nORDER BY (event_date, user_id)      -- 排序键，决定存储顺序\n```\n\n**核心机制**：\n- **分区**：按分区存储，查询可裁剪跳过无关分区；\n- **排序键**：数据按 ORDER BY 列有序存储，等于天然索引；\n- **后台合并**：小片段后台异步合并成大片段，写入即可查。\n\n## 三、实战查询：从基础到进阶\n\n### 3.1 基础聚合\n\n```sql\n-- 按日统计营收\nSELECT event_date, sum(amount) AS revenue\nFROM events\nWHERE event_date >= '2026-10-01'\nGROUP BY event_date\nORDER BY event_date;\n```\n\n### 3.2 常用函数\n\n```sql\n-- 去重计数（近似，速度快）\nSELECT uniqExact(user_id) FROM events WHERE event_date = '2026-10-20';\n\n-- 分位数（P95 延迟）\nSELECT quantile(0.95)(latency_ms) FROM requests WHERE date = today();\n\n-- 漏斗\u002F路径分析\nSELECT\n    level,\n    count() AS users\nFROM (\n    SELECT user_id,\n           multiIf(\n               has(groupArray(event_type), 'view') AND has(groupArray(event_type), 'click'), 2,\n               has(groupArray(event_type), 'view'), 1, 0) AS level\n    FROM events GROUP BY user_id\n)\nGROUP BY level;\n```\n\n### 3.3 物化视图：预聚合加速\n\n```sql\nCREATE MATERIALIZED VIEW daily_stats\nENGINE = SummingMergeTree\nORDER BY (event_date, event_type)\nAS\nSELECT\n    toDate(time) AS event_date,\n    event_type,\n    count() AS cnt,\n    sum(amount) AS total\nFROM events\nGROUP BY event_date, event_type;\n```\n\n**价值**：写入时实时增量聚合，查询直接读预聚合结果，亿级明细上的秒级统计不再依赖全量扫描。\n\n## 四、写入与更新：理解 CH 的数据模型\n\n### 4.1 批量写入\n\n```sql\n-- 批量 INSERT 是推荐方式\nINSERT INTO events VALUES\n  ('2026-10-20', 'view', 1, 10.0),\n  ('2026-10-20', 'click', 1, 0.0);\n```\n\n```go\n\u002F\u002F Go 侧批量写入\nbatch := ch.Conn.Batch(\"INSERT INTO events VALUES (?, ?, ?, ?)\")\nfor _, e := range events {\n    batch.Append(e.Date, e.Type, e.UserID, e.Amount)\n}\nbatch.Send()\n```\n\n### 4.2 更新与删除（Mutation）\n\nCH 的 UPDATE\u002FDELETE 是\"异步重写\"操作，开销大：\n\n```sql\nALTER TABLE events DELETE WHERE event_date \u003C '2026-01-01';\nALTER TABLE events UPDATE amount = 0 WHERE event_type = 'refund';\n```\n\n**实践**：高频更新场景不适合 CH；数据修正用\"插入新版本 + 查询取最新\"的幂等思路更合适。\n\n## 五、分布式表：从单机到集群\n\n### 5.1 集群与分布式表\n\n```xml\n\u003C!-- config.xml 配置集群 -->\n\u003Cremote_servers>\n    \u003Cmy_cluster>\n        \u003Cshard>\u003Creplica>\u003Chost>ch-1\u003C\u002Fhost>\u003Cport>9000\u003C\u002Fport>\u003C\u002Freplica>\u003C\u002Fshard>\n        \u003Cshard>\u003Creplica>\u003Chost>ch-2\u003C\u002Fhost>\u003Cport>9000\u003C\u002Fport>\u003C\u002Freplica>\u003C\u002Fshard>\n    \u003C\u002Fmy_cluster>\n\u003C\u002Fremote_servers>\n```\n\n```sql\n-- 本地表 + 分布式表\nCREATE TABLE events_local ... ENGINE = MergeTree ...;\nCREATE TABLE events AS events_local\nENGINE = Distributed(my_cluster, default, events_local, rand());\n```\n\n应用只写分布式表，数据自动按分片路由，查询自动聚合各分片结果。\n\n### 5.2 副本：ReplicatedMergeTree\n\n```sql\nCREATE TABLE events\nENGINE = ReplicatedMergeTree('\u002Fclickhouse\u002Ftables\u002F{shard}\u002Fevents', '{replica}')\nPARTITION BY toYYYYMM(event_date)\nORDER BY (event_date, user_id);\n```\n\nReplicated 引擎基于 ZooKeeper\u002FClickHouse Keeper 自动同步副本，任一节点故障数据不丢。\n\n## 六、常用运维与监控\n\n```bash\nclickhouse-client --query \"SELECT * FROM system.parts WHERE active=1\"   # 查看分区\nclickhouse-client --query \"SELECT * FROM system.mutations\"              # 查看后台任务\nclickhouse-client --query \"SHOW PROCESSLIST\"                            # 查看运行中查询\n```\n\n**监控指标**：查询延迟 P99、每秒查询数、磁盘 IO、后台 merge 队列长度。\n\n## 七、适用边界与选型\n\n| 场景 | 推荐 |\n|------|------|\n| 亿级日志、埋点分析、漏斗转化 | ClickHouse |\n| 在线交易、强一致性 OLTP | MySQL\u002FPostgreSQL（不是 CH） |\n| 多维报表、即席查询 | ClickHouse |\n| 秒级实时流计算 | 配合 Kafka + 物化视图 |\n\n**边界**：CH 不适合高频点查（按主键随机查一行）与小事务更新，那是 KV\u002FOLTP 的领域。\n\n## 结语\n\nClickHouse 的价值一句话概括：**把\"等不起的分析\"变成\"等得起的分析\"**。列式存储 + 向量化执行 + 预聚合物化视图，三板斧下来，亿级数据的聚合统计可以从分钟级压到毫秒级。落地建议：先从最痛的报表\u002F日志场景开始，用 MergeTree + 物化视图跑通一条真实链路，再逐步把分析查询从 MySQL 迁移过来——你会发现，分析型需求终于不用再\"砍需求\"了。\n","\u003Ch1>ClickHouse 实战：从列式存储到海量数据分析的完整指南\u003C\u002Fh1>\n\u003Cp>当业务数据量达到亿级、查询要求秒级返回时，传统 OLTP 数据库开始力不从心。ClickHouse 以列式存储 + 向量化执行引擎，专为 OLAP 分析场景而生，单机即可支撑每秒数亿行的扫描吞吐。本文从核心原理讲起，完整走一遍落地路径。\u003C\u002Fp>\n\u003Ch2>一、为什么 ClickHouse 这么快\u003C\u002Fh2>\n\u003Ch3>1.1 列式存储\u003C\u002Fh3>\n\u003Cp>传统行式存储按行读写，分析查询（只取几列）也要读整行；ClickHouse 按列存储，查询只读取涉及的列，IO 量降低一个数量级：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>行式: [id, name, time, value] [id, name, time, value] ...\n列式: [id,id,id] [name,name,name] [time,time,time] [value,value,value]\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>1.2 向量化执行\u003C\u002Fh3>\n\u003Cp>CPU 一次指令处理一整个数据块（SIMD），而非逐行循环，配合极致的压缩率（列同质性高），磁盘 IO 与 CPU 双双受益。\u003C\u002Fp>\n\u003Ch3>1.3 数据压缩\u003C\u002Fh3>\n\u003Cp>列式存储让同列数据高度相似，压缩比可达 10:1 甚至更高，进一步减少 IO。\u003C\u002Fp>\n\u003Ch2>二、核心表引擎：MergeTree 家族\u003C\u002Fh2>\n\u003Cp>MergeTree 是 ClickHouse 的基石，理解它的机制就理解了 CH 的用法：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">CREATE TABLE events (\n    event_date Date,\n    event_type String,\n    user_id    UInt64,\n    amount     Float64\n) ENGINE = MergeTree\nPARTITION BY toYYYYMM(event_date)   -- 按月分区\nORDER BY (event_date, user_id)      -- 排序键，决定存储顺序\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>核心机制\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>分区\u003C\u002Fstrong>：按分区存储，查询可裁剪跳过无关分区；\u003C\u002Fli>\n\u003Cli>\u003Cstrong>排序键\u003C\u002Fstrong>：数据按 ORDER BY 列有序存储，等于天然索引；\u003C\u002Fli>\n\u003Cli>\u003Cstrong>后台合并\u003C\u002Fstrong>：小片段后台异步合并成大片段，写入即可查。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>三、实战查询：从基础到进阶\u003C\u002Fh2>\n\u003Ch3>3.1 基础聚合\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 按日统计营收\nSELECT event_date, sum(amount) AS revenue\nFROM events\nWHERE event_date &gt;= '2026-10-01'\nGROUP BY event_date\nORDER BY event_date;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>3.2 常用函数\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 去重计数（近似，速度快）\nSELECT uniqExact(user_id) FROM events WHERE event_date = '2026-10-20';\n\n-- 分位数（P95 延迟）\nSELECT quantile(0.95)(latency_ms) FROM requests WHERE date = today();\n\n-- 漏斗\u002F路径分析\nSELECT\n    level,\n    count() AS users\nFROM (\n    SELECT user_id,\n           multiIf(\n               has(groupArray(event_type), 'view') AND has(groupArray(event_type), 'click'), 2,\n               has(groupArray(event_type), 'view'), 1, 0) AS level\n    FROM events GROUP BY user_id\n)\nGROUP BY level;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>3.3 物化视图：预聚合加速\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">CREATE MATERIALIZED VIEW daily_stats\nENGINE = SummingMergeTree\nORDER BY (event_date, event_type)\nAS\nSELECT\n    toDate(time) AS event_date,\n    event_type,\n    count() AS cnt,\n    sum(amount) AS total\nFROM events\nGROUP BY event_date, event_type;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>价值\u003C\u002Fstrong>：写入时实时增量聚合，查询直接读预聚合结果，亿级明细上的秒级统计不再依赖全量扫描。\u003C\u002Fp>\n\u003Ch2>四、写入与更新：理解 CH 的数据模型\u003C\u002Fh2>\n\u003Ch3>4.1 批量写入\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 批量 INSERT 是推荐方式\nINSERT INTO events VALUES\n  ('2026-10-20', 'view', 1, 10.0),\n  ('2026-10-20', 'click', 1, 0.0);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cpre>\u003Ccode class=\"language-go\">\u002F\u002F Go 侧批量写入\nbatch := ch.Conn.Batch(&quot;INSERT INTO events VALUES (?, ?, ?, ?)&quot;)\nfor _, e := range events {\n    batch.Append(e.Date, e.Type, e.UserID, e.Amount)\n}\nbatch.Send()\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>4.2 更新与删除（Mutation）\u003C\u002Fh3>\n\u003Cp>CH 的 UPDATE\u002FDELETE 是&quot;异步重写&quot;操作，开销大：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">ALTER TABLE events DELETE WHERE event_date &lt; '2026-01-01';\nALTER TABLE events UPDATE amount = 0 WHERE event_type = 'refund';\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>实践\u003C\u002Fstrong>：高频更新场景不适合 CH；数据修正用&quot;插入新版本 + 查询取最新&quot;的幂等思路更合适。\u003C\u002Fp>\n\u003Ch2>五、分布式表：从单机到集群\u003C\u002Fh2>\n\u003Ch3>5.1 集群与分布式表\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-xml\">&lt;!-- config.xml 配置集群 --&gt;\n&lt;remote_servers&gt;\n    &lt;my_cluster&gt;\n        &lt;shard&gt;&lt;replica&gt;&lt;host&gt;ch-1&lt;\u002Fhost&gt;&lt;port&gt;9000&lt;\u002Fport&gt;&lt;\u002Freplica&gt;&lt;\u002Fshard&gt;\n        &lt;shard&gt;&lt;replica&gt;&lt;host&gt;ch-2&lt;\u002Fhost&gt;&lt;port&gt;9000&lt;\u002Fport&gt;&lt;\u002Freplica&gt;&lt;\u002Fshard&gt;\n    &lt;\u002Fmy_cluster&gt;\n&lt;\u002Fremote_servers&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 本地表 + 分布式表\nCREATE TABLE events_local ... ENGINE = MergeTree ...;\nCREATE TABLE events AS events_local\nENGINE = Distributed(my_cluster, default, events_local, rand());\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>应用只写分布式表，数据自动按分片路由，查询自动聚合各分片结果。\u003C\u002Fp>\n\u003Ch3>5.2 副本：ReplicatedMergeTree\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">CREATE TABLE events\nENGINE = ReplicatedMergeTree('\u002Fclickhouse\u002Ftables\u002F{shard}\u002Fevents', '{replica}')\nPARTITION BY toYYYYMM(event_date)\nORDER BY (event_date, user_id);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Replicated 引擎基于 ZooKeeper\u002FClickHouse Keeper 自动同步副本，任一节点故障数据不丢。\u003C\u002Fp>\n\u003Ch2>六、常用运维与监控\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-bash\">clickhouse-client --query &quot;SELECT * FROM system.parts WHERE active=1&quot;   # 查看分区\nclickhouse-client --query &quot;SELECT * FROM system.mutations&quot;              # 查看后台任务\nclickhouse-client --query &quot;SHOW PROCESSLIST&quot;                            # 查看运行中查询\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>监控指标\u003C\u002Fstrong>：查询延迟 P99、每秒查询数、磁盘 IO、后台 merge 队列长度。\u003C\u002Fp>\n\u003Ch2>七、适用边界与选型\u003C\u002Fh2>\n\u003Cp>| 场景 | 推荐 |\n|------|------|\n| 亿级日志、埋点分析、漏斗转化 | ClickHouse |\n| 在线交易、强一致性 OLTP | MySQL\u002FPostgreSQL（不是 CH） |\n| 多维报表、即席查询 | ClickHouse |\n| 秒级实时流计算 | 配合 Kafka + 物化视图 |\u003C\u002Fp>\n\u003Cp>\u003Cstrong>边界\u003C\u002Fstrong>：CH 不适合高频点查（按主键随机查一行）与小事务更新，那是 KV\u002FOLTP 的领域。\u003C\u002Fp>\n\u003Ch2>结语\u003C\u002Fh2>\n\u003Cp>ClickHouse 的价值一句话概括：\u003Cstrong>把&quot;等不起的分析&quot;变成&quot;等得起的分析&quot;\u003C\u002Fstrong>。列式存储 + 向量化执行 + 预聚合物化视图，三板斧下来，亿级数据的聚合统计可以从分钟级压到毫秒级。落地建议：先从最痛的报表\u002F日志场景开始，用 MergeTree + 物化视图跑通一条真实链路，再逐步把分析查询从 MySQL 迁移过来——你会发现，分析型需求终于不用再&quot;砍需求&quot;了。\u003C\u002Fp>\n","从列式存储原理、MergeTree 表引擎到 SQL 分析实战与集群高可用，掌握海量数据实时分析的 ClickHouse 方案。",2,39834.28824250944,{"id":8,"created_at":21,"updated_at":22,"code":23,"username":24,"display_name":25,"role":26,"avatar_url":27,"is_active":17,"is_banned":16,"follower_count":28,"following_count":18,"last_login_at":29}]