[{"data":1,"prerenderedAt":72},["ShallowReactive",2],{"article-138":3},{"code":4,"msg":5,"data":6,"count":14},200,"查询成功",{"id":7,"title":8,"keywords":9,"description":10,"category_id":11,"content":12,"body_html":13,"thumb_up":14,"clicks":15,"sort":14,"remark":13,"status":16,"is_open":16,"is_deleted":14,"is_top":16,"is_recommend":16,"create_time":17,"update_time":18,"image_id":19,"url":13,"member_id":14,"cate_name":20,"prev":21,"next":24,"tags":27,"words":43,"read_time":44,"comments":14,"cover":45,"relevant":46},138,"MySQL 高级用法完全指南：从窗口函数到性能调优的实战手册","MySQL高级用法, 窗口函数, CTE递归查询, 索引优化, EXPLAIN, 性能调优","MySQL 是全球最流行的开源关系型数据库之一，但大多数开发者只用到其功能的冰山一角。随着 MySQL 8.0 的普及，窗口函数、CTE 递归查询、JSON_TABLE 等新特性已经成熟可用，配合合理的索引设计和查询优化策略，MySQL 完全能够支撑中等规模业务的全部数据需求。本文系统梳理 MySQL 8.x 时代的高级用法，通过大量实战代码演示，助你从 CRUD 开发者进阶为数据库高手。",6,"## 一、查询进阶：玩转复杂数据操作\n\n### 1.1 窗口函数（MySQL 8.0+）—— SQL 的分析利器\n\n窗口函数是 MySQL 8.0 引入的重磅特性。与 `GROUP BY` 的区别在于，它**不会收缩数据集**，而是在每一行基础上返回一个聚合值，同时保留原始行的完整信息。\n\n**语法结构：**\n\n```sql\n函数名() OVER (\n    [PARTITION BY 列1, 列2, ...]  -- 分区（类似 GROUP BY 的分组）\n    [ORDER BY 列 [ASC|DESC], ...] -- 区内排序\n    [ROWS BETWEEN ... AND ...]    -- 帧窗口（物理范围）\n)\n```\n\n#### 案例一：员工薪资排名与部门对比\n\n```sql\n-- 创建演示数据\nCREATE TABLE employees (\n    id          INT PRIMARY KEY AUTO_INCREMENT,\n    name        VARCHAR(50),\n    department  VARCHAR(50),\n    salary      DECIMAL(10,2),\n    hire_date   DATE\n);\n\nINSERT INTO employees (name, department, salary, hire_date) VALUES\n('张三', '研发部', 15000, '2020-01-15'),\n('李四', '研发部', 18000, '2019-03-20'),\n('王五', '研发部', 12000, '2021-06-10'),\n('赵六', '销售部', 9000,  '2020-08-01'),\n('钱七', '销售部', 11000, '2018-12-05'),\n('孙八', '销售部', 8500,  '2022-02-14'),\n('周九', '人事部', 8000,  '2021-09-01'),\n('吴十', '人事部', 9500,  '2019-07-20');\n```\n\n```sql\n-- 完整窗口函数示例：排名、占比、累计、环比\nSELECT\n    name,\n    department,\n    salary,\n    hire_date,\n\n    -- ① 区内排名（三种排名方式对比）\n    RANK()       OVER (PARTITION BY department ORDER BY salary DESC) AS rank_normal,\n    DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rank_dense,\n    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num,\n\n    -- ② 区内薪资占比：该员工薪资 \u002F 部门薪资总和\n    ROUND(\n        salary \u002F SUM(salary) OVER (PARTITION BY department) * 100, 2\n    ) AS salary_pct_in_dept,\n\n    -- ③ 区内最高、最低薪资\n    MAX(salary) OVER (PARTITION BY department) AS dept_max_salary,\n    MIN(salary) OVER (PARTITION BY department) AS dept_min_salary,\n\n    -- ④ 区内薪资平均值\n    ROUND(AVG(salary) OVER (PARTITION BY department), 2) AS dept_avg_salary,\n\n    -- ⑤ 全公司排名（不分部门）\n    RANK() OVER (ORDER BY salary DESC) AS rank_all,\n\n    -- ⑥ 累计薪资（按入职日期顺序）\n    SUM(salary) OVER (ORDER BY hire_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total_salary\n\nFROM employees\nORDER BY department, salary DESC;\n```\n\n**输出示例（研发部部分）：**\n\n| name | department | salary | rank_dense | salary_pct_in_dept | dept_avg_salary |\n|------|------------|--------|------------|--------------------|-----------------|\n| 李四 | 研发部 | 18000 | 1 | 40.91% | 15000 |\n| 张三 | 研发部 | 15000 | 2 | 34.09% | 15000 |\n| 王五 | 研发部 | 12000 | 3 | 27.27% | 15000 |\n\n#### 案例二：环比与同比计算（LAG \u002F LEAD）\n\n```sql\n-- 场景：电商订单，按月统计并计算环比增长\nCREATE TABLE monthly_orders (\n    month       DATE PRIMARY KEY,  -- 每月1日\n    order_count INT,\n    revenue     DECIMAL(12,2)\n);\n\nINSERT INTO monthly_orders (month, order_count, revenue) VALUES\n('2024-01-01', 1200, 50000),\n('2024-02-01', 980,  42000),\n('2024-03-01', 1350, 61000),\n('2024-04-01', 1100, 48000),\n('2024-05-01', 1500, 72000),\n('2024-06-01', 1650, 79000);\n\n-- 环比计算：与上月对比\nSELECT\n    month,\n    order_count,\n    revenue,\n\n    -- 上月订单数\n    LAG(order_count, 1) OVER (ORDER BY month) AS prev_month_orders,\n    -- 下月订单数\n    LEAD(order_count, 1) OVER (ORDER BY month) AS next_month_orders,\n\n    -- 环比增长率：(本月-上月)\u002F上月 * 100\n    CONCAT(\n        ROUND(\n            (order_count - LAG(order_count,1) OVER (ORDER BY month))\n            \u002F LAG(order_count,1) OVER (ORDER BY month) * 100, 2\n        ), '%'\n    ) AS mom_growth,\n\n    -- 相比三个月前的变化\n    ROUND(\n        (order_count - LAG(order_count,3) OVER (ORDER BY month))\n        \u002F LAG(order_count,3) OVER (ORDER BY month) * 100, 2\n    ) AS mom3_growth_pct\n\nFROM monthly_orders;\n```\n\n#### 案例三：帧窗口（ROWS BETWEEN）—— 移动平均\n\n```sql\n-- 场景：计算最近3个月的滚动平均订单量\nSELECT\n    month,\n    order_count,\n\n    -- 移动平均：本行及前2行的均值\n    ROUND(\n        AVG(order_count) OVER (\n            ORDER BY month\n            ROWS BETWEEN 2 PRECEDING AND CURRENT ROW\n        ), 2\n    ) AS rolling_avg_3month,\n\n    -- 本行及前后各1行（对称窗口）\n    ROUND(\n        AVG(order_count) OVER (\n            ORDER BY month\n            ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING\n        ), 2\n    ) AS rolling_avg_centered\n\nFROM monthly_orders;\n```\n\n---\n\n### 1.2 公用表表达式（CTE）\n\nCTE（Common Table Expression）用 `WITH` 语句定义临时命名结果集，比子查询语法更清晰，支持递归调用。\n\n#### 案例一：多层级联查询重构\n\n```sql\n-- ❌ 子查询地狱（深层嵌套，难以维护）\nSELECT *\nFROM (\n    SELECT *, region,\n        (SELECT SUM(sales) FROM orders o2 WHERE o2.region = o1.region) AS region_total\n    FROM orders o1\n    WHERE status = 'completed'\n) t1\nWHERE region_total > 100000;\n\n-- ✅ CTE 版本：逻辑清晰，可逐步调试\nWITH\nactive_orders AS (\n    SELECT * FROM orders WHERE status = 'completed'\n),\nregional_totals AS (\n    SELECT region, SUM(sales) AS total_sales\n    FROM active_orders\n    GROUP BY region\n),\ntop_regions AS (\n    SELECT region FROM regional_totals WHERE total_sales > 100000\n)\nSELECT o.*\nFROM active_orders o\nINNER JOIN top_regions tr ON o.region = tr.region\nORDER BY o.sales DESC;\n```\n\n#### 案例二：递归 CTE —— 树形结构查询\n\n**场景**：查询公司组织架构，从 CEO 到每位员工。\n\n```sql\nCREATE TABLE org_chart (\n    id         INT PRIMARY KEY,\n    name       VARCHAR(50),\n    title      VARCHAR(50),\n    manager_id INT  -- 上级ID，CEO 为 NULL\n);\n\nINSERT INTO org_chart VALUES\n(1, '陈总裁', 'CEO',              NULL),\n(2, '张副总', '技术副总裁',         1),\n(3, '刘副总', '运营副总裁',         1),\n(4, '王总监', '后端研发总监',       2),\n(5, '赵总监', '前端研发总监',       2),\n(6, '孙经理', 'PHP开发组经理',     4),\n(7, '周工',   'PHP高级工程师',      6),\n(8, '吴工',   'PHP中级工程师',      6),\n(9, '郑工',   '前端开发工程师',     5);\n```\n\n```sql\n-- 递归 CTE：自顶向下遍历整棵树\nWITH RECURSIVE org_tree AS (\n    -- ① 锚点：CEO（根节点）\n    SELECT id, name, title, manager_id, 1 AS level, name AS path\n    FROM org_chart\n    WHERE manager_id IS NULL\n\n    UNION ALL\n\n    -- ② 递归：不断找直接下属\n    SELECT\n        o.id, o.name, o.title, o.manager_id,\n        ot.level + 1,\n        CONCAT(ot.path, ' → ', o.name) AS path  -- 展示完整汇报链\n    FROM org_chart o\n    INNER JOIN org_tree ot ON o.manager_id = ot.id\n)\nSELECT\n    REPEAT('    ', level - 1) AS indent,  -- 缩进：层级越深，空白越多\n    level,\n    name,\n    title,\n    path AS reporting_chain\nFROM org_tree\nORDER BY path;\n```\n\n**输出效果：**\n\n```\nlevel | name  | title         | reporting_chain\n1     | 陈总裁 | CEO           | 陈总裁\n2     | 张副总 | 技术副总裁     | 陈总裁 → 张副总\n3     | 王总监 | 后端研发总监   | 陈总裁 → 张副总 → 王总监\n4     | 孙经理 | PHP开发组经理  | 陈总裁 → 张副总 → 王总监 → 孙经理\n5     | 周工   | PHP高级工程师  | 陈总裁 → 张副总 → 王总监 → 孙经理 → 周工\n...\n```\n\n#### 案例三：递归 CTE —— 分类树（评论嵌套结构）\n\n```sql\nCREATE TABLE comments (\n    id         INT PRIMARY KEY,\n    article_id INT,\n    content    VARCHAR(500),\n    parent_id  INT  -- 回复的上一级ID，顶级评论为 NULL\n);\n\n-- 查询评论树（将嵌套结构展开为扁平列表，含深度）\nWITH RECURSIVE comment_tree AS (\n    -- 锚点：顶级评论\n    SELECT id, article_id, content, parent_id, 0 AS depth, CAST(id AS CHAR(200)) AS path\n    FROM comments\n    WHERE parent_id IS NULL AND article_id = 1\n\n    UNION ALL\n\n    -- 递归：回复\n    SELECT\n        c.id, c.article_id, c.content, c.parent_id,\n        ct.depth + 1,\n        CONCAT(ct.path, ',', c.id) AS path\n    FROM comments c\n    INNER JOIN comment_tree ct ON c.parent_id = ct.id\n)\nSELECT depth, id, content, path FROM comment_tree ORDER BY path;\n```\n\n---\n\n### 1.3 复杂 JOIN 操作\n\n#### 自连接（Self JOIN）—— 处理层级与对比\n\n```sql\n-- 场景一：查询每位员工及其直接主管\nSELECT\n    e1.name  AS employee,\n    e1.title AS emp_title,\n    e2.name  AS manager,\n    e2.title AS mgr_title\nFROM employees e1\nLEFT JOIN employees e2 ON e1.manager_id = e2.id;\n\n-- 场景二：查询同一部门内薪资高于平均值的员工\nSELECT\n    e1.name     AS employee,\n    e1.department,\n    e1.salary,\n    ROUND(AVG(e2.salary), 2) AS dept_avg_salary\nFROM employees e1\nINNER JOIN employees e2 ON e1.department = e2.department\nGROUP BY e1.id, e1.name, e1.department, e1.salary\nHAVING e1.salary > AVG(e2.salary);\n\n-- 场景三：查询连续日期缺口（数据补齐）\n-- 场景：用户记录了有数据的日期，但报表需要展示每一天\nCREATE TABLE sales_log (\n    sale_date DATE,\n    amount    DECIMAL(10,2)\n);\n\n-- 生成连续日期序列并左连接原始数据（日期缺口一目了然）\nSELECT\n    d.date_seq  AS missing_date,\n    s.amount\nFROM (\n    SELECT DATE_SUB(CURDATE(), INTERVAL n DAY) AS date_seq\n    FROM (\n        SELECT @row := @row + 1 AS n\n        FROM any_table, (SELECT @row := -6) r  -- 生成最近7天\n        LIMIT 7\n    ) t\n) d\nLEFT JOIN sales_log s ON s.sale_date = d.date_seq\nORDER BY d.date_seq;\n```\n\n---\n\n## 二、存储过程与函数：把业务逻辑下沉到数据库\n\n### 2.1 为什么使用存储过程？\n\n| 优势 | 说明 |\n|------|------|\n| **减少网络往返** | 业务逻辑在数据库端执行，无需多次往返传输数据 |\n| **预编译缓存** | 语句一次编译、反复调用，执行计划被 MySQL 缓存 |\n| **事务封装** | 多步写操作原子化，任何一步失败自动回滚 |\n| **安全** | 应用层只调用存储过程名称，无法直接操作底层表字段 |\n\n### 2.2 完整案例：电商订单创建存储过程\n\n**需求**：创建订单时，同时扣减库存、记录日志。任何一步失败则全部回滚。\n\n```sql\n-- ① 创建库存表和订单表\nCREATE TABLE products (\n    product_id  INT PRIMARY KEY,\n    name        VARCHAR(100),\n    price       DECIMAL(10,2),\n    stock       INT DEFAULT 0\n);\n\nCREATE TABLE orders (\n    order_id    INT PRIMARY KEY AUTO_INCREMENT,\n    customer_id INT,\n    product_id  INT,\n    quantity    INT,\n    total_price DECIMAL(10,2),\n    status      VARCHAR(20) DEFAULT 'pending',\n    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE TABLE order_logs (\n    log_id      INT PRIMARY KEY AUTO_INCREMENT,\n    order_id    INT,\n    action      VARCHAR(50),\n    message     VARCHAR(200),\n    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\nINSERT INTO products VALUES (1, 'iPhone 16', 7999.00, 50);\n```\n\n```sql\n-- ② 存储过程：带错误处理和日志记录\nDELIMITER $$\n\nCREATE PROCEDURE sp_create_order(\n    IN  p_customer_id INT,\n    IN  p_product_id  INT,\n    IN  p_quantity    INT,\n    OUT p_order_id    INT,\n    OUT p_status      VARCHAR(100)\n)\nBEGIN\n    DECLARE v_price     DECIMAL(10,2);\n    DECLARE v_stock     INT;\n    DECLARE v_order_id  INT DEFAULT 0;\n    DECLARE v_error     VARCHAR(200) DEFAULT '';\n\n    -- 声明异常处理器\n    DECLARE EXIT HANDLER FOR SQLEXCEPTION\n    BEGIN\n        ROLLBACK;\n        GET DIAGNOSTICS CONDITION 1 v_error = MESSAGE_TEXT;\n        SET p_status = CONCAT('执行失败: ', v_error);\n    END;\n\n    -- 开始事务\n    START TRANSACTION;\n\n    -- 锁定行（防止并发超卖）\n    SELECT price, stock INTO v_price, v_stock\n    FROM products\n    WHERE product_id = p_product_id\n    FOR UPDATE;  -- 关键：行级锁，并发安全\n\n    -- 库存校验\n    IF v_stock \u003C p_quantity THEN\n        SET p_status = CONCAT('库存不足：当前 ', v_stock, '，需求 ', p_quantity);\n        SET p_order_id = 0;\n        ROLLBACK;\n    ELSE\n        -- 创建订单\n        INSERT INTO orders (customer_id, product_id, quantity, total_price, status)\n        VALUES (p_customer_id, p_product_id, p_quantity, v_price * p_quantity, 'confirmed');\n\n        SET v_order_id = LAST_INSERT_ID();\n\n        -- 扣减库存\n        UPDATE products\n        SET stock = stock - p_quantity\n        WHERE product_id = p_product_id;\n\n        -- 记录操作日志\n        INSERT INTO order_logs (order_id, action, message)\n        VALUES (v_order_id, 'CREATE_ORDER',\n                CONCAT('创建订单，商品ID=', p_product_id, '，数量=', p_quantity));\n\n        COMMIT;\n\n        SET p_order_id = v_order_id;\n        SET p_status  = '订单创建成功';\n    END IF;\nEND$$\n\nDELIMITER ;\n```\n\n```sql\n-- ③ 调用存储过程\nCALL sp_create_order(101, 1, 3, @order_id, @msg);\nSELECT @order_id AS 订单ID, @msg AS 状态;\n\n-- ④ 查看执行结果\nSELECT * FROM orders WHERE order_id = @order_id;\nSELECT * FROM order_logs WHERE order_id = @order_id;\nSELECT stock FROM products WHERE product_id = 1;  -- 库存应从50变为47\n```\n\n### 2.3 存储函数：在 SELECT 中直接调用\n\n```sql\n-- 场景：根据订单金额计算会员折扣价\nDELIMITER $$\n\nCREATE FUNCTION fn_calc_discount(\n    p_amount   DECIMAL(10,2),\n    p_level    TINYINT   -- 1=普通 2=银卡 3=金卡 4=钻石\n)\nRETURNS DECIMAL(10,2)\nDETERMINISTIC\nBEGIN\n    DECLARE v_discount_rate DECIMAL(3,2);\n\n    CASE p_level\n        WHEN 1 THEN SET v_discount_rate = 1.00;     -- 普通：无折扣\n        WHEN 2 THEN SET v_discount_rate = 0.95;     -- 银卡：95折\n        WHEN 3 THEN SET v_discount_rate = 0.88;     -- 金卡：88折\n        WHEN 4 THEN SET v_discount_rate = 0.80;     -- 钻石：8折\n        ELSE         SET v_discount_rate = 1.00;\n    END CASE;\n\n    RETURN ROUND(p_amount * v_discount_rate, 2);\nEND$$\n\nDELIMITER ;\n```\n\n```sql\n-- 在查询中直接使用\nSELECT\n    order_id,\n    total_price AS 原价,\n    customer_level,\n    fn_calc_discount(total_price, customer_level) AS 折后价,\n    total_price - fn_calc_discount(total_price, customer_level) AS 节省金额\nFROM orders\nJOIN customers USING (customer_id);\n```\n\n### 2.4 动态 SQL 与条件执行\n\n```sql\n-- 场景：根据不同条件动态构建查询\nDELIMITER $$\n\nCREATE PROCEDURE sp_search_orders(\n    IN p_status   VARCHAR(20),   -- NULL 表示不限\n    IN p_min_amt  DECIMAL(10,2),\n    IN p_max_amt  DECIMAL(10,2),\n    IN p_limit    INT\n)\nBEGIN\n    SET @sql = 'SELECT * FROM orders WHERE 1=1';\n\n    IF p_status IS NOT NULL THEN\n        SET @sql = CONCAT(@sql, \" AND status = '\", p_status, \"'\");\n    END IF;\n\n    IF p_min_amt IS NOT NULL THEN\n        SET @sql = CONCAT(@sql, ' AND total_price >= ', p_min_amt);\n    END IF;\n\n    IF p_max_amt IS NOT NULL THEN\n        SET @sql = CONCAT(@sql, ' AND total_price \u003C= ', p_max_amt);\n    END IF;\n\n    SET @sql = CONCAT(@sql, ' ORDER BY created_at DESC LIMIT ', p_limit);\n\n    -- 预处理并执行动态 SQL\n    PREPARE stmt FROM @sql;\n    EXECUTE stmt;\n    DEALLOCATE PREPARE stmt;\nEND$$\n\nDELIMITER ;\n\n-- 调用：查找所有待支付订单，最多返回10条\nCALL sp_search_orders('pending', NULL, NULL, 10);\n```\n\n---\n\n## 三、性能优化：让查询飞起来\n\n### 3.1 索引设计：六大核心原则\n\n#### 原则一：最左前缀原则\n\n复合索引 `(A, B, C)` 会按从左到右的顺序生效：\n\n```sql\nCREATE INDEX idx_user ON users (status, created_at, email);\n\n-- ✅ 命中索引的场景（必须包含最左列 status）\nSELECT * FROM users WHERE status = 'active';                    -- 命中 A\nSELECT * FROM users WHERE status = 'active' AND created_at > '2025-01-01';  -- 命中 A+B\nSELECT * FROM users WHERE status = 'active' AND created_at > '2025-01-01' AND email = 'x@y.com'; -- 命中 A+B+C\n\n-- ❌ 不命中索引（跳过了最左列 A）\nSELECT * FROM users WHERE created_at > '2025-01-01';              -- 不命中\nSELECT * FROM users WHERE email = 'x@y.com';                      -- 不命中\n```\n\n#### 原则二：覆盖索引（最理想的索引使用方式）\n\n当查询的所有字段都包含在索引中时，MySQL 不需要回表（访问主键索引），直接返回结果。\n\n```sql\n-- 常见新闻列表页：只需 id、title、created_at、status\n-- 查询只需这4个字段，idx_news_cover 覆盖全部，无需回表\nCREATE INDEX idx_news_cover ON news (status, created_at DESC, id, title);\n\nSELECT id, title, created_at, status\nFROM news\nWHERE status = 'published'\nORDER BY created_at DESC\nLIMIT 20;\n```\n\n#### 原则三：前缀索引（大文本字段优化）\n\n对 VARCHAR(255) 的长字段建立完整 B+Tree 成本过高，使用前缀索引：\n\n```sql\n-- 前5个字符的索引，节省空间，适用于以该字段做 LIKE 前缀查询\nCREATE INDEX idx_email_prefix ON users (email(5));\n\n-- 验证前缀长度是否够用（区分度测试）\nSELECT\n    COUNT(DISTINCT LEFT(email, 5)) \u002F COUNT(*) AS selectivity_5,\n    COUNT(DISTINCT LEFT(email, 8)) \u002F COUNT(*) AS selectivity_8,\n    COUNT(DISTINCT LEFT(email, 10)) \u002F COUNT(*) AS selectivity_10\nFROM users;\n-- 区分度越高（前缀越长），查询越精确\n```\n\n#### 原则四：联合索引 vs 多个单列索引\n\n```sql\n-- ❌ 低效：为每个字段单独建索引，查询时 MySQL 每次只选一个\nINDEX idx_status  ON orders(status);\nINDEX idx_created ON orders(created_at);\nINDEX idx_customer ON orders(customer_id);\n\n-- ✅ 高效：一个复合索引覆盖所有查询条件\nINDEX idx_order_search ON orders(customer_id, status, created_at);\n\n-- 这条查询三个字段都在索引中，MySQL 一次定位，无需回表\nSELECT customer_id, status, created_at\nFROM orders\nWHERE customer_id = 101\n  AND status = 'shipped'\n  AND created_at BETWEEN '2025-01-01' AND '2025-03-31';\n```\n\n#### 原则五：索引列上避免使用函数\n\n```sql\n-- ❌ 函数包裹列，无法使用索引（全表扫描）\nSELECT * FROM orders\nWHERE YEAR(created_at) = 2025\n  AND MONTH(created_at) = 3;\n\n-- ✅ 范围查询，保持列独立，索引正常命中\nSELECT * FROM orders\nWHERE created_at >= '2025-03-01'\n  AND created_at \u003C  '2025-04-01';\n```\n\n#### 原则六：高区分度列优先\n\n```sql\n-- ❌ 性别（2种）、状态（3种）放复合索引最左 → 区分度极低，索引意义不大\nINDEX idx_gender_status ON users(gender, status, created_at);\n\n-- ✅ 交换顺序：高区分度列（status）放前面\nINDEX idx_status ON users(status, created_at);\n```\n\n### 3.2 EXPLAIN 深度解析\n\n```sql\nEXPLAIN ANALYZE  -- MySQL 8.0+，实际执行并返回估算 vs 实际成本\nSELECT\n    o.order_id,\n    u.username,\n    p.product_name,\n    o.quantity,\n    o.total_price\nFROM orders o\nINNER JOIN users u  ON o.customer_id = u.id\nINNER JOIN products p ON o.product_id = p.id\nWHERE o.status = 'shipped'\n  AND o.created_at >= '2025-01-01';\n```\n\n**关键字段解读：**\n\n| 字段 | 常见值 | 含义 |\n|------|--------|------|\n| `type` | `const` > `ref` > `range` > `index` > `ALL` | 访问方式，`ALL` 为全表扫描需优化 |\n| `possible_keys` | 可选索引列表 | MySQL 考虑了哪些索引 |\n| `key` | 实际使用索引名 | `NULL` 表示走了全表扫描 |\n| `key_len` | 索引使用字节数 | 越大说明索引列使用得越多 |\n| `rows` | 预估扫描行数 | 越少越好 |\n| `filtered` | 过滤后剩余比例 | 0-100%，越高越精确 |\n| `extra` | 详细策略 | 见下方说明 |\n\n**Extra 字段常见值及含义：**\n\n| Extra 值 | 说明 | 优化建议 |\n|---------|------|---------|\n| `Using index` | 覆盖索引，无需回表 | ✅ 最优 |\n| `Using index condition` | 索引条件下推（ICP） | ✅ 正常 |\n| `Using where` | 服务层过滤 | 检查是否可前推到索引 |\n| `Using filesort` | 额外排序（内存或磁盘） | 添加对应 ORDER BY 列的索引 |\n| `Using temporary` | 使用临时表 | 考虑优化查询结构或加索引 |\n| `Using MRR` | 范围读取优化 | ✅ 正常 |\n\n### 3.3 慢查询优化实战\n\n```sql\n-- 开启慢查询日志（生产环境）\nSET GLOBAL slow_query_log = 'ON';\nSET GLOBAL long_query_time = 1;  -- 超过1秒记录\nSET GLOBAL slow_query_log_file = '\u002Fvar\u002Flog\u002Fmysql\u002Fslow.log';\n\n-- 查看慢查询\nSHOW VARIABLES LIKE 'slow_query%';\nSHOW VARIABLES LIKE 'long_query_time';\n```\n\n```sql\n-- 案例：优化前慢查询（执行时间 3.2s）\n-- 原 SQL\nSELECT *\nFROM orders o\nJOIN users u ON o.customer_id = u.id\nWHERE o.status = 'cancelled'\n  AND o.created_at >= '2025-01-01'\nORDER BY o.created_at DESC\nLIMIT 100;\n\n-- EXPLAIN 分析：\n-- type=ALL（全表扫描）, rows=500000+, extra=Using filesort\n\n-- ✅ 优化后（执行时间 0.08s）\n-- 1. 添加复合索引覆盖 WHERE + ORDER BY\nALTER TABLE orders ADD INDEX idx_cancel_time (status, created_at DESC);\n\n-- 2. 覆盖索引，直接返回所需字段，无需回表\nSELECT o.order_id, o.customer_id, o.total_price, o.created_at,\n       u.username, u.email\nFROM orders o\nINNER JOIN users u ON o.customer_id = u.id\nWHERE o.status = 'cancelled'\n  AND o.created_at >= '2025-01-01'\nORDER BY o.created_at DESC\nLIMIT 100;\n\n-- 验证：type=range, key=idx_cancel_time, rows=120, extra=Using index\n```\n\n### 3.4 子查询优化策略\n\n```sql\n-- ❌ 危险写法：IN 子查询在大表上性能差\nSELECT * FROM orders\nWHERE customer_id IN (\n    SELECT id FROM customers\n    WHERE region = '华东' AND level >= 3\n);\n-- MySQL 对内表的外键列无索引时，会将子查询物化为临时表，全表扫描\n\n-- ✅ 方案一：EXISTS（相关子查询，找到第一条即停）\nSELECT * FROM orders o\nWHERE EXISTS (\n    SELECT 1 FROM customers c\n    WHERE c.id = o.customer_id\n      AND c.region = '华东'\n      AND c.level >= 3\n);\n\n-- ✅ 方案二：JOIN（MySQL 优化器处理更灵活）\nSELECT DISTINCT o.*\nFROM orders o\nINNER JOIN customers c ON o.customer_id = c.id\nWHERE c.region = '华东' AND c.level >= 3;\n\n-- ✅ 方案三：派生表（子查询先执行，结果集通常较小）\nSELECT o.*\nFROM orders o\nINNER JOIN (\n    SELECT id FROM customers\n    WHERE region = '华东' AND level >= 3\n) c ON o.customer_id = c.id;\n```\n\n**子查询优化核心规则：**\n- `EXISTS` 优于 `IN`（外大内小：外层结果少、内层子查询快）\n- `IN` 优于 `EXISTS`（外小内大：内层结果少、扫描快）\n- 避免相关子查询（子查询引用外层列，每次外层行都重跑）\n- 嵌套超过 2 层 → 改写为 JOIN\n\n---\n\n## 四、数据类型与字符集最佳实践\n\n### 4.1 字符集选型与配置\n\n```sql\n-- 生产环境推荐配置\nCREATE DATABASE production_db\n    DEFAULT CHARACTER SET utf8mb4          -- 完整 UTF-8，支持 emoji 和所有 Unicode 字符\n    DEFAULT COLLATE utf8mb4_unicode_ci;    -- Unicode 排序规则，支持多语言自然排序\n```\n\n| 字符集 | 最大存储长度 | Emoji 支持 | 推荐场景 |\n|--------|------------|-----------|---------|\n| `latin1` | 1字节\u002F字符 | ❌ | 纯英文遗留系统 |\n| `utf8` (utf8mb3) | 3字节\u002F字符 | ❌ 部分 | 旧系统，慎用 |\n| `utf8mb4` | 4字节\u002F字符 | ✅ 完整 | **所有新项目必选** |\n| `ascii` | 1字节\u002F字符 | ❌ | 纯 ASCII |\n\n### 4.2 字段类型精确选型\n\n```sql\nCREATE TABLE products (\n    -- 主键：自增 BIGINT 更稳妥（INT 最大 21 亿）\n    id              BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,\n\n    -- 价格：DECIMAL 精确存储（避免浮点误差），VARCHAR 存金额是大忌\n    price           DECIMAL(10,2) NOT NULL DEFAULT 0.00,\n\n    -- 短文本：VARCHAR(255) 够用，CHAR 是定长，空间浪费\n    product_name    VARCHAR(255) NOT NULL,\n\n    -- 长文本：TEXT 不建立索引，如需全文搜索用 FULLTEXT 索引\n    description     TEXT,\n\n    -- 状态：ENUM 语义清晰，数据库层校验合法值\n    status          ENUM('draft','active','offline','deleted') DEFAULT 'draft',\n\n    -- 时间戳：TIMESTAMP 自动更新，比 DATETIME 节省 4 字节\n    created_at      TIMESTAMP   DEFAULT CURRENT_TIMESTAMP,\n    updated_at      TIMESTAMP   DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n\n    -- 地理信息：MySQL 8.0+ 原生空间数据类型\n    location        POINT,\n    delivery_zone   POLYGON,\n\n    -- JSON 数据：MySQL 5.7+ 原生支持，无需单独建表\n    -- 适合灵活扩展字段（如商品规格、用户偏好）\n    specs           JSON,\n\n    -- IP地址：存储为无符号 INT，节省空间，支持排序\n    last_login_ip   INT UNSIGNED\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n```\n\n### 4.3 JSON 类型实战\n\n```sql\n-- 场景：商品规格（尺寸、颜色、重量随时变化，不适合固定列）\nCREATE TABLE products (\n    id    INT PRIMARY KEY AUTO_INCREMENT,\n    name  VARCHAR(255),\n    specs JSON  -- 动态规格，不固定字段结构\n);\n\nINSERT INTO products (name, specs) VALUES ('iPhone 16', '\n{\n    \"colors\": [\"黑色\", \"白色\", \"蓝色\"],\n    \"storage\": [\"128GB\", \"256GB\", \"512GB\"],\n    \"weight\": \"170g\",\n    \"display\": { \"size\": \"6.1英寸\", \"type\": \"OLED\" }\n}\n');\n\n-- JSON 查询函数（MySQL 8.0+）\nSELECT\n    name,\n    JSON_EXTRACT(specs, '$.weight')                      AS weight,\n    JSON_EXTRACT(specs, '$.colors[0]')                   AS first_color,\n    JSON_EXTRACT(specs, '$.display.type')                 AS display_type,\n    JSON_VALUE(specs, '$.display.size')                  AS display_size,  -- 直接返回字符串\n    JSON_KEYS(specs)                                     AS all_keys       -- 返回所有 key 数组\nFROM products;\n\n-- 更新 JSON 字段（局部更新，不影响其他字段）\nUPDATE products\nSET specs = JSON_SET(specs, '$.colors', JSON_ARRAY('黑色', '银色', '金色'))\nWHERE id = 1;\n\n-- 搜索 JSON 数组中的值（索引支持）\nALTER TABLE products ADD INDEX idx_storage ((CAST(specs->'$.storage' AS CHAR(50) ARRAY)));\n```\n\n### 4.4 日期时间函数实战\n\n```sql\n-- ① 基础函数\nSELECT\n    NOW()         AS 当前时间,         -- 2025-04-11 23:21:00\n    CURDATE()     AS 当前日期,         -- 2025-04-11\n    CURTIME()     AS 当前时间,         -- 23:21:00\n    UTC_TIMESTAMP() AS UTC时间;       -- UTC时区\n\n-- ② 日期加减：查询最近 N 天的数据\nSELECT * FROM orders\nWHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)   -- 最近7天\n  AND created_at \u003C= DATE_ADD(NOW(), INTERVAL 1 DAY);   -- 明天23:59前\n\n-- ③ 日期差计算\nSELECT\n    DATEDIFF('2025-12-31', CURDATE())    AS 距离年末天数,\n    TIMESTAMPDIFF(HOUR, created_at, NOW()) AS 订单距今小时数,\n    TIMESTAMPDIFF(DAY, created_at, NOW())  AS 订单距今天数;\n\n-- ④ 日期截断（MySQL 8.0+）\nSELECT\n    created_at,\n    DATE(created_at)                        AS 当天日期,\n    DATE_FORMAT(created_at, '%Y-%m')        AS 年月,\n    DATE_TRUNC('MONTH', created_at)        AS 月初（需 MySQL 8.0.28+）;\n\n-- ⑤ 每月第 N 天 \u002F 每周第 N 天\nSELECT\n    DAYOFWEEK(created_at)  AS 周几(1=周日),\n    DAYOFMONTH(created_at) AS 几号,\n    LAST_DAY(created_at)   AS 月末,\n    DAYNAME(created_at)     AS 星期名称;\n```\n\n---\n\n## 五、安全与运维建议\n\n### 5.1 SQL 注入防护（最核心的安全防线）\n\n```php\n\u003C?php\n\u002F\u002F ❌ 危险：字符串拼接，100% 可被 SQL 注入\n$sql = \"SELECT * FROM users WHERE email = '\" . $_POST['email'] . \"'\";\nmysqli_query($db, $sql);\n\n\u002F\u002F ✅ 安全：预处理语句（PDO）\n$pdo = new PDO('mysql:host=localhost;dbname=shop', 'root', 'password');\n$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);  \u002F\u002F 关闭模拟预编译\n\n$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email LIMIT 1');\n$stmt->execute(['email' => $_POST['email']]);\n$user = $stmt->fetch(PDO::FETCH_ASSOC);\n\n\u002F\u002F ✅ 安全：mysqli 预处理\n$stmt = $mysqli->prepare('SELECT * FROM users WHERE email = ? LIMIT 1');\n$stmt->bind_param('s', $_POST['email']);\n$stmt->execute();\n$result = $stmt->get_result();\n```\n\n### 5.2 最小权限原则\n\n```sql\n-- 创建应用专用账户（只能访问指定数据库和表）\nCREATE USER 'app_reader'@'%' IDENTIFIED BY 'Str0ngP@ss!';\nGRANT SELECT ON shop.products TO 'app_reader'@'%';   -- 只读\n\nCREATE USER 'app_writer'@'%' IDENTIFIED BY 'Str0ngP@ss!';\nGRANT SELECT, INSERT, UPDATE, DELETE ON shop.orders TO 'app_writer'@'%';\nGRANT SELECT, INSERT, UPDATE, DELETE ON shop.products TO 'app_writer'@'%';\nGRANT EXECUTE ON PROCEDURE shop.sp_create_order TO 'app_writer'@'%';  -- 只授权指定存储过程\n\nFLUSH PRIVILEGES;\n\n-- 查看用户权限\nSHOW GRANTS FOR 'app_writer'@'%';\n```\n\n### 5.3 大表 DDL 变更（pt-online-schema-change）\n\n```sql\n-- 场景：为 5000 万行的 orders 表添加索引，直接 ALTER 会锁表\n-- 使用 pt-online-schema-change（Percona Toolkit）在线变更\n\n-- ① 安装 Percona Toolkit\n-- yum install percona-toolkit\n\n-- ② 在线加索引（不锁表，实时同步数据）\npt-online-schema-change \\\n    --alter \"ADD INDEX idx_customer_date (customer_id, created_at)\" \\\n    D=t_shop,t=orders \\\n    --execute \\\n    --charset=utf8mb4 \\\n    --chunk-size=1000 \\\n    --max-load=\"Threads_running=50\" \\\n    --critical-load=\"Threads_running=100\"\n\n-- ③ 原理：\n--    1. 创建新表（新索引结构）\n--    2. 逐步从原表复制数据到新表\n--    3. 同步期间原表的所有写操作通过触发器同步到新表\n--    4. 切换新旧表（毫秒级锁）\n--    5. 删除原表\n```\n\n---\n\n## 结语\n\nMySQL 的高级用法是一个庞大的体系，窗口函数让 SQL 具备了分析能力，CTE 让复杂查询可读性大幅提升，存储过程把业务逻辑安全地沉淀在数据库层，合理的索引设计是性能优化的基石。掌握以上内容，你已经具备了一个中级 DBA 所需的核心技能。\n\n> **推荐学习路径**：\n> 第一阶段：窗口函数 + EXPLAIN 分析（立竿见影，立刻用得上）\n> 第二阶段：CTE 递归查询 + 存储过程（进阶必备）\n> 第三阶段：索引设计原理 + 慢查询优化（系统性提升）\n",null,0,56,1,"2026-04-11 23:31:45","2026-04-12 04:13:32",129,"Mysql",{"id":22,"title":23},137,"2026年PHP技术指南：8.x新特性、安全实践与框架生态全解析",{"id":25,"title":26},139,"Nginx 性能优化实战指南：从基础配置到高并发压测",[28,31,34,37,40],{"id":29,"name":30},26,"MySQL",{"id":32,"name":33},113,"数据库优化",{"id":35,"name":36},114,"SQL进阶",{"id":38,"name":39},115," 索引设计",{"id":41,"name":42},116,"运维实战",14310,35,"https:\u002F\u002Ftp.myong.top\u002Fstorage\u002Farticle\u002F47\u002F7aaf5aeee68d3e4794a17c6d5b2eea.jpg",[47,52,57,62,67],{"id":48,"title":49,"create_time":50,"description":51},90,"Redis常见使用场景","2020-06-17 16:09:42","Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库，并提供多种语言的API。本篇文章，主要介绍利用PHP使用Redis，主要的应用场景。",{"id":53,"title":54,"create_time":55,"description":56},94,"客户端连接MySQL8提示 caching-sha2-password 问题","2020-10-19 10:55:26","在安装mysql8的时候如果选择了密码加密，之后用客户端连接比如navicate，会提示客户端连接caching-sha2-password,是由于客户端不支持这种插件，可以通过如下方式进行修改：",{"id":58,"title":59,"create_time":60,"description":61},87,"MySQL中InnoDB和MyISAM区别","2020-06-12 17:11:21","InnoDB具有事务，支持4个事务隔离级别，回滚，崩溃修复能力和多版本并发的事务安全，包括ACID.如果应用中需要执行大量的INSERT或UPDATE操作，则应该使用InnoDB,这样可以提高多用户并发操作的性能。MyISAM管理非事务表，提供高速存储和检索，以及全文搜索能力，如果应用中需要执行大量的SELECT查询，那么MyISAM是更好的选择。",{"id":63,"title":64,"create_time":65,"description":66},88,"MySQL实现循环插入千万级数据","2020-06-12 17:56:22","对于一些数据量较大的系统，数据库面临的问题除了查询效率低下，还有就是数据入库时间长。特别像报表系统，可能每天花费在数据导入上的时间就会长达几个小时之久。因此，优化数据库插入性能是很有意义的。",{"id":68,"title":69,"create_time":70,"description":71},86,"MySQL查询表结构命令","2020-06-09 11:30:00","###### MySQL查询表结构命令\n\n###### 1、查询表结构\n\n主要显示字段类型主键是否允许为空等\n\n```mysql\nDESC 表名;\n```\n\n结果显示\n\n| Field | Type         | Null | Key  | Default | Extra          |\n| :---- | ------------ | ---- | ---- | ------- | -------------- |\n| id    | int(11)      | NO   | P",1783431647468]