资讯动态

SQL多表查询、事务与DCL:数据库读写到权限管理实战

发布时间:2026/10/21 6:28:01 来源:尧图企业网站定制
如果你正跟着数据库学习计划走到第四天前面的建库建表、增删改查和单表查询已经练得比较熟那今天要聊的多表查询、事务和DCL就是把你从“单表工具人”变成“能设计真实业务数据操作”的分水岭。很多人觉得多表查询就是多写几个JOIN事务就是BEGIN和COMMITDCL就是GRANT但真到了项目里Join几条大表跑出来的结果不对事务并发出现脏数据权限开太大被运维找上门才发现每一个点都值得认真抠。这篇文章按Day04的学习主线展开讲清三块内容的核心逻辑、实操命令和避坑经验适合正在系统学SQL的人也适合工作一两年想补基础的同学。读完不需要记太多花哨语法只要跟着例子走一遍再回头看日常脚本里那些“不知道为什么能跑”的SQL会有新的理解。1. 为什么把多表查询、事务、DCL放在同一天学习1.1 三个主题指向的是同一个核心能力实际业务里的数据几乎不会只待在一张表里。用户表放用户信息订单表放订单信息订单明细表放商品信息它们之间靠外键关联。只有多表查询能把它们按照关系拼起来让前端页面直接拿到一个完整结果集。但光会读是不够的写操作还必须考虑一致性。下单这个动作可能同时要扣库存、生成订单、记录日志任何一步失败都不能让其他步骤“半成功”。事务就是让多条写操作变成一个整体。再往下谁有权限执行这些操作DCL就是管这个的。所以你会发现多表查询、事务、DCL分别在“读”“写”“管”三个层面协作。前三天学的是单表内部的操作第四天开始跳出单表去处理表与表之间、操作与操作之间、用户与资源之间的关系。Day04把它们放在一起不是为了凑知识点而是希望学习者在同一个阶段建立起完整的数据操作视角而不是孤立地背命令。1.2 我的学习顺序建议我见过不少同学今天急着把 JOIN 花式写法全背下来明天又想把事务隔离级别整明白最后遇到问题仍然一脸懵。正确顺序应该按数据流来走先用一套简单业务表把多表查询写熟再通过一个转账场景理解事务最后按角色分配权限。多表查询影响的是读事务影响的是写DCL影响的是管理边界把读、写、管分开练遇到问题才能准确定位。举个例子一个订单列表页面要显示客户姓名、订单编号、总金额、商品清单如果只会单表查询你得先查订单再循环查客户然后在业务代码里组装。学会多表查询后一条JOIN就能直接把数据取到同一结果集。订单提交时则要考虑事务比如下单同时要扣库存、加积分、生成物流单任何一步失败都应该回滚。至于账号权限开发账号、只读账号、管理账号必须分得清清楚楚否则上线第一天就会被运维约谈。这三部分连起来正好是一套真实项目数据操作的最小闭环。2. 多表查询会JOIN是第一步知道为什么是JOIN才是进阶多表查询先聊因为它最容易做出直观结果也最容易出错。很多人写 JOIN 非常熟练但问他为什么用左连接不用内连接为什么子查询里 NOT IN 会返回空就答不上来。这一章我们不用复杂案例就建一套能练手的小表把连接查询、自连接、子查询这些点逐个跑通。2.1 先建一套能练手的业务表在实际项目里我不建议直接拿生产环境表练手强烈建议自建一个迷你订单模型。下面这三张表足够覆盖大部分连接场景CREATE TABLE customer ( id INT PRIMARY KEY, name VARCHAR(50) ); CREATE TABLE orders ( id INT PRIMARY KEY, customer_id INT, total_amount DECIMAL(10,2), CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customer(id) ); CREATE TABLE order_item ( id INT PRIMARY KEY, orders_id INT, goods_name VARCHAR(50), price DECIMAL(10,2), CONSTRAINT fk_orders FOREIGN KEY (orders_id) REFERENCES orders(id) );插入模拟数据INSERT INTO customer VALUES (1,张三),(2,李四),(3,王五); INSERT INTO orders VALUES (101,1,500.00),(102,1,300.00),(103,2,800.00); INSERT INTO order_item VALUES (1001,101,键盘,200.00), (1002,101,鼠标,300.00), (1003,103,显示器,800.00);为什么故意让王五没有订单让张三有两张订单让订单102没有商品明细就是为了练出“连接后缺数据”和“连接后多数据”的效果。很多初学者拿到的练习数据整整齐齐反而测不出 JOIN 的边界行为。数据本身不干净是真正业务表和练习表最大的区别。2.2 内连接两张表都满足连接条件才会出现查询所有下过单的客户以及订单金额SELECT c.name, o.total_amount FROM customer c INNER JOIN orders o ON c.id o.customer_id;结果里会有张三和李四王五不会出现。这就是内连接的语义取两表的交集。面试常问的INNER JOIN和 WHERE 里写笛卡尔积过滤没有本质区别但 JOIN 语法把连接条件和筛选条件分开了可读性更好。比如只需要看金额大于400的订单可以在后面加WHERE o.total_amount 400。注意不要把连接条件也塞进 WHERE除非你写的是老式逗号连接。这个例子看似简单但你要理解一个关键点内连接只返回匹配成功的行。真实业务里“查所有下单客户”和“查所有客户及其订单”的差别就是内连接和外连接的差别。用错连接类型最典型的问题就是少数据。2.3 外连接主表数据必须保留副表没有就用NULL补业务中经常需要“所有客户都要显示没下单的也要显示”这时用左连接SELECT c.name, o.id, o.total_amount FROM customer c LEFT JOIN orders o ON c.id o.customer_id;王五会返回一行订单相关字段是NULL。注意 LEFT JOIN 的结果行数至少等于左表行数如果左表一条记录对应右表多条记录行数会膨胀。右外连接只是把主表换到右边SELECT c.name, o.id, o.total_amount FROM orders o RIGHT JOIN customer c ON o.customer_id c.id;MySQL 没有全外连接想要“两边都保留”时只能用左连接和右连接的 UNION 模拟。实际项目里我基本只用 LEFT JOIN因为大家习惯把主表写在前面这样先读主表、再补副表字段逻辑非常顺。真正要注意的是连接后行数膨胀的问题如果你需要的是“每个客户一行”然后关联出他的订单聚合结果就不能直接 JOIN 订单表后不管。2.4 自连接一张表和自己比较必须给表起别名自连接最常见的场景是员工表里存着上级ID。假设有一张 emp 表CREATE TABLE emp ( id INT PRIMARY KEY, name VARCHAR(50), manager_id INT ); INSERT INTO emp VALUES (1,赵老板,NULL), (2,钱主管,1), (3,孙专员,2);查询每个员工及其上级姓名SELECT e.name AS 员工, m.name AS 上级 FROM emp e LEFT JOIN emp m ON e.manager_id m.id;这里如果不给表起别名同一个表出现两次会直接报错。自连接的本质是把一张表“复制”成两份逻辑表再关联所以别名不是可选项而是必须项。查询结果里老板的上级是NULL符合预期。自连接除了员工上级场景还常用于商品分类、组织树、关系链等数据模型凡是同一张表里的行之间存在层级关系基本都逃不掉自连接。2.5 子查询和EXISTS的使用边界子查询不一定比 JOIN 差要分场景。先看最直观的查“没有下过单的客户”用 NOT IN 或 NOT EXISTSSELECT * FROM customer WHERE id NOT IN ( SELECT customer_id FROM orders );如果orders.customer_id里有 NULLNOT IN 会返回空结果因为NOT IN (NULL, ...)的结果是 UNKNOWNWHERE 只保留 TRUE所以全部不返回。这一点我见很多人踩坑。更稳的写法是 NOT EXISTSSELECT * FROM customer c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id c.id );EXISTS 只要子查询返回一行就为真性能上通常适合“判断存在性”而 IN 适合返回少量确定值列表。现在 MySQL 优化器有能力做很多改写但语义差异依然存在。我在项目中习惯用 EXISTS 做关联子查询用 IN 做不相关的集合判断这样的代码自己回看时也容易理解。如果你还想深挖性能可以在这些 SQL 前面加EXPLAIN观察执行计划里的 type 字段和驱动表。优化器大多数时候是靠谱的但如果发现选错了驱动表可以用STRAIGHT_JOIN强制指定不过这是调优话题Day04 阶段先把语义写对更重要。3. 事务ACID不是眼前一排文字而是一套保护数据的规则多表查询解决的是读接下来看写。写操作最怕的不是写错而是写到一半“卡住”留下一堆不一致的数据。事务就是让多条写操作成为一个整体要么全成功要么全失败。3.1 一个转账场景把事务说清楚假设有账户表CREATE TABLE account ( id INT PRIMARY KEY, name VARCHAR(50), balance DECIMAL(10,2) ); INSERT INTO account VALUES (1,张三,1000),(2,李四,1000);转账逻辑张三账户减100李四账户加100。这两条 UPDATE 必须作为一个整体。如果没有事务第一条执行后程序崩溃张三钱少了李四没收到谁都说不清楚。用事务包起来后要么两条都提交要么都回滚。事务的四个特性我建议结合场景理解而不是死背。原子性指一组操作不可分割一致性指事务完成前后数据满足约束比如转账前后总余额不变隔离性指多个事务并发时互不干扰持久性指提交后的修改不会因为数据库重启而丢失。面试时如果只会背这四个英文单词很容易被追问噎住但如果你能从转账、库存、流水这些真实场景里各举一个例子说服力完全不同。3.2 事务命令实操提交、回滚、保存点在 MySQL 命令行里执行START TRANSACTION; UPDATE account SET balance balance - 100 WHERE id 1; UPDATE account SET balance balance 100 WHERE id 2; COMMIT;如果在中途发现第二条 UPDATE 条件写错可以执行ROLLBACK;两条更新都会撤销。还有更细的控制方式START TRANSACTION; UPDATE account SET balance balance - 100 WHERE id 1; SAVEPOINT after_debit; UPDATE account SET balance balance 100 WHERE id 2; -- 发现加错了回滚到保存点 ROLLBACK TO after_debit; RELEASE SAVEPOINT after_debit; COMMIT;这样只回滚保存点之后的修改第一条扣款仍然保留。保存点适合处理长事务里的局部错误但如果业务逻辑已经复杂到需要保存点我会建议直接拆成多个事务因为长事务本身会持有锁更久容易造成锁等待。这里有几个非常容易踩的坑。MySQL 默认每条单独的 SQL 都是自动提交所以你不写 START TRANSACTION每条 UPDATE 就是独立事务失败不会回滚之前的语句。另外 InnoDB 才支持事务MyISAM 不支持如果你用的旧表是 MyISAM事务看起来就是“无效”的。还有一点DDL 语句会隐式提交当前事务比如事务里执行 CREATE TABLE之前未提交的修改会直接提交这点很反直觉但在日常脚本里经常遇到。3.3 隔离级别并发事务之间到底隔离到什么程度并发事务可能产生三类问题脏读事务A修改了数据但未提交事务B读到修改后的值A回滚后B的数据就成了幻觉。不可重复读事务B前后两次读同一行第一次读到100期间事务A提交改成200第二次变成200同一事务内读到的值不稳定。幻读事务B按条件查询出一个结果集期间事务A插入了符合条件的新记录事务B再次按同样条件查询多出原本不存在的行。不同隔离级别解决不同问题隔离级别脏读不可重复读幻读读未提交 READ UNCOMMITTED可能可能可能读已提交 READ COMMITTED不会可能可能可重复读 REPEATABLE READ不会不会标准SQL可能InnoDB通过间隙锁基本避免串行化 SERIALIZABLE不会不会不会MySQL 默认隔离级别是 REPEATABLE READ和 Oracle 默认 READ COMMITTED 不同。查看当前隔离级别SELECT transaction_isolation;练习时可以开两个会话分别执行SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;再观察事务B在事务A提交前后读到的值。不用死记自己跑一遍比背十遍都牢。3.4 锁、死锁和事务性能事务并发写入需要加锁。SELECT ... FOR UPDATE可以手动对读取的行加排他锁但加锁范围要清楚如果 WHERE 条件带索引锁的是具体行或区间如果 WHERE 条件列没有索引InnoDB 可能锁表性能下降明显。死锁最常见的原因是两个事务以不同顺序更新相同资源。事务A先锁账号1再想锁账号2事务B先锁账号2再想锁账号1互相等待。InnoDB 会检测到死锁并回滚其中一方报Deadlock found。避免死锁的实操原则有三条固定更新顺序事务尽量短不要在事务里做耗时查询或等待用户输入。这三个原则平时看着简单真正设计代码时最容易违反。4. DCL权限管理做得乱数据库早晚出事故读和写都搞清楚了最后一道管住边界的门就是 DCL。相比多表查询和事务DCL 的命令并不多但它的重要性在真实环境里被严重低估。很多项目上线后出事故根因不是SQL写错而是权限开太大。4.1 用户是权限的最小单元且要区分主机MySQL 中的用户不像其他系统只是一个名字而是“用户名可登录主机”的组合。例如app_userlocalhost只允许本机连接app_user%允许任意主机连接它们是两个不同账号。练习时创建用户CREATE USER app_userlocalhost IDENTIFIED BY App123456;创建后可以查看权限SHOW GRANTS FOR app_userlocalhost;这里要特别记住MySQL 8.0 开始不支持直接GRANT ALL ON ... TO 新用户主机 IDENTIFIED BY ...来创建用户必须先 CREATE USER再 GRANT。这个行为变化让很多从老版本习惯带过来的同事报错过。4.2 GRANT授权精确到库、表、列GRANT 的基础语法GRANT SELECT, INSERT, UPDATE ON db_shop.orders TO app_userlocalhost;如果希望一个账号对某个库有全部权限可以写成GRANT ALL PRIVILEGES ON db_shop.* TO app_userlocalhost;DCL 还能精确到列级权限。比如只允许某个账号查询 customer 表的 name 字段GRANT SELECT (name) ON db_shop.customer TO clerklocalhost;授权之后如果发现不生效执行FLUSH PRIVILEGES;。虽然大多数情况下 GRANT 语句会直接修改内存中的权限表但刚接触 DCL 时养成刷新习惯没坏处。更重要的是每次授权后都要用SHOW GRANTS FOR 账号主机;确认一下权限这个东西嘴上说“给了”不算查出来才算。4.3 REVOKE回收权限与删除用户回收权限REVOKE SELECT ON db_shop.customer FROM clerklocalhost;撤销某用户的所有权限REVOKE ALL PRIVILEGES, GRANT OPTION FROM app_userlocalhost;删除账号DROP USER app_userlocalhost;注意 GRANT OPTION 是一种特殊权限代表账号能否把权限继续转授给其他人。回收时如果不带 GRANT OPTION某些拥有转授权限的账号可能依然能把权限传出去所以完整回收要写全。这一点很多文章不会专门提但恰恰是 DCL 最容易被忽视的安全隐患。4.4 权限分配的最小化和日常审查项目中的常见错误是给应用程序账号开通root或者干脆开整个库的ALL PRIVILEGES。开发环境可以忍生产环境一旦出现 SQL 注入或者误操作就是大事故。我见过一个项目给只读报表账号也开了 DELETE 权限结果有人在测试环境顺手清了一张历史表恢复数据花了整整一天。正确做法是按模块拆账号。订单服务只用能操作 orders 相关表的账号报表服务用只读账号后台管理账号才允许 DELETE 和 UPDATE。日常定期检查SELECT user, host, plugin FROM mysql.user; SHOW GRANTS FOR app_userlocalhost;修改密码用ALTER USER app_userlocalhost IDENTIFIED BY NewPass123;权限管理是运维的边界也是对业务负责。Day04 里 DCL 命令虽然最少但在真实公司里最容易被忽略也最容易在出问题时被追责。5. 常见问题与避坑实录这一章更像我的个人排错笔记把多表查询、事务、DCL 三个主题里高频踩过的坑集中列一下。你以后遇到类似报错可以直接对照排查。5.1 多表查询结果行数为什么膨胀了一个客户对应多个订单JOIN 订单表后每个订单都会带出客户信息所以客户行会重复。如果需求是“取客户列表并显示第一个订单”直接 JOIN 后 GROUP BY 很容易取错。我常用的办法是先按客户聚合订单再关联或者用窗口函数。不要以为 JOIN 一定只是“拼接”要留意关联键是否一对多业务语义是“一行客户对应多行订单”还是“一行订单对应一行客户”。5.2 事务好像没生效先确认表引擎是否为 InnoDBSHOW TABLE STATUS LIKE account;再查看是否自动提交SELECT autocommit;如果为1但没写 START TRANSACTION每条 UPDATE 都会独立提交。还有一种情况是事务里执行了 DDL导致隐式提交。排查事务问题最重要的一点是把终端拆成两个会话别在同一个查询窗口里既写又查否则你看到的“成功回滚”可能只是假象。5.3 NOT IN 遇到 NULL 直接查不出来前面已经说过WHERE id NOT IN (SELECT customer_id FROM orders)如果 orders.customer_id 有空值最终结果集为空。原因不是数据真的不存在而是 SQL 三值逻辑里 NULL 参与 NOT IN 的结果是 UNKNOWNWHERE 只保留 TRUE。解决方案是不允许该列出现 NULL或者在子查询里先过滤掉 NULL或者改用 NOT EXISTS。我个人最推荐 NOT EXISTS因为它语义更清晰也不容易受到 NULL 干扰。5.4 权限报错Access denied 但账号确实存在先用 SHOW GRANTS 看账号权限再确认当前连接用的主机是否匹配。比如账号是app_user%客户端从本机连接时 MySQL 可能会优先匹配更精确的app_userlocalhost而本机账号没有权限因此报错。解决方式是把两处主机指向统一或者把授权语句明确写到对应的主机范围。5.5 锁等待超时怎么办出现Lock wait timeout exceeded时用SHOW PROCESSLIST;找到处于 Waiting for lock 的会话再用KILL 线程ID;结束阻塞源。根本解法是优化事务时长、检查是否忘记提交。这里有一个很实用的习惯在事务代码里设置超时时间比如在 JDBC 层设置事务超时避免一条事务占着锁把整张表堵死拖垮所有业务请求。DCL权限整理这件事我一直建议放在项目上线清单里越早做越省心。每次发布前把开发库、测试库、生产库的账号权限列成一张表逐条核对谁有写权限、谁有删权限、谁只有只读权限很多夜间告警都能提前避免。Day04的内容大概就是这样如果后面你练到调优阶段再看这些命令会发现当初觉得不起眼的细节全都有它的用途。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