数据库入门:SQL学习路线图与实战技巧

1. SQL基础语法与核心概念:从SELECT到JOIN的深度解析

作为有多年经验的开发者,我们可能已经习惯了在项目中直接使用ORM框架,但SQL作为数据库操作的基础语言,其底层逻辑和优化空间往往被忽视。今天我们就从最基础的SELECT语句开始,重新审视那些看似简单的语法背后隐藏的优化技巧。

SELECT语句的进阶用法

很多人认为SELECT只是简单的"查询所有列",但实际上它的表达能力远超想象。考虑这样一个场景:我们需要从用户表中查询活跃用户的统计信息,同时计算他们的平均消费金额。一个典型的实现可能是:

sql 复制代码
SELECT 
    user_id,
    username,
    COUNT(order_id) AS order_count,
    AVG(amount) AS avg_amount
FROM 
    users
LEFT JOIN 
    orders ON users.user_id = orders.user_id
WHERE 
    users.status = 'active'
GROUP BY 
    user_id, username
HAVING 
    COUNT(order_id) > 5
ORDER BY 
    avg_amount DESC
LIMIT 10;

这个简单的查询包含了SELECT的核心要素:列选择、表连接、条件过滤、分组聚合和结果排序。值得注意的是,列选择的顺序会影响结果集的可读性,而聚合函数与GROUP BY的配合使用则是数据分析的基础。

JOIN操作的深度解析

JOIN是SQL中最复杂也最强大的操作之一。在实际项目中,我们经常需要处理多表关联查询。以电商系统为例,假设我们有用户表、订单表和商品表,需要查询每个用户的订单详情及对应商品信息:

sql 复制代码
SELECT 
    u.user_id,
    u.username,
    o.order_id,
    o.order_date,
    p.product_name,
    p.price,
    o.quantity,
    o.quantity * p.price AS total_amount
FROM 
    users u
INNER JOIN 
    orders o ON u.user_id = o.user_id
INNER JOIN 
    order_items oi ON o.order_id = oi.order_id
INNER JOIN 
    products p ON oi.product_id = p.product_id
WHERE 
    o.order_date BETWEEN '2023-01-01' AND '2023-12-31'
ORDER BY 
    u.user_id, o.order_date;

这里我们使用了三个INNER JOIN,确保只返回有完整关联数据的记录。在实际应用中,选择合适的JOIN类型(INNER/LEFT/RIGHT/FULL)对查询性能影响巨大。例如,在用户分析场景中,我们可能需要保留所有用户,即使他们没有订单,这时LEFT JOIN就更为合适。

子查询与CTE的实战技巧

子查询和公用表表达式(CTE)是提升SQL可读性和性能的关键工具。考虑这样一个需求:找出每个部门中薪资最高的员工。使用子查询的写法:

sql 复制代码
SELECT 
    e.employee_id,
    e.employee_name,
    e.salary,
    e.department_id
FROM 
    employees e
WHERE 
    e.salary = (
        SELECT MAX(salary)
        FROM employees
        WHERE department_id = e.department_id
    );

而使用CTE的写法则更加清晰:

vbnet 复制代码
WITH dept_max_salary AS (
    SELECT 
        department_id,
        MAX(salary) AS max_salary
    FROM 
        employees
    GROUP BY 
        department_id
)
SELECT 
    e.employee_id,
    e.employee_name,
    e.salary,
    e.department_id
FROM 
    employees e
JOIN 
    dept_max_salary dms ON e.department_id = dms.department_id
    AND e.salary = dms.max_salary;

CTE的优势在于将复杂查询分解为逻辑单元,提高可读性,同时现代数据库优化器也能更好地处理CTE。在实际项目中,我建议对于超过3层嵌套的子查询,优先考虑使用CTE重构。

