MySQL 高级用法完全指南:从窗口函数到性能调优的实战手册

MySQL 是全球最流行的开源关系型数据库之一,但大多数开发者只用到其功能的冰山一角。随着 MySQL 8.0 的普及,窗口函数、CTE 递归查询、JSON_TABLE 等新特性已经成熟可用,配合合理的索引设计和查询优化策略,MySQL 完全能够支撑中等规模业务的全部数据需求。本文系统梳理 MySQL 8.x 时代的高级用法,通过大量实战代码演示,助你从 CRUD 开发者进阶为数据库高手。

MySQL 高级用法完全指南:从窗口函数到性能调优的实战手册

一、查询进阶:玩转复杂数据操作

1.1 窗口函数(MySQL 8.0+)—— SQL 的分析利器

窗口函数是 MySQL 8.0 引入的重磅特性。与 GROUP BY 的区别在于,它不会收缩数据集,而是在每一行基础上返回一个聚合值,同时保留原始行的完整信息。

语法结构:

函数名() OVER (
    [PARTITION BY1, 列2, ...]  -- 分区(类似 GROUP BY 的分组)
    [ORDER BY 列 [ASC|DESC], ...] -- 区内排序
    [ROWS BETWEEN ... AND ...]    -- 帧窗口(物理范围)
)

案例一:员工薪资排名与部门对比

-- 创建演示数据
CREATE TABLE employees (
    id          INT PRIMARY KEY AUTO_INCREMENT,
    name        VARCHAR(50),
    department  VARCHAR(50),
    salary      DECIMAL(10,2),
    hire_date   DATE
);

