Skip to content

practical examples

文件信息

  • 📄 原文件:06_practical_examples.sql
  • 🔤 语言:SQL

SQL 脚本

sql
-- ============================================================
--                    MySQL 实战示例
-- ============================================================
-- 本文件包含常见业务场景的 SQL 实现。
-- ============================================================

USE learn_mysql;

-- ============================================================
--                    1. 电商订单系统
-- ============================================================

-- --- 创建表结构 ---

-- 商品表
CREATE TABLE IF NOT EXISTS shop_products (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    category_id INT UNSIGNED,
    price DECIMAL(10, 2) NOT NULL,
    stock INT UNSIGNED DEFAULT 0,
    status ENUM('on_sale', 'off_sale', 'deleted') DEFAULT 'on_sale',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_category (category_id),
    INDEX idx_status (status)
) ENGINE=InnoDB;

-- 用户表
CREATE TABLE IF NOT EXISTS shop_users (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL,
    phone VARCHAR(20),
    balance DECIMAL(10, 2) DEFAULT 0.00,
    vip_level TINYINT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- 订单表
CREATE TABLE IF NOT EXISTS shop_orders (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    order_no VARCHAR(32) NOT NULL UNIQUE,
    user_id INT UNSIGNED NOT NULL,
    total_amount DECIMAL(10, 2) NOT NULL,
    discount_amount DECIMAL(10, 2) DEFAULT 0.00,
    pay_amount DECIMAL(10, 2) NOT NULL,
    status ENUM('pending', 'paid', 'shipped', 'completed', 'cancelled', 'refunded') DEFAULT 'pending',
    paid_at TIMESTAMP NULL,
    shipped_at TIMESTAMP NULL,
    completed_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user (user_id),
    INDEX idx_status (status),
    INDEX idx_created (created_at)
) ENGINE=InnoDB;

-- 订单明细表
CREATE TABLE IF NOT EXISTS shop_order_items (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    order_id INT UNSIGNED NOT NULL,
    product_id INT UNSIGNED NOT NULL,
    product_name VARCHAR(200) NOT NULL,
    price DECIMAL(10, 2) NOT NULL,
    quantity INT UNSIGNED NOT NULL,
    amount DECIMAL(10, 2) NOT NULL,
    INDEX idx_order (order_id),
    FOREIGN KEY (order_id) REFERENCES shop_orders(id) ON DELETE CASCADE
) ENGINE=InnoDB;

-- 插入测试数据
INSERT INTO shop_products (name, category_id, price, stock) VALUES
    ('iPhone 15', 1, 5999.00, 100),
    ('MacBook Pro', 2, 14999.00, 50),
    ('AirPods Pro', 3, 1899.00, 200),
    ('iPad Air', 2, 4599.00, 80),
    ('Apple Watch', 3, 2999.00, 150);

INSERT INTO shop_users (username, email, phone, balance, vip_level) VALUES
    ('user1', 'user1@example.com', '13800001111', 10000.00, 3),
    ('user2', 'user2@example.com', '13800002222', 5000.00, 2),
    ('user3', 'user3@example.com', '13800003333', 2000.00, 1);

-- --- 业务查询示例 ---

-- 1. 生成订单号
SELECT CONCAT(DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'), LPAD(FLOOR(RAND() * 1000000), 6, '0')) AS order_no;

-- 2. 创建订单存储过程
DELIMITER //

CREATE PROCEDURE create_order(
    IN p_user_id INT,
    IN p_product_id INT,
    IN p_quantity INT,
    OUT p_order_no VARCHAR(32),
    OUT p_result VARCHAR(100)
)
BEGIN
    DECLARE v_price DECIMAL(10, 2);
    DECLARE v_stock INT;
    DECLARE v_product_name VARCHAR(200);
    DECLARE v_total DECIMAL(10, 2);
    DECLARE v_order_id INT;

    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SET p_result = '订单创建失败';
    END;

    START TRANSACTION;

    -- 检查库存(加锁)
    SELECT price, stock, name INTO v_price, v_stock, v_product_name
    FROM shop_products
    WHERE id = p_product_id
    FOR UPDATE;

    IF v_stock < p_quantity THEN
        ROLLBACK;
        SET p_result = '库存不足';
    ELSE
        -- 生成订单号
        SET p_order_no = CONCAT(DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'),
                                LPAD(FLOOR(RAND() * 1000000), 6, '0'));

        -- 计算总价
        SET v_total = v_price * p_quantity;

        -- 创建订单
        INSERT INTO shop_orders (order_no, user_id, total_amount, pay_amount)
        VALUES (p_order_no, p_user_id, v_total, v_total);

        SET v_order_id = LAST_INSERT_ID();

        -- 创建订单明细
        INSERT INTO shop_order_items (order_id, product_id, product_name, price, quantity, amount)
        VALUES (v_order_id, p_product_id, v_product_name, v_price, p_quantity, v_total);

        -- 扣减库存
        UPDATE shop_products SET stock = stock - p_quantity WHERE id = p_product_id;

        COMMIT;
        SET p_result = '订单创建成功';
    END IF;
END//

DELIMITER ;

-- 调用创建订单
CALL create_order(1, 1, 2, @order_no, @result);
SELECT @order_no, @result;

-- 3. 订单统计报表
SELECT
    DATE(created_at) AS order_date,
    COUNT(*) AS order_count,
    COUNT(DISTINCT user_id) AS user_count,
    SUM(pay_amount) AS total_sales,
    AVG(pay_amount) AS avg_order_amount
FROM shop_orders
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY DATE(created_at)
ORDER BY order_date DESC;

-- 4. 用户消费排行
SELECT
    u.username,
    u.vip_level,
    COUNT(o.id) AS order_count,
    SUM(o.pay_amount) AS total_spent,
    AVG(o.pay_amount) AS avg_spent
FROM shop_users u
LEFT JOIN shop_orders o ON u.id = o.user_id AND o.status IN ('paid', 'completed')
GROUP BY u.id, u.username, u.vip_level
ORDER BY total_spent DESC
LIMIT 10;

-- 5. 商品销售排行
SELECT
    p.name AS product_name,
    p.price,
    COALESCE(SUM(oi.quantity), 0) AS total_sold,
    COALESCE(SUM(oi.amount), 0) AS total_revenue
FROM shop_products p
LEFT JOIN shop_order_items oi ON p.id = oi.product_id
LEFT JOIN shop_orders o ON oi.order_id = o.id AND o.status IN ('paid', 'completed')
GROUP BY p.id, p.name, p.price
ORDER BY total_sold DESC;


-- ============================================================
--                    2. 用户签到系统
-- ============================================================

CREATE TABLE IF NOT EXISTS user_checkins (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id INT UNSIGNED NOT NULL,
    checkin_date DATE NOT NULL,
    points_earned INT DEFAULT 10,
    consecutive_days INT DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_user_date (user_id, checkin_date),
    INDEX idx_user (user_id)
) ENGINE=InnoDB;

-- 签到存储过程
DELIMITER //

CREATE PROCEDURE user_checkin(
    IN p_user_id INT,
    OUT p_points INT,
    OUT p_consecutive INT,
    OUT p_message VARCHAR(100)
)
BEGIN
    DECLARE v_last_date DATE;
    DECLARE v_last_consecutive INT;
    DECLARE v_today DATE DEFAULT CURDATE();

    -- 检查今日是否已签到
    IF EXISTS (SELECT 1 FROM user_checkins WHERE user_id = p_user_id AND checkin_date = v_today) THEN
        SET p_message = '今日已签到';
        SELECT points_earned, consecutive_days INTO p_points, p_consecutive
        FROM user_checkins WHERE user_id = p_user_id AND checkin_date = v_today;
    ELSE
        -- 获取上次签到信息
        SELECT checkin_date, consecutive_days INTO v_last_date, v_last_consecutive
        FROM user_checkins
        WHERE user_id = p_user_id
        ORDER BY checkin_date DESC
        LIMIT 1;

        -- 计算连续天数
        IF v_last_date = DATE_SUB(v_today, INTERVAL 1 DAY) THEN
            SET p_consecutive = v_last_consecutive + 1;
        ELSE
            SET p_consecutive = 1;
        END IF;

        -- 计算积分(连续签到奖励)
        SET p_points = 10 + LEAST(p_consecutive - 1, 7) * 5;

        -- 插入签到记录
        INSERT INTO user_checkins (user_id, checkin_date, points_earned, consecutive_days)
        VALUES (p_user_id, v_today, p_points, p_consecutive);

        SET p_message = CONCAT('签到成功,获得 ', p_points, ' 积分');
    END IF;
END//

DELIMITER ;

-- 签到
CALL user_checkin(1, @points, @consecutive, @msg);
SELECT @points AS 积分, @consecutive AS 连续天数, @msg AS 消息;

-- 签到排行榜
SELECT
    u.username,
    COUNT(*) AS total_checkins,
    SUM(c.points_earned) AS total_points,
    MAX(c.consecutive_days) AS max_consecutive
FROM shop_users u
JOIN user_checkins c ON u.id = c.user_id
WHERE c.checkin_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY u.id, u.username
ORDER BY total_points DESC
LIMIT 10;


-- ============================================================
--                    3. 消息队列表
-- ============================================================

CREATE TABLE IF NOT EXISTS message_queue (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    queue_name VARCHAR(50) NOT NULL,
    payload JSON NOT NULL,
    status ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending',
    retry_count INT DEFAULT 0,
    max_retries INT DEFAULT 3,
    scheduled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    started_at TIMESTAMP NULL,
    completed_at TIMESTAMP NULL,
    error_message TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_queue_status (queue_name, status, scheduled_at),
    INDEX idx_scheduled (scheduled_at)
) ENGINE=InnoDB;

-- 添加消息
INSERT INTO message_queue (queue_name, payload, scheduled_at)
VALUES ('email', '{"to": "user@example.com", "subject": "Welcome", "body": "Hello!"}', NOW());

-- 获取并锁定消息(消费者)
DELIMITER //

CREATE PROCEDURE consume_message(
    IN p_queue_name VARCHAR(50),
    OUT p_message_id BIGINT,
    OUT p_payload JSON
)
BEGIN
    DECLARE v_id BIGINT;

    -- 查找并锁定一条消息
    SELECT id INTO v_id
    FROM message_queue
    WHERE queue_name = p_queue_name
      AND status = 'pending'
      AND scheduled_at <= NOW()
      AND retry_count < max_retries
    ORDER BY scheduled_at
    LIMIT 1
    FOR UPDATE SKIP LOCKED;

    IF v_id IS NOT NULL THEN
        -- 更新状态
        UPDATE message_queue
        SET status = 'processing', started_at = NOW()
        WHERE id = v_id;

        -- 返回消息
        SELECT id, payload INTO p_message_id, p_payload
        FROM message_queue WHERE id = v_id;
    ELSE
        SET p_message_id = NULL;
        SET p_payload = NULL;
    END IF;
END//

-- 完成消息
CREATE PROCEDURE complete_message(IN p_message_id BIGINT)
BEGIN
    UPDATE message_queue
    SET status = 'completed', completed_at = NOW()
    WHERE id = p_message_id;
END//

-- 失败重试
CREATE PROCEDURE fail_message(IN p_message_id BIGINT, IN p_error TEXT)
BEGIN
    UPDATE message_queue
    SET
        status = IF(retry_count + 1 >= max_retries, 'failed', 'pending'),
        retry_count = retry_count + 1,
        error_message = p_error,
        scheduled_at = DATE_ADD(NOW(), INTERVAL POW(2, retry_count) MINUTE)
    WHERE id = p_message_id;
END//

DELIMITER ;


-- ============================================================
--                    4. 树形结构(邻接表)
-- ============================================================

CREATE TABLE IF NOT EXISTS categories (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    parent_id INT UNSIGNED,
    level INT DEFAULT 1,
    path VARCHAR(500),
    sort_order INT DEFAULT 0,
    INDEX idx_parent (parent_id),
    INDEX idx_path (path)
) ENGINE=InnoDB;

INSERT INTO categories (name, parent_id, level, path) VALUES
    ('电子产品', NULL, 1, '/1/'),
    ('手机', 1, 2, '/1/2/'),
    ('电脑', 1, 2, '/1/3/'),
    ('智能手机', 2, 3, '/1/2/4/'),
    ('功能手机', 2, 3, '/1/2/5/'),
    ('笔记本', 3, 3, '/1/3/6/'),
    ('台式机', 3, 3, '/1/3/7/');

-- 查询所有子分类(使用路径)
SELECT * FROM categories WHERE path LIKE '/1/2/%';

-- 查询所有父分类
SELECT * FROM categories
WHERE FIND_IN_SET(id, (
    SELECT REPLACE(TRIM(BOTH '/' FROM path), '/', ',')
    FROM categories WHERE id = 4
));

-- 递归查询(MySQL 8.0+)
WITH RECURSIVE category_tree AS (
    SELECT id, name, parent_id, level, path
    FROM categories
    WHERE parent_id IS NULL

    UNION ALL

    SELECT c.id, c.name, c.parent_id, c.level, c.path
    FROM categories c
    JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree;


-- ============================================================
--                    5. 数据统计与报表
-- ============================================================

-- 按时间维度统计
SELECT
    DATE_FORMAT(created_at, '%Y-%m') AS month,
    COUNT(*) AS order_count,
    SUM(pay_amount) AS revenue,
    COUNT(DISTINCT user_id) AS unique_users
FROM shop_orders
WHERE status IN ('paid', 'completed')
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
ORDER BY month;

-- 同比环比分析
WITH monthly_stats AS (
    SELECT
        DATE_FORMAT(created_at, '%Y-%m') AS month,
        SUM(pay_amount) AS revenue
    FROM shop_orders
    WHERE status IN ('paid', 'completed')
    GROUP BY DATE_FORMAT(created_at, '%Y-%m')
)
SELECT
    month,
    revenue,
    LAG(revenue) OVER (ORDER BY month) AS last_month_revenue,
    revenue - LAG(revenue) OVER (ORDER BY month) AS mom_change,
    ROUND((revenue - LAG(revenue) OVER (ORDER BY month)) /
          NULLIF(LAG(revenue) OVER (ORDER BY month), 0) * 100, 2) AS mom_rate
FROM monthly_stats;

-- 用户留存分析
WITH first_order AS (
    SELECT user_id, MIN(DATE(created_at)) AS first_date
    FROM shop_orders
    GROUP BY user_id
),
retention AS (
    SELECT
        fo.first_date,
        DATEDIFF(DATE(o.created_at), fo.first_date) AS days_after,
        COUNT(DISTINCT o.user_id) AS users
    FROM first_order fo
    JOIN shop_orders o ON fo.user_id = o.user_id
    GROUP BY fo.first_date, days_after
)
SELECT
    first_date AS 注册日期,
    MAX(CASE WHEN days_after = 0 THEN users END) AS 当日,
    MAX(CASE WHEN days_after = 1 THEN users END) AS 次日,
    MAX(CASE WHEN days_after = 7 THEN users END) AS 七日,
    MAX(CASE WHEN days_after = 30 THEN users END) AS 三十日
FROM retention
GROUP BY first_date
ORDER BY first_date;


-- ============================================================
--                    6. 数据清洗与迁移
-- ============================================================

-- 批量更新(避免锁表过久)
DELIMITER //

CREATE PROCEDURE batch_update(
    IN p_batch_size INT,
    OUT p_total_updated INT
)
BEGIN
    DECLARE v_affected INT DEFAULT 1;
    SET p_total_updated = 0;

    WHILE v_affected > 0 DO
        UPDATE shop_products
        SET status = 'off_sale'
        WHERE status = 'on_sale' AND stock = 0
        LIMIT p_batch_size;

        SET v_affected = ROW_COUNT();
        SET p_total_updated = p_total_updated + v_affected;

        -- 短暂休息,避免锁竞争
        DO SLEEP(0.1);
    END WHILE;
END//

DELIMITER ;

-- 数据归档
INSERT INTO shop_orders_archive
SELECT * FROM shop_orders
WHERE created_at < DATE_SUB(CURDATE(), INTERVAL 1 YEAR)
  AND status IN ('completed', 'cancelled', 'refunded');

DELETE FROM shop_orders
WHERE created_at < DATE_SUB(CURDATE(), INTERVAL 1 YEAR)
  AND status IN ('completed', 'cancelled', 'refunded')
LIMIT 10000;


-- ============================================================
--                    总结
-- ============================================================

/*
实战技巧:

订单系统:
- 使用事务保证数据一致性
- 库存扣减加锁防止超卖
- 订单号生成策略

签到系统:
- 唯一索引防止重复签到
- 连续签到计算逻辑

消息队列:
- SKIP LOCKED 实现并发消费
- 指数退避重试策略

树形结构:
- 路径枚举法快速查询
- 递归 CTE(MySQL 8.0+)

数据统计:
- 窗口函数计算同比环比
- 用户留存分析

批量操作:
- 分批处理避免锁表
- 数据归档策略
*/

💬 讨论

使用 GitHub 账号登录后即可参与讨论

基于 MIT 许可发布