实用技巧与最佳实践

  1. 列选择要精准 :避免使用SELECT *,明确指定需要的列,这不仅能减少网络传输,还能让优化器更好地工作。

  2. 索引利用意识:在WHERE、JOIN和ORDER BY中使用的列应该考虑建立索引。例如:

    sql 复制代码
    -- 假设user_id和status都有索引
    SELECT * FROM users WHERE user_id = 100 AND status = 'active';
  3. 避免NULL值陷阱:NULL在SQL中有特殊处理逻辑,例如:

    sql 复制代码
    -- 这两个查询结果不同
    SELECT * FROM users WHERE age = NULL;  -- 永远返回空
    SELECT * FROM users WHERE age IS NULL; -- 正确写法
  4. LIMIT分页优化 :对于大数据量表分页,避免使用LIMIT offset, size,改用基于索引的分页:

    vbnet 复制代码
    -- 传统写法(性能差)
    SELECT * FROM orders ORDER BY order_date LIMIT 10000, 10;
    
    -- 优化写法
    SELECT * FROM orders 
    WHERE order_date < (SELECT order_date FROM (SELECT order_date FROM orders ORDER BY order_date LIMIT 10000, 1) AS t)
    ORDER BY order_date 
    LIMIT 10;

通过掌握这些基础语法和核心概念,我们不仅能写出更高效的SQL查询,还能更好地理解数据库优化器的行为,为后续的索引优化和性能调优打下坚实基础

2. 索引优化与查询性能调优:提升数据库响应速度的实战技巧

在上一章我们探讨了SQL基础语法,但光会写SQL还不够,性能优化才是数据库工作的核心挑战。今天我们就来深入聊聊索引优化和查询性能调优那些事儿,这些技巧在实际项目中能显著提升数据库响应速度。

索引类型与创建策略

数据库索引就像书的目录,能极大加速数据查找。但索引并非越多越好,我们需要根据查询模式精心设计。常见的索引类型包括:

  1. B-Tree索引:最常用的索引类型,适合等值查询和范围查询

    scss 复制代码
    -- 创建B-Tree索引
    CREATE INDEX idx_user_name ON users(username);
    CREATE INDEX idx_order_date ON orders(order_date);
  2. 哈希索引:仅适合等值查询,不适合范围查询

    sql 复制代码
    -- MySQL中InnoDB不支持哈希索引,但Memory引擎支持
    CREATE TABLE lookup (
        id INT,
        data VARCHAR(100),
        INDEX USING HASH (id)
    ) ENGINE=MEMORY;
  3. 全文索引:适合文本搜索

    scss 复制代码
    CREATE FULLTEXT INDEX idx_content ON articles(content);
  4. 空间索引:适合地理空间数据

    scss 复制代码
    CREATE SPATIAL INDEX idx_location ON places(geom);

在实际项目中,我建议遵循"20/80"原则:为最常用的20%查询创建索引,覆盖80%的访问需求。避免为所有列都创建索引,这会增加写入开销和存储空间。

索引使用中的常见误区

很多开发者对索引的理解存在一些误区,导致索引未能发挥应有作用:

  1. 函数操作破坏索引

    sql 复制代码
    -- 错误示例:函数操作导致索引失效
    SELECT * FROM users WHERE LOWER(email) = 'john@example.com';
    
    -- 正确做法:存储时就统一大小写
    SELECT * FROM users WHERE email = 'john@example.com';
  2. LIKE通配符前置问题

    sql 复制代码
    -- 错误示例:'%'在前导致索引失效
    SELECT * FROM products WHERE name LIKE '%phone';
    
    -- 正确做法:使用全文索引或调整查询逻辑
  3. ORDER BY与索引方向

    sql 复制代码
    -- 假设有一个降序索引
    CREATE INDEX idx_price_desc ON products(price DESC);
    
    -- 这个查询能利用索引
    SELECT * FROM products ORDER BY price DESC LIMIT 10;
    
    -- 而这个查询不能
    SELECT * FROM products ORDER BY price ASC LIMIT 10;
  4. 多列索引的列顺序

    sql 复制代码
    -- 假设有一个多列索引
    CREATE INDEX idx_user_status_date ON users(status, created_at);
    
    -- 这些查询能利用索引
    SELECT * FROM users WHERE status = 'active';
    SELECT * FROM users WHERE status = 'active' AND created_at > '2023-01-01';
    
    -- 这个查询不能利用索引
    SELECT * FROM users WHERE created_at > '2023-01-01';