INSERT INTO employees (name, department, salary, hire_date) VALUES
('张三', '研发部', 15000, '2020-01-15'),
('李四', '研发部', 18000, '2019-03-20'),
('王五', '研发部', 12000, '2021-06-10'),
('赵六', '销售部', 9000,  '2020-08-01'),
('钱七', '销售部', 11000, '2018-12-05'),
('孙八', '销售部', 8500,  '2022-02-14'),
('周九', '人事部', 8000,  '2021-09-01'),
('吴十', '人事部', 9500,  '2019-07-20');
-- 完整窗口函数示例:排名、占比、累计、环比
SELECT
    name,
    department,
    salary,
    hire_date,

    -- ① 区内排名(三种排名方式对比)
    RANK()       OVER (PARTITION BY department ORDER BY salary DESC) AS rank_normal,
    DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rank_dense,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num,

    -- ② 区内薪资占比:该员工薪资 / 部门薪资总和
    ROUND(
        salary / SUM(salary) OVER (PARTITION BY department) * 100, 2
    ) AS salary_pct_in_dept,

    -- ③ 区内最高、最低薪资
    MAX(salary) OVER (PARTITION BY department) AS dept_max_salary,
    MIN(salary) OVER (PARTITION BY department) AS dept_min_salary,

    -- ④ 区内薪资平均值
    ROUND(AVG(salary) OVER (PARTITION BY department), 2) AS dept_avg_salary,

    -- ⑤ 全公司排名(不分部门)
    RANK() OVER (ORDER BY salary DESC) AS rank_all,

    -- ⑥ 累计薪资(按入职日期顺序)
    SUM(salary) OVER (ORDER BY hire_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total_salary

FROM employees
ORDER BY department, salary DESC;

输出示例(研发部部分):

name department salary rank_dense salary_pct_in_dept dept_avg_salary
李四 研发部 18000 1 40.91% 15000
张三 研发部 15000 2 34.09% 15000
王五 研发部 12000 3 27.27% 15000

案例二:环比与同比计算(LAG / LEAD)

-- 场景:电商订单,按月统计并计算环比增长
CREATE TABLE monthly_orders (
    month       DATE PRIMARY KEY,  -- 每月1日
    order_count INT,
    revenue     DECIMAL(12,2)
);

INSERT INTO monthly_orders (month, order_count, revenue) VALUES
('2024-01-01', 1200, 50000),
('2024-02-01', 980,  42000),
('2024-03-01', 1350, 61000),
('2024-04-01', 1100, 48000),
('2024-05-01', 1500, 72000),
('2024-06-01', 1650, 79000);

-- 环比计算:与上月对比
SELECT
    month,
    order_count,
    revenue,

    -- 上月订单数
    LAG(order_count, 1) OVER (ORDER BY month) AS prev_month_orders,
    -- 下月订单数
    LEAD(order_count, 1) OVER (ORDER BY month) AS next_month_orders,

    -- 环比增长率:(本月-上月)/上月 * 100
    CONCAT(
        ROUND(
            (order_count - LAG(order_count,1) OVER (ORDER BY month))
            / LAG(order_count,1) OVER (ORDER BY month) * 100, 2
        ), '%'
    ) AS mom_growth,

    -- 相比三个月前的变化
    ROUND(
        (order_count - LAG(order_count,3) OVER (ORDER BY month))
        / LAG(order_count,3) OVER (ORDER BY month) * 100, 2
    ) AS mom3_growth_pct

FROM monthly_orders;

案例三:帧窗口(ROWS BETWEEN)—— 移动平均

-- 场景:计算最近3个月的滚动平均订单量
SELECT
    month,
    order_count,

    -- 移动平均:本行及前2行的均值
    ROUND(
        AVG(order_count) OVER (
            ORDER BY month
            ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
        ), 2
    ) AS rolling_avg_3month,

    -- 本行及前后各1行(对称窗口)
    ROUND(
        AVG(order_count) OVER (
            ORDER BY month
            ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
        ), 2
    ) AS rolling_avg_centered

FROM monthly_orders;

1.2 公用表表达式(CTE)

CTE(Common Table Expression)用 WITH 语句定义临时命名结果集,比子查询语法更清晰,支持递归调用。

案例一:多层级联查询重构

-- ❌ 子查询地狱(深层嵌套,难以维护)
SELECT *
FROM (
    SELECT *, region,
        (SELECT SUM(sales) FROM orders o2 WHERE o2.region = o1.region) AS region_total
    FROM orders o1
    WHERE status = 'completed'
) t1
WHERE region_total > 100000;

-- ✅ CTE 版本:逻辑清晰,可逐步调试
WITH
active_orders AS (
    SELECT * FROM orders WHERE status = 'completed'
),
regional_totals AS (
    SELECT region, SUM(sales) AS total_sales
    FROM active_orders
    GROUP BY region
),
top_regions AS (
    SELECT region FROM regional_totals WHERE total_sales > 100000
)
SELECT o.*
FROM active_orders o
INNER JOIN top_regions tr ON o.region = tr.region
ORDER BY o.sales DESC;

案例二:递归 CTE —— 树形结构查询

场景:查询公司组织架构,从 CEO 到每位员工。

CREATE TABLE org_chart (
    id         INT PRIMARY KEY,
    name       VARCHAR(50),
    title      VARCHAR(50),
    manager_id INT  -- 上级ID,CEO 为 NULL
);

INSERT INTO org_chart VALUES
(1, '陈总裁', 'CEO',              NULL),
(2, '张副总', '技术副总裁',         1),
(3, '刘副总', '运营副总裁',         1),
(4, '王总监', '后端研发总监',       2),
(5, '赵总监', '前端研发总监',       2),
(6, '孙经理', 'PHP开发组经理',     4),
(7, '周工',   'PHP高级工程师',      6),
(8, '吴工',   'PHP中级工程师',      6),
(9, '郑工',   '前端开发工程师',     5);
-- 递归 CTE:自顶向下遍历整棵树
WITH RECURSIVE org_tree AS (
    -- ① 锚点:CEO(根节点)
    SELECT id, name, title, manager_id, 1 AS level, name AS path
    FROM org_chart
    WHERE manager_id IS NULL

    UNION ALL

    -- ② 递归:不断找直接下属
    SELECT
        o.id, o.name, o.title, o.manager_id,
        ot.level + 1,
        CONCAT(ot.path, ' → ', o.name) AS path  -- 展示完整汇报链
    FROM org_chart o
    INNER JOIN org_tree ot ON o.manager_id = ot.id
)
SELECT
    REPEAT('    ', level - 1) AS indent,  -- 缩进:层级越深,空白越多
    level,
    name,
    title,
    path AS reporting_chain
FROM org_tree
ORDER BY path;

输出效果:

level | name  | title         | reporting_chain
1     | 陈总裁 | CEO           | 陈总裁
2     | 张副总 | 技术副总裁     | 陈总裁 → 张副总
3     | 王总监 | 后端研发总监   | 陈总裁 → 张副总 → 王总监
4     | 孙经理 | PHP开发组经理  | 陈总裁 → 张副总 → 王总监 → 孙经理
5     | 周工   | PHP高级工程师  | 陈总裁 → 张副总 → 王总监 → 孙经理 → 周工
...

案例三:递归 CTE —— 分类树(评论嵌套结构)

CREATE TABLE comments (
    id         INT PRIMARY KEY,
    article_id INT,
    content    VARCHAR(500),
    parent_id  INT  -- 回复的上一级ID,顶级评论为 NULL
);

-- 查询评论树(将嵌套结构展开为扁平列表,含深度)
WITH RECURSIVE comment_tree AS (
    -- 锚点:顶级评论
    SELECT id, article_id, content, parent_id, 0 AS depth, CAST(id AS CHAR(200)) AS path
    FROM comments
    WHERE parent_id IS NULL AND article_id = 1

    UNION ALL

    -- 递归:回复
    SELECT
        c.id, c.article_id, c.content, c.parent_id,
        ct.depth + 1,
        CONCAT(ct.path, ',', c.id) AS path
    FROM comments c
    INNER JOIN comment_tree ct ON c.parent_id = ct.id
)
SELECT depth, id, content, path FROM comment_tree ORDER BY path;

1.3 复杂 JOIN 操作

自连接(Self JOIN)—— 处理层级与对比

-- 场景一:查询每位员工及其直接主管
SELECT
    e1.name  AS employee,
    e1.title AS emp_title,
    e2.name  AS manager,
    e2.title AS mgr_title
FROM employees e1
LEFT JOIN employees e2 ON e1.manager_id = e2.id;

-- 场景二:查询同一部门内薪资高于平均值的员工
SELECT
    e1.name     AS employee,
    e1.department,
    e1.salary,
    ROUND(AVG(e2.salary), 2) AS dept_avg_salary
FROM employees e1
INNER JOIN employees e2 ON e1.department = e2.department
GROUP BY e1.id, e1.name, e1.department, e1.salary
HAVING e1.salary > AVG(e2.salary);

-- 场景三:查询连续日期缺口(数据补齐)
-- 场景:用户记录了有数据的日期,但报表需要展示每一天
CREATE TABLE sales_log (
    sale_date DATE,
    amount    DECIMAL(10,2)
);

-- 生成连续日期序列并左连接原始数据(日期缺口一目了然)
SELECT
    d.date_seq  AS missing_date,
    s.amount
FROM (
    SELECT DATE_SUB(CURDATE(), INTERVAL n DAY) AS date_seq
    FROM (
        SELECT @row := @row + 1 AS n
        FROM any_table, (SELECT @row := -6) r  -- 生成最近7天
        LIMIT 7
    ) t
) d
LEFT JOIN sales_log s ON s.sale_date = d.date_seq
ORDER BY d.date_seq;

二、存储过程与函数:把业务逻辑下沉到数据库

2.1 为什么使用存储过程?

优势 说明
减少网络往返 业务逻辑在数据库端执行,无需多次往返传输数据
预编译缓存 语句一次编译、反复调用,执行计划被 MySQL 缓存
事务封装 多步写操作原子化,任何一步失败自动回滚
安全 应用层只调用存储过程名称,无法直接操作底层表字段

2.2 完整案例:电商订单创建存储过程

需求:创建订单时,同时扣减库存、记录日志。任何一步失败则全部回滚。

-- ① 创建库存表和订单表
CREATE TABLE products (
    product_id  INT PRIMARY KEY,
    name        VARCHAR(100),
    price       DECIMAL(10,2),
    stock       INT DEFAULT 0
);

CREATE TABLE orders (
    order_id    INT PRIMARY KEY AUTO_INCREMENT,
    customer_id INT,
    product_id  INT,
    quantity    INT,
    total_price DECIMAL(10,2),
    status      VARCHAR(20) DEFAULT 'pending',
    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE order_logs (
    log_id      INT PRIMARY KEY AUTO_INCREMENT,
    order_id    INT,
    action      VARCHAR(50),
    message     VARCHAR(200),
    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO products VALUES (1, 'iPhone 16', 7999.00, 50);
-- ② 存储过程:带错误处理和日志记录
DELIMITER $$

CREATE PROCEDURE sp_create_order(
    IN  p_customer_id INT,
    IN  p_product_id  INT,
    IN  p_quantity    INT,
    OUT p_order_id    INT,
    OUT p_status      VARCHAR(100)
)
BEGIN
    DECLARE v_price     DECIMAL(10,2);
    DECLARE v_stock     INT;
    DECLARE v_order_id  INT DEFAULT 0;
    DECLARE v_error     VARCHAR(200) DEFAULT '';

    -- 声明异常处理器
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        GET DIAGNOSTICS CONDITION 1 v_error = MESSAGE_TEXT;
        SET p_status = CONCAT('执行失败: ', v_error);
    END;

    -- 开始事务
    START TRANSACTION;

    -- 锁定行(防止并发超卖)
    SELECT price, stock INTO v_price, v_stock
    FROM products
    WHERE product_id = p_product_id
    FOR UPDATE;  -- 关键:行级锁,并发安全

    -- 库存校验
    IF v_stock < p_quantity THEN
        SET p_status = CONCAT('库存不足:当前 ', v_stock, ',需求 ', p_quantity);
        SET p_order_id = 0;
        ROLLBACK;
    ELSE
        -- 创建订单
        INSERT INTO orders (customer_id, product_id, quantity, total_price, status)
        VALUES (p_customer_id, p_product_id, p_quantity, v_price * p_quantity, 'confirmed');

        SET v_order_id = LAST_INSERT_ID();

        -- 扣减库存
        UPDATE products
        SET stock = stock - p_quantity
        WHERE product_id = p_product_id;

        -- 记录操作日志
        INSERT INTO order_logs (order_id, action, message)
        VALUES (v_order_id, 'CREATE_ORDER',
                CONCAT('创建订单,商品ID=', p_product_id, ',数量=', p_quantity));

        COMMIT;

        SET p_order_id = v_order_id;
        SET p_status  = '订单创建成功';
    END IF;
END$$

DELIMITER ;
-- ③ 调用存储过程
CALL sp_create_order(101, 1, 3, @order_id, @msg);
SELECT @order_id AS 订单ID, @msg AS 状态;

-- ④ 查看执行结果
SELECT * FROM orders WHERE order_id = @order_id;
SELECT * FROM order_logs WHERE order_id = @order_id;
SELECT stock FROM products WHERE product_id = 1;  -- 库存应从50变为47

2.3 存储函数:在 SELECT 中直接调用

-- 场景:根据订单金额计算会员折扣价
DELIMITER $$

CREATE FUNCTION fn_calc_discount(
    p_amount   DECIMAL(10,2),
    p_level    TINYINT   -- 1=普通 2=银卡 3=金卡 4=钻石
)
RETURNS DECIMAL(10,2)
DETERMINISTIC
BEGIN
    DECLARE v_discount_rate DECIMAL(3,2);

    CASE p_level
        WHEN 1 THEN SET v_discount_rate = 1.00;     -- 普通:无折扣
        WHEN 2 THEN SET v_discount_rate = 0.95;     -- 银卡:95折
        WHEN 3 THEN SET v_discount_rate = 0.88;     -- 金卡:88折
        WHEN 4 THEN SET v_discount_rate = 0.80;     -- 钻石:8折
        ELSE         SET v_discount_rate = 1.00;
    END CASE;

    RETURN ROUND(p_amount * v_discount_rate, 2);
END$$

DELIMITER ;
-- 在查询中直接使用
SELECT
    order_id,
    total_price AS 原价,
    customer_level,
    fn_calc_discount(total_price, customer_level) AS 折后价,
    total_price - fn_calc_discount(total_price, customer_level) AS 节省金额
FROM orders
JOIN customers USING (customer_id);

2.4 动态 SQL 与条件执行

-- 场景:根据不同条件动态构建查询
DELIMITER $$

CREATE PROCEDURE sp_search_orders(
    IN p_status   VARCHAR(20),   -- NULL 表示不限
    IN p_min_amt  DECIMAL(10,2),
    IN p_max_amt  DECIMAL(10,2),
    IN p_limit    INT
)
BEGIN
    SET @sql = 'SELECT * FROM orders WHERE 1=1';

    IF p_status IS NOT NULL THEN
        SET @sql = CONCAT(@sql, " AND status = '", p_status, "'");
    END IF;

    IF p_min_amt IS NOT NULL THEN
        SET @sql = CONCAT(@sql, ' AND total_price >= ', p_min_amt);
    END IF;

    IF p_max_amt IS NOT NULL THEN
        SET @sql = CONCAT(@sql, ' AND total_price <= ', p_max_amt);
    END IF;

    SET @sql = CONCAT(@sql, ' ORDER BY created_at DESC LIMIT ', p_limit);

    -- 预处理并执行动态 SQL
    PREPARE stmt FROM @sql;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;
END$$

DELIMITER ;

-- 调用:查找所有待支付订单,最多返回10条
CALL sp_search_orders('pending', NULL, NULL, 10);

三、性能优化:让查询飞起来

3.1 索引设计:六大核心原则

原则一:最左前缀原则

复合索引 (A, B, C) 会按从左到右的顺序生效:

CREATE INDEX idx_user ON users (status, created_at, email);

-- ✅ 命中索引的场景(必须包含最左列 status)
SELECT * FROM users WHERE status = 'active';                    -- 命中 A
SELECT * FROM users WHERE status = 'active' AND created_at > '2025-01-01';  -- 命中 A+B
SELECT * FROM users WHERE status = 'active' AND created_at > '2025-01-01' AND email = 'x@y.com'; -- 命中 A+B+C

-- ❌ 不命中索引(跳过了最左列 A)
SELECT * FROM users WHERE created_at > '2025-01-01';              -- 不命中
SELECT * FROM users WHERE email = 'x@y.com';                      -- 不命中

原则二:覆盖索引(最理想的索引使用方式)

当查询的所有字段都包含在索引中时,MySQL 不需要回表(访问主键索引),直接返回结果。

-- 常见新闻列表页:只需 id、title、created_at、status
-- 查询只需这4个字段,idx_news_cover 覆盖全部,无需回表
CREATE INDEX idx_news_cover ON news (status, created_at DESC, id, title);

SELECT id, title, created_at, status
FROM news
WHERE status = 'published'
ORDER BY created_at DESC
LIMIT 20;

原则三:前缀索引(大文本字段优化)

对 VARCHAR(255) 的长字段建立完整 B+Tree 成本过高,使用前缀索引:

-- 前5个字符的索引,节省空间,适用于以该字段做 LIKE 前缀查询
CREATE INDEX idx_email_prefix ON users (email(5));

-- 验证前缀长度是否够用(区分度测试)
SELECT
    COUNT(DISTINCT LEFT(email, 5)) / COUNT(*) AS selectivity_5,
    COUNT(DISTINCT LEFT(email, 8)) / COUNT(*) AS selectivity_8,
    COUNT(DISTINCT LEFT(email, 10)) / COUNT(*) AS selectivity_10
FROM users;
-- 区分度越高(前缀越长),查询越精确

原则四:联合索引 vs 多个单列索引

-- ❌ 低效:为每个字段单独建索引,查询时 MySQL 每次只选一个
INDEX idx_status  ON orders(status);
INDEX idx_created ON orders(created_at);
INDEX idx_customer ON orders(customer_id);

-- ✅ 高效:一个复合索引覆盖所有查询条件
INDEX idx_order_search ON orders(customer_id, status, created_at);

-- 这条查询三个字段都在索引中,MySQL 一次定位,无需回表
SELECT customer_id, status, created_at
FROM orders
WHERE customer_id = 101
  AND status = 'shipped'
  AND created_at BETWEEN '2025-01-01' AND '2025-03-31';

原则五:索引列上避免使用函数

-- ❌ 函数包裹列,无法使用索引(全表扫描)
SELECT * FROM orders
WHERE YEAR(created_at) = 2025
  AND MONTH(created_at) = 3;

-- ✅ 范围查询,保持列独立,索引正常命中
SELECT * FROM orders
WHERE created_at >= '2025-03-01'
  AND created_at <  '2025-04-01';

原则六:高区分度列优先

-- ❌ 性别(2种)、状态(3种)放复合索引最左 → 区分度极低,索引意义不大
INDEX idx_gender_status ON users(gender, status, created_at);

-- ✅ 交换顺序:高区分度列(status)放前面
INDEX idx_status ON users(status, created_at);

3.2 EXPLAIN 深度解析

EXPLAIN ANALYZE  -- MySQL 8.0+,实际执行并返回估算 vs 实际成本
SELECT
    o.order_id,
    u.username,
    p.product_name,
    o.quantity,
    o.total_price
FROM orders o
INNER JOIN users u  ON o.customer_id = u.id
INNER JOIN products p ON o.product_id = p.id
WHERE o.status = 'shipped'
  AND o.created_at >= '2025-01-01';

关键字段解读:

字段 常见值 含义
type const > ref > range > index > ALL 访问方式,ALL 为全表扫描需优化
possible_keys 可选索引列表 MySQL 考虑了哪些索引
key 实际使用索引名 NULL 表示走了全表扫描
key_len 索引使用字节数 越大说明索引列使用得越多
rows 预估扫描行数 越少越好
filtered 过滤后剩余比例 0-100%,越高越精确
extra 详细策略 见下方说明

Extra 字段常见值及含义:

Extra 值 说明 优化建议
Using index 覆盖索引,无需回表 ✅ 最优
Using index condition 索引条件下推(ICP) ✅ 正常
Using where 服务层过滤 检查是否可前推到索引
Using filesort 额外排序(内存或磁盘) 添加对应 ORDER BY 列的索引
Using temporary 使用临时表 考虑优化查询结构或加索引
Using MRR 范围读取优化 ✅ 正常

3.3 慢查询优化实战

-- 开启慢查询日志(生产环境)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- 超过1秒记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

-- 查看慢查询
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 案例:优化前慢查询(执行时间 3.2s)
-- 原 SQL
SELECT *
FROM orders o
JOIN users u ON o.customer_id = u.id
WHERE o.status = 'cancelled'
  AND o.created_at >= '2025-01-01'
ORDER BY o.created_at DESC
LIMIT 100;

-- EXPLAIN 分析:
-- type=ALL(全表扫描), rows=500000+, extra=Using filesort

-- ✅ 优化后(执行时间 0.08s)
-- 1. 添加复合索引覆盖 WHERE + ORDER BY
ALTER TABLE orders ADD INDEX idx_cancel_time (status, created_at DESC);

-- 2. 覆盖索引,直接返回所需字段,无需回表
SELECT o.order_id, o.customer_id, o.total_price, o.created_at,
       u.username, u.email
FROM orders o
INNER JOIN users u ON o.customer_id = u.id
WHERE o.status = 'cancelled'
  AND o.created_at >= '2025-01-01'
ORDER BY o.created_at DESC
LIMIT 100;

-- 验证:type=range, key=idx_cancel_time, rows=120, extra=Using index

3.4 子查询优化策略

-- ❌ 危险写法:IN 子查询在大表上性能差
SELECT * FROM orders
WHERE customer_id IN (
    SELECT id FROM customers
    WHERE region = '华东' AND level >= 3
);
-- MySQL 对内表的外键列无索引时,会将子查询物化为临时表,全表扫描

-- ✅ 方案一:EXISTS(相关子查询,找到第一条即停)
SELECT * FROM orders o
WHERE EXISTS (
    SELECT 1 FROM customers c
    WHERE c.id = o.customer_id
      AND c.region = '华东'
      AND c.level >= 3
);

-- ✅ 方案二:JOIN(MySQL 优化器处理更灵活)
SELECT DISTINCT o.*
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id
WHERE c.region = '华东' AND c.level >= 3;

-- ✅ 方案三:派生表(子查询先执行,结果集通常较小)
SELECT o.*
FROM orders o
INNER JOIN (
    SELECT id FROM customers
    WHERE region = '华东' AND level >= 3
) c ON o.customer_id = c.id;

子查询优化核心规则:

  • EXISTS 优于 IN(外大内小:外层结果少、内层子查询快)
  • IN 优于 EXISTS(外小内大:内层结果少、扫描快)
  • 避免相关子查询(子查询引用外层列,每次外层行都重跑)
  • 嵌套超过 2 层 → 改写为 JOIN

四、数据类型与字符集最佳实践

4.1 字符集选型与配置

-- 生产环境推荐配置
CREATE DATABASE production_db
    DEFAULT CHARACTER SET utf8mb4          -- 完整 UTF-8,支持 emoji 和所有 Unicode 字符
    DEFAULT COLLATE utf8mb4_unicode_ci;    -- Unicode 排序规则,支持多语言自然排序
字符集 最大存储长度 Emoji 支持 推荐场景
latin1 1字节/字符 纯英文遗留系统
utf8 (utf8mb3) 3字节/字符 ❌ 部分 旧系统,慎用
utf8mb4 4字节/字符 ✅ 完整 所有新项目必选
ascii 1字节/字符 纯 ASCII

4.2 字段类型精确选型

CREATE TABLE products (
    -- 主键:自增 BIGINT 更稳妥(INT 最大 21 亿)
    id              BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,

    -- 价格:DECIMAL 精确存储(避免浮点误差),VARCHAR 存金额是大忌
    price           DECIMAL(10,2) NOT NULL DEFAULT 0.00,

    -- 短文本:VARCHAR(255) 够用,CHAR 是定长,空间浪费
    product_name    VARCHAR(255) NOT NULL,

    -- 长文本:TEXT 不建立索引,如需全文搜索用 FULLTEXT 索引
    description     TEXT,

    -- 状态:ENUM 语义清晰,数据库层校验合法值
    status          ENUM('draft','active','offline','deleted') DEFAULT 'draft',

    -- 时间戳:TIMESTAMP 自动更新,比 DATETIME 节省 4 字节
    created_at      TIMESTAMP   DEFAULT CURRENT_TIMESTAMP,
    updated_at      TIMESTAMP   DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    -- 地理信息:MySQL 8.0+ 原生空间数据类型
    location        POINT,
    delivery_zone   POLYGON,

    -- JSON 数据:MySQL 5.7+ 原生支持,无需单独建表
    -- 适合灵活扩展字段(如商品规格、用户偏好)
    specs           JSON,

    -- IP地址:存储为无符号 INT,节省空间,支持排序
    last_login_ip   INT UNSIGNED
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

4.3 JSON 类型实战

-- 场景:商品规格(尺寸、颜色、重量随时变化,不适合固定列)
CREATE TABLE products (
    id    INT PRIMARY KEY AUTO_INCREMENT,
    name  VARCHAR(255),
    specs JSON  -- 动态规格,不固定字段结构
);

INSERT INTO products (name, specs) VALUES ('iPhone 16', '
{
    "colors": ["黑色", "白色", "蓝色"],
    "storage": ["128GB", "256GB", "512GB"],
    "weight": "170g",
    "display": { "size": "6.1英寸", "type": "OLED" }
}
');

-- JSON 查询函数(MySQL 8.0+)
SELECT
    name,
    JSON_EXTRACT(specs, '$.weight')                      AS weight,
    JSON_EXTRACT(specs, '$.colors[0]')                   AS first_color,
    JSON_EXTRACT(specs, '$.display.type')                 AS display_type,
    JSON_VALUE(specs, '$.display.size')                  AS display_size,  -- 直接返回字符串
    JSON_KEYS(specs)                                     AS all_keys       -- 返回所有 key 数组
FROM products;

-- 更新 JSON 字段(局部更新,不影响其他字段)
UPDATE products
SET specs = JSON_SET(specs, '$.colors', JSON_ARRAY('黑色', '银色', '金色'))
WHERE id = 1;

-- 搜索 JSON 数组中的值(索引支持)
ALTER TABLE products ADD INDEX idx_storage ((CAST(specs->'$.storage' AS CHAR(50) ARRAY)));

4.4 日期时间函数实战

-- ① 基础函数
SELECT
    NOW()         AS 当前时间,         -- 2025-04-11 23:21:00
    CURDATE()     AS 当前日期,         -- 2025-04-11
    CURTIME()     AS 当前时间,         -- 23:21:00
    UTC_TIMESTAMP() AS UTC时间;       -- UTC时区

-- ② 日期加减:查询最近 N 天的数据
SELECT * FROM orders
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)   -- 最近7天
  AND created_at <= DATE_ADD(NOW(), INTERVAL 1 DAY);   -- 明天23:59前

-- ③ 日期差计算
SELECT
    DATEDIFF('2025-12-31', CURDATE())    AS 距离年末天数,
    TIMESTAMPDIFF(HOUR, created_at, NOW()) AS 订单距今小时数,
    TIMESTAMPDIFF(DAY, created_at, NOW())  AS 订单距今天数;

-- ④ 日期截断(MySQL 8.0+)
SELECT
    created_at,
    DATE(created_at)                        AS 当天日期,
    DATE_FORMAT(created_at, '%Y-%m')        AS 年月,
    DATE_TRUNC('MONTH', created_at)        AS 月初(需 MySQL 8.0.28+);

-- ⑤ 每月第 N 天 / 每周第 N 天
SELECT
    DAYOFWEEK(created_at)  AS 周几(1=周日),
    DAYOFMONTH(created_at) AS 几号,
    LAST_DAY(created_at)   AS 月末,
    DAYNAME(created_at)     AS 星期名称;

五、安全与运维建议

5.1 SQL 注入防护(最核心的安全防线)

<?php
// ❌ 危险:字符串拼接,100% 可被 SQL 注入
$sql = "SELECT * FROM users WHERE email = '" . $_POST['email'] . "'";
mysqli_query($db, $sql);

// ✅ 安全:预处理语句(PDO)
$pdo = new PDO('mysql:host=localhost;dbname=shop', 'root', 'password');
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);  // 关闭模拟预编译

$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email LIMIT 1');
$stmt->execute(['email' => $_POST['email']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

// ✅ 安全:mysqli 预处理
$stmt = $mysqli->prepare('SELECT * FROM users WHERE email = ? LIMIT 1');
$stmt->bind_param('s', $_POST['email']);
$stmt->execute();
$result = $stmt->get_result();

5.2 最小权限原则

-- 创建应用专用账户(只能访问指定数据库和表)
CREATE USER 'app_reader'@'%' IDENTIFIED BY 'Str0ngP@ss!';
GRANT SELECT ON shop.products TO 'app_reader'@'%';   -- 只读

CREATE USER 'app_writer'@'%' IDENTIFIED BY 'Str0ngP@ss!';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.orders TO 'app_writer'@'%';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.products TO 'app_writer'@'%';
GRANT EXECUTE ON PROCEDURE shop.sp_create_order TO 'app_writer'@'%';  -- 只授权指定存储过程

FLUSH PRIVILEGES;

-- 查看用户权限
SHOW GRANTS FOR 'app_writer'@'%';

5.3 大表 DDL 变更(pt-online-schema-change)

-- 场景:为 5000 万行的 orders 表添加索引,直接 ALTER 会锁表
-- 使用 pt-online-schema-change(Percona Toolkit)在线变更

-- ① 安装 Percona Toolkit
-- yum install percona-toolkit

-- ② 在线加索引(不锁表,实时同步数据)
pt-online-schema-change \
    --alter "ADD INDEX idx_customer_date (customer_id, created_at)" \
    D=t_shop,t=orders \
    --execute \
    --charset=utf8mb4 \
    --chunk-size=1000 \
    --max-load="Threads_running=50" \
    --critical-load="Threads_running=100"

-- ③ 原理:
--    1. 创建新表(新索引结构)
--    2. 逐步从原表复制数据到新表
--    3. 同步期间原表的所有写操作通过触发器同步到新表
--    4. 切换新旧表(毫秒级锁)
--    5. 删除原表

结语

MySQL 的高级用法是一个庞大的体系,窗口函数让 SQL 具备了分析能力,CTE 让复杂查询可读性大幅提升,存储过程把业务逻辑安全地沉淀在数据库层,合理的索引设计是性能优化的基石。掌握以上内容,你已经具备了一个中级 DBA 所需的核心技能。

推荐学习路径
第一阶段:窗口函数 + EXPLAIN 分析(立竿见影,立刻用得上)
第二阶段:CTE 递归查询 + 存储过程(进阶必备)
第三阶段:索引设计原理 + 慢查询优化(系统性提升)

上一篇:2026年PHP技术指南:8.x新特性、安全实践与框架生态全解析 下一篇:Nginx 性能优化实战指南:从基础配置到高并发压测

评论

评论(0)

暂无评论