查询优化实战技巧

  1. EXPLAIN分析工具

    ini 复制代码
    EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'completed';

    通过EXPLAIN输出,我们可以看到:

    • 是否使用了索引
    • 扫描的行数(rows)
    • 执行计划(type)
    • 是否使用了临时表或文件排序
  2. 覆盖索引优化

    sql 复制代码
    -- 原始查询(需要回表)
    SELECT * FROM users WHERE user_id = 100;
    
    -- 优化为覆盖索引查询
    SELECT user_id, username, email FROM users WHERE user_id = 100;
    
    -- 创建覆盖索引
    CREATE INDEX idx_user_cover ON users(user_id, username, email);
  3. 索引合并策略

    sql 复制代码
    -- 假设有两个单列索引
    CREATE INDEX idx_user_id ON orders(user_id);
    CREATE INDEX idx_status ON orders(status);
    
    -- 这个查询可能触发索引合并
    SELECT * FROM orders WHERE user_id = 100 OR status = 'pending';
  4. 分区表优化

    sql 复制代码
    -- 按时间范围分区
    CREATE TABLE orders (
        order_id INT,
        user_id INT,
        order_date DATETIME,
        amount DECIMAL(10,2),
        PRIMARY KEY (order_id, order_date)
    ) PARTITION BY RANGE (TO_DAYS(order_date)) (
        PARTITION p2023 VALUES LESS THAN (TO_DAYS('2024-01-01')),
        PARTITION p2024 VALUES LESS THAN (TO_DAYS('2025-01-01'))
    );
    
    -- 查询时自动只扫描相关分区
    SELECT * FROM orders WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31';

实战案例:电商订单查询优化

假设我们有一个电商系统,需要优化以下查询:

sql 复制代码
SELECT o.order_id, o.order_date, u.username, p.product_name, o.quantity, o.amount
FROM orders o
JOIN users u ON o.user_id = u.user_id
JOIN products p ON o.product_id = p.product_id
WHERE o.order_date BETWEEN '2023-01-01' AND '2023-12-31'
AND u.city = 'Beijing'
ORDER BY o.order_date DESC
LIMIT 100;

优化步骤:

  1. 分析查询模式,发现主要按日期范围和城市筛选

  2. 创建复合索引:

    scss 复制代码
    CREATE INDEX idx_order_date_city ON orders(order_date, user_id);
    CREATE INDEX idx_user_city ON users(city, user_id);
  3. 考虑使用覆盖索引减少JOIN操作:

    sql 复制代码
    -- 优化后的查询
    SELECT o.order_id, o.order_date, u.username, p.product_name, o.quantity, o.amount
    FROM orders o
    JOIN (SELECT user_id, username FROM users WHERE city = 'Beijing') u ON o.user_id = u.user_id
    JOIN products p ON o.product_id = p.product_id
    WHERE o.order_date BETWEEN '2023-01-01' AND '2023-12-31'
    ORDER BY o.order_date DESC
    LIMIT 100;
  4. 对于历史数据,考虑分区表按年份分区

通过这些优化,原本需要扫描数百万行的查询,可能只需要扫描几千行,性能提升几个数量级。在实际项目中,我建议建立性能监控机制,定期分析慢查询日志,持续优化索引策略

3. 事务管理与并发控制:确保数据一致性的高级实践

在上一章我们讨论了如何通过索引优化提升数据库性能,但性能提升不能以牺牲数据一致性为代价。今天我们就来深入探讨事务管理和并发控制,这些技术是确保多用户环境下数据一致性的关键。

事务的基本概念与ACID特性

事务是数据库操作的基本单位,它必须满足ACID四个特性:

  1. 原子性(Atomicity) :事务中的所有操作要么全部成功,要么全部失败

    sql 复制代码
    BEGIN;
    -- 一系列操作
    INSERT INTO orders (...) VALUES (...);
    UPDATE inventory SET stock = stock - 1 WHERE product_id = 123;
    -- 如果任何操作失败
    ROLLBACK;
    -- 如果全部成功
    COMMIT;
  2. 一致性(Consistency) :事务执行前后,数据库必须始终处于一致性状态

    sql 复制代码
    -- 银行转账示例
    BEGIN;
    UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
    -- 假设这里发生错误
    UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
    COMMIT;
  3. 隔离性(Isolation) :并发执行的事务互不干扰

    sql 复制代码
    -- 两个并发事务
    -- 事务A
    BEGIN;
    SELECT balance FROM accounts WHERE account_id = 1;
    -- 事务B同时执行
    BEGIN;
    UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
    COMMIT;
    -- 事务A继续
    UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
    COMMIT;
  4. 持久性(Durability) :已提交的事务永久保存,不会因系统故障而丢失

并发控制与隔离级别

数据库系统通过锁机制实现并发控制,不同的隔离级别提供了不同的并发控制强度:

  1. READ UNCOMMITTED:最低隔离级别,允许读取未提交数据(脏读)

    sql 复制代码
    -- 事务A
    BEGIN;
    INSERT INTO test VALUES (1);
    -- 事务B可以读取到这个未提交的数据
    -- 事务A
    ROLLBACK;
  2. READ COMMITTED:大多数数据库的默认级别,防止脏读

    sql 复制代码
    -- 事务A
    BEGIN;
    SELECT * FROM accounts WHERE account_id = 1;
    -- 事务B
    BEGIN;
    UPDATE accounts SET balance = 1000 WHERE account_id = 1;
    COMMIT;
    -- 事务A再次查询会看到更新后的数据
    SELECT * FROM accounts WHERE account_id = 1;
  3. REPEATABLE READ:防止脏读和不可重复读

    sql 复制代码
    -- 事务A
    BEGIN;
    SELECT * FROM accounts WHERE account_id = 1;
    -- 事务B
    BEGIN;
    UPDATE accounts SET balance = 1000 WHERE account_id = 1;
    COMMIT;
    -- 事务A再次查询仍然看到最初的数据
    SELECT * FROM accounts WHERE account_id = 1;
  4. SERIALIZABLE:最高隔离级别,完全串行化执行

    sql 复制代码
    -- 事务A和事务B会互相阻塞
    -- 事务A
    BEGIN;
    SELECT * FROM accounts WHERE account_id = 1;
    -- 事务B
    BEGIN;
    SELECT * FROM accounts WHERE account_id = 1;
    -- 事务A和事务B会互相等待对方释放锁

死锁与性能优化

在高并发环境下,死锁是常见问题。以下是一个典型的死锁场景:

sql 复制代码
-- 事务A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 事务B
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 2;
-- 事务A继续
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
-- 事务B继续
UPDATE accounts SET balance = balance + 100 WHERE account_id = 1;
-- 死锁发生

预防死锁的最佳实践:

  1. 按固定顺序访问资源(如总是先更新account_id=1,再更新account_id=2)
  2. 保持事务简短
  3. 避免事务中的用户交互
  4. 使用适当的隔离级别

实战案例:电商库存扣减优化

考虑一个电商系统中的库存扣减场景,需要确保库存不会出现负数:

sql 复制代码
-- 传统做法(可能产生超卖)
BEGIN;
SELECT stock FROM products WHERE product_id = 123;
-- 假设返回stock=5
UPDATE products SET stock = stock - 1 WHERE product_id = 123;
COMMIT;

-- 优化做法(使用SELECT FOR UPDATE)
BEGIN;
SELECT stock FROM products WHERE product_id = 123 FOR UPDATE;
-- 确认库存足够
UPDATE products SET stock = stock - 1 WHERE product_id = 123;
COMMIT;

对于高并发场景,还可以考虑以下优化:

  1. 使用乐观锁:

    ini 复制代码
    -- 表结构添加version字段
    UPDATE products 
    SET stock = stock - 1, version = version + 1 
    WHERE product_id = 123 AND version = 5;
  2. 使用队列系统解耦扣减操作

  3. 对于超卖容忍度较高的场景,可以接受少量超卖,后续人工处理

分布式事务处理

在微服务架构中,分布式事务是常见挑战。以下是一个典型的分布式事务场景:

sql 复制代码
-- 服务A
BEGIN;
-- 扣减库存
UPDATE inventory SET stock = stock - 1 WHERE product_id = 123;
-- 调用服务B创建订单
CALL create_order(123, 1);
-- 如果服务B失败
ROLLBACK;
-- 如果成功
COMMIT;

常见的分布式事务解决方案:

  1. 两阶段提交(2PC)

    diff 复制代码
    -- 阶段1:准备
    -- 服务A准备扣减库存
    -- 服务B准备创建订单
    -- 阶段2:提交或回滚
  2. 补偿事务(TCC)

    diff 复制代码
    -- Try阶段:预留资源
    -- Confirm阶段:确认执行
    -- Cancel阶段:取消执行
  3. 本地消息表

    diff 复制代码
    -- 在每个服务本地维护消息表
    -- 通过定时任务重试未处理的消息

在实际项目中,我建议根据业务场景选择合适的方案。对于强一致性要求高的场景,可以使用2PC;对于最终一致性可接受的场景,补偿事务或本地消息表是更好的选择

4. 存储过程与触发器设计:自动化数据库操作的进阶指南

在上一章我们讨论了事务管理和并发控制,确保了数据的一致性。但数据库操作往往需要更高级的自动化能力,这就是存储过程和触发器大显身手的地方。今天我们就来深入探讨如何设计高效的存储过程和触发器,实现数据库操作的自动化。

存储过程的基础与优势

存储过程是一组预编译的SQL语句,可以接受参数并返回结果。相比直接执行SQL语句,存储过程有诸多优势:

  1. 性能提升:存储过程在创建时编译并存储执行计划,后续调用时无需重新编译

    sql 复制代码
    -- 创建存储过程
    DELIMITER //
    CREATE PROCEDURE get_user_orders(IN p_user_id INT)
    BEGIN
        SELECT o.order_id, o.order_date, p.product_name, o.quantity, o.amount
        FROM orders o
        JOIN products p ON o.product_id = p.product_id
        WHERE o.user_id = p_user_id
        ORDER BY o.order_date DESC;
    END //
    DELIMITER ;
    
    -- 调用存储过程
    CALL get_user_orders(100);
  2. 安全性增强:可以通过存储过程限制对底层表的直接访问

    sql 复制代码
    -- 撤销直接访问权限
    REVOKE SELECT ON orders FROM app_user;
    
    -- 授予执行存储过程的权限
    GRANT EXECUTE ON PROCEDURE get_user_orders TO app_user;
  3. 代码复用:复杂的业务逻辑只需编写一次,多处调用

    sql 复制代码
    -- 创建带输出参数的存储过程
    DELIMITER //
    CREATE PROCEDURE calculate_order_total(
        IN p_order_id INT,
        OUT p_total DECIMAL(10,2),
        OUT p_tax DECIMAL(10,2)
    )
    BEGIN
        DECLARE v_subtotal DECIMAL(10,2);
        
        SELECT SUM(quantity * price) INTO v_subtotal
        FROM order_items oi
        JOIN products p ON oi.product_id = p.product_id
        WHERE oi.order_id = p_order_id;
        
        SET p_total = v_subtotal;
        SET p_tax = v_subtotal * 0.08; -- 假设税率为8%
    END //
    DELIMITER ;
    
    -- 调用存储过程
    CALL calculate_order_total(123, @total, @tax);
    SELECT @total, @tax;

存储过程的进阶设计

  1. 错误处理:使用DECLARE HANDLER处理异常

    sql 复制代码
    DELIMITER //
    CREATE PROCEDURE transfer_funds(
        IN p_from_account INT,
        IN p_to_account INT,
        IN p_amount DECIMAL(10,2)
    )
    BEGIN
        DECLARE EXIT HANDLER FOR SQLEXCEPTION
        BEGIN
            ROLLBACK;
            SELECT 'Transaction failed' AS result;
        END;
        
        START TRANSACTION;
        UPDATE accounts SET balance = balance - p_amount WHERE account_id = p_from_account;
        UPDATE accounts SET balance = balance + p_amount WHERE account_id = p_to_account;
        COMMIT;
        
        SELECT 'Transaction successful' AS result;
    END //
    DELIMITER ;
  2. 游标使用:处理结果集

    sql 复制代码
    DELIMITER //
    CREATE PROCEDURE process_orders()
    BEGIN
        DECLARE done INT DEFAULT FALSE;
        DECLARE v_order_id INT;
        DECLARE v_total DECIMAL(10,2);
        DECLARE cur CURSOR FOR SELECT order_id FROM orders WHERE status = 'pending';
        DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
        
        OPEN cur;
        read_loop: LOOP
            FETCH cur INTO v_order_id;
            IF done THEN
                LEAVE read_loop;
            END IF;
            
            -- 计算订单总额
            CALL calculate_order_total(v_order_id, @total, @tax);
            
            -- 更新订单状态
            UPDATE orders 
            SET status = 'processed', 
                total_amount = @total,
                tax_amount = @tax
            WHERE order_id = v_order_id;
        END LOOP;
        
        CLOSE cur;
    END //
    DELIMITER ;
  3. 动态SQL:根据条件生成SQL

    sql 复制代码
    DELIMITER //
    CREATE PROCEDURE dynamic_query(IN p_column VARCHAR(50))
    BEGIN
        SET @sql = CONCAT('SELECT ', p_column, ' FROM users WHERE status = ''active''');
        PREPARE stmt FROM @sql;
        EXECUTE stmt;
        DEALLOCATE PREPARE stmt;
    END //
    DELIMITER ;
    
    -- 调用示例
    CALL dynamic_query('username');

触发器的设计与应用

触发器是在特定事件发生时自动执行的存储过程。合理使用触发器可以维护数据完整性、实现审计跟踪等。

  1. 数据完整性维护

    sql 复制代码
    -- 创建触发器,确保订单金额不为负
    DELIMITER //
    CREATE TRIGGER before_order_insert
    BEFORE INSERT ON orders
    FOR EACH ROW
    BEGIN
        IF NEW.amount < 0 THEN
            SET NEW.amount = 0;
        END IF;
    END //
    DELIMITER ;
  2. 审计跟踪

    sql 复制代码
    -- 创建审计表
    CREATE TABLE user_audit (
        audit_id INT AUTO_INCREMENT PRIMARY KEY,
        user_id INT,
        action VARCHAR(20),
        old_value VARCHAR(255),
        new_value VARCHAR(255),
        action_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    
    -- 创建触发器记录用户信息变更
    DELIMITER //
    CREATE TRIGGER after_user_update
    AFTER UPDATE ON users
    FOR EACH ROW
    BEGIN
        IF OLD.username != NEW.username THEN
            INSERT INTO user_audit(user_id, action, old_value, new_value)
            VALUES (OLD.user_id, 'UPDATE_USERNAME', OLD.username, NEW.username);
        END IF;
    END //
    DELIMITER ;
  3. 级联操作

    sql 复制代码
    -- 创建触发器,当用户删除时自动删除其订单
    DELIMITER //
    CREATE TRIGGER before_user_delete
    BEFORE DELETE ON users
    FOR EACH ROW
    BEGIN
        DELETE FROM orders WHERE user_id = OLD.user_id;
    END //
    DELIMITER ;

实战案例:电商订单处理流程

考虑一个完整的电商订单处理流程,我们可以设计如下存储过程和触发器:

  1. 订单创建存储过程

    sql 复制代码
    DELIMITER //
    CREATE PROCEDURE create_order(
        IN p_user_id INT,
        IN p_items JSON,
        OUT p_order_id INT
    )
    BEGIN
        DECLARE v_total DECIMAL(10,2) DEFAULT 0;
        DECLARE v_item_count INT DEFAULT 0;
        DECLARE v_item JSON;
        DECLARE v_product_id INT;
        DECLARE v_quantity INT;
        DECLARE v_price DECIMAL(10,2);
        
        -- 开始事务
        START TRANSACTION;
        
        -- 创建订单头
        INSERT INTO orders(user_id, status, created_at)
        VALUES (p_user_id, 'pending', NOW());
        
        SET p_order_id = 0;
        
        -- 处理订单项
        SET v_item_count = JSON_LENGTH(p_items);
        SET v_index = 0;
        
        WHILE v_index < v_item_count DO
            SET v_item = JSON_EXTRACT(p_items, CONCAT('$[', v_index, ']'));
            SET v_product_id = JSON_UNQUOTE(JSON_EXTRACT(v_item, '$.product_id'));
            SET v_quantity = JSON_UNQUOTE(JSON_EXTRACT(v_item, '$.quantity'));
            
            -- 检查库存
            SELECT price INTO v_price
            FROM products
            WHERE product_id = v_product_id
            FOR UPDATE; -- 加锁防止超卖
            
            -- 创建订单项
            INSERT INTO order_items(order_id, product_id, quantity, price)
            VALUES (p_order_id, v_product_id, v_quantity, v_price);
            
            -- 累加总价
            SET v_total = v_total + (v_quantity * v_price);
            
            -- 减少库存
            UPDATE products
            SET stock = stock - v_quantity
            WHERE product_id = v_product_id;
            
            SET v_index = v_index + 1;
        END WHILE;
        
        -- 更新订单总价
        UPDATE orders
        SET total_amount = v_total
        WHERE order_id = p_order_id;
        
        -- 提交事务
        COMMIT;
    END //
    DELIMITER ;
  2. 订单状态变更触发器

    sql 复制代码
    DELIMITER //
    CREATE TRIGGER after_order_status_change
    AFTER UPDATE ON orders
    FOR EACH ROW
    BEGIN
        IF OLD.status != NEW.status THEN
            -- 记录状态变更
            INSERT INTO order_status_history(order_id, status, changed_at)
            VALUES (NEW.order_id, NEW.status, NOW());
            
            -- 如果订单完成,更新用户积分
            IF NEW.status = 'completed' THEN
                UPDATE users
                SET points = points + (NEW.total_amount * 10) -- 假设每消费1元得10积分
                WHERE user_id = NEW.user_id;
相关推荐
半点寒12W1 小时前
微信小程序实现路由拦截的方法
前端
某公司摸鱼前端2 小时前
uniapp socket 封装 (可拿去直接用)
前端·javascript·websocket·uni-app
要加油哦~2 小时前
vue | 插件 | 移动文件的插件 —— move-file-cli 插件 的安装与使用
前端·javascript·vue.js
小林学习编程2 小时前
Springboot + vue + uni-app小程序web端全套家具商场
前端·vue.js·spring boot
柳鲲鹏2 小时前
WINDOWS最快布署WEB服务器:apache2
服务器·前端·windows
weixin-a153003083163 小时前
【playwright篇】教程(十七)[html元素知识]
java·前端·html
ai小鬼头4 小时前
AIStarter最新版怎么卸载AI项目?一键删除操作指南(附路径设置技巧)
前端·后端·github
一只叫煤球的猫4 小时前
普通程序员,从开发到管理岗,为什么我越升职越痛苦?
前端·后端·全栈
vvilkim4 小时前
Electron 自动更新机制详解:实现无缝应用升级
前端·javascript·electron
vvilkim4 小时前
Electron 应用中的内容安全策略 (CSP) 全面指南
前端·javascript·electron