登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

MySQL EXCEPT 怎样排除另一查询的结果行

来源:17golang原创

时间:2026-10-26 20:17:26 478浏览 收藏

要从查询 A 中排除查询 B 已有的结果行,可以把两条查询写成 A EXCEPT B。比较单位是两侧选出的完整行,默认去重;需要保留重复次数差时显式使用 EXCEPT ALL。左右交换会改变含义,EXCEPT 也不会删除原表数据。

本文以 MySQL 8.4 为说明环境;该语法从 MySQL 8.0.31 开始支持,不能只看“8.0”就判断可用。官方入口:https://dev.mysql.com/doc/refman/8.4/en/except.html。下面用“同一租户的待导出编号减去已交付编号”说明写法,示例为原创推演,未在本文环境执行。

先保护查询范围:差集不等于权限过滤

在设计导出名单时,我会先把要保护的对象写下来:候选编号、所属租户,以及排除依据。这样看 SQL 时,注意力不会只停留在 EXCEPT 这个关键字上。它只负责差集,不能自动替应用补上访问范围。

例如某租户的候选编号中有 102,如果右侧查询把其他租户的 102 也拿来比较,当前租户的候选就可能被错误排除。这个问题首先影响结果完整性;如果左侧本身漏了范围约束,差集也不会阻止越权数据进入结果。租户值应来自已授权的服务端上下文,并通过驱动参数绑定传入。

这里的风险分级按后果理解:漏掉左侧范围可能涉及数据暴露,漏掉右侧范围可能造成误排除,重复语义选错则会改变导出数量。本文只围绕这条差集查询处理这些边界。

沿着列投影找出误排除的路径

EXCEPT 不会根据列名推测主键,也不会自动找关联键。左右列数必须一致,对应位置应使用一致的数据类型和业务含义。如果左侧选出 customer_id, status,右侧也选出这两列,那么编号相同而状态不同的行仍是不同元组。只想按编号排除,就让两侧都只投影编号。

候选范围和排除范围共同约束比较列的静态结构图
静态关系说明图:比较对象由两侧 SELECT 列决定,租户范围分别约束两侧输入。

我倾向于在这里逐列写出字段,避免使用 SELECT *:表结构增加字段后,参与比较的内容可能随之变化。最终列名来自第一个查询块;对应位置类型不同时 MySQL 会综合两侧决定结果类型,因此也不要依靠隐式转换修正业务口径。字符串比较还需要关注列的字符集与排序规则。

用独立样例明确 DISTINCT 与 ALL

先用只读 CTE 构造一组可理解的数据。左侧 102 出现三次,右侧出现一次;左侧还有 101、103,右侧另有 104。下面每一条 SQL 都是独立语句,不创建或修改业务表。

-- 唯一名单:右侧出现过的编号从左侧集合中排除
WITH candidate AS (
    SELECT 101 AS customer_id UNION ALL
    SELECT 102 UNION ALL SELECT 102 UNION ALL SELECT 102 UNION ALL
    SELECT 103
), delivered AS (
    SELECT 102 AS customer_id UNION ALL SELECT 104
)
SELECT customer_id FROM candidate
EXCEPT DISTINCT
SELECT customer_id FROM delivered
ORDER BY customer_id;

按照集合语义,预期只剩 101、103。即使左侧有多条 102,DISTINCT 也不会留下其中两条;右侧出现过该值,就足以从唯一名单中排除它。省略 DISTINCT 是同一个含义。

-- 次数差:同值在左侧的次数减去右侧次数,最少为零
WITH candidate AS (
    SELECT 101 AS customer_id UNION ALL
    SELECT 102 UNION ALL SELECT 102 UNION ALL SELECT 102 UNION ALL
    SELECT 103
), delivered AS (
    SELECT 102 AS customer_id UNION ALL SELECT 104
)
SELECT customer_id FROM candidate
EXCEPT ALL
SELECT customer_id FROM delivered
ORDER BY customer_id;

这条的预期序列为 101、102、102、103:102 剩余两次。一般可把某个元组的剩余次数理解为 max(左侧次数 - 右侧次数, 0)。ALL 适合要保留多重集合次数的任务,不等于“右侧有这个编号也保留左侧全部重复行”。如果记录各有独立业务身份,应把真实身份列纳入契约,而不是指望重复次数替代身份。

把租户范围与比较列写进查询

回到导出场景,假定两个查询来源都包含 tenant_id、customer_id,交付来源还有 delivery_status。下面只生成唯一客户编号,不携带姓名等额外信息。问号是数据库驱动的预处理参数,占位值在应用中绑定,不能原样当作已填值 SQL 粘贴到普通客户端运行。

-- 两个参数分别绑定同一授权租户,不能由任意请求字段替代授权判断
SELECT customer_id
FROM export_candidate
WHERE tenant_id = ?
EXCEPT DISTINCT
SELECT customer_id
FROM delivery_record
WHERE tenant_id = ?
  AND delivery_status = 'delivered'
ORDER BY customer_id;

两侧都限定租户,右侧还明确“已交付”的状态。把所有历史状态都纳入右侧,会把未成功交付的编号也排除。若导出对象其实是“客户 + 活动”组合,两侧就都应选出这两个字段,并在最终排序中使用相同的结果字段。

DISTINCT唯一名单与ALL次数差的结果契约结构图
静态结构说明图:DISTINCT 服务于唯一名单,ALL 服务于重复次数差,排序和审计属于结果契约。

最终 ORDER BY 控制差集结果的显示顺序;不能依赖原表顺序。如果要对某一侧单独做 ORDER BY、LIMIT,需要用括号明确查询块范围。先给左侧加 LIMIT 再做差集,表达的是“先截取候选再排除”,并不保证最终还能得到同样数量的行。

给结果留下可追溯的口径

我会把这条查询的口径与导出任务一起记录:授权租户、候选范围、右侧状态条件、比较列、DISTINCT 或 ALL、执行时间以及结果数量。这里的“审计”是保留业务解释,不是把原始名单和敏感字段无差别写进日志。

如果事后需要重现同一结果,仅存 SQL 文本还不够:源数据可能已经变化。应在任务设计中说明结果文件或快照的保存策略,避免把两次不同时间的查询误认为同一数据状态。也不要宣称 EXCEPT 必然更快;数据量、重复分布及实际执行计划都会影响成本。

语法与版本依据分别见 MySQL EXCEPT 官方说明、集合查询的列、排序与组合规则和 8.0.31 发布说明。本文的租户导出场景与样例数据为原创设计。

使用前逐项确认结果含义

  • 服务器版本支持 EXCEPT,不能把所有 MySQL 8.0 版本视为相同。
  • 左侧是要保留的候选,右侧是排除依据,两侧范围已分别明确。
  • 比较列数量、位置、类型与业务含义一致,额外说明字段没有混入比较键。
  • 唯一名单选择 DISTINCT;确实需要次数相减才使用 ALL。
  • 最终结果有明确排序,LIMIT 的位置与业务要求相符。

为什么同一个编号还是留在结果里?

先看两侧是否还选了其他列。EXCEPT 比较完整元组,相同编号搭配不同状态,并不是相同行。然后再看右侧条件是否实际覆盖了目标编号。

EXCEPT 会删除候选表中的行吗?

不会。本文的语句返回查询结果,原始行仍在来源表中。不要把得到差集名单误当成完成了删除或状态更新。

反过来写能得到同样结果吗?

不能保证。A EXCEPT B 与 B EXCEPT A 表达不同方向,右侧独有的 104 就是本例中的直观区别。

多个集合运算混用时怎样表达清楚?

用括号写清业务分组。MySQL 的 INTERSECT 优先于 UNION 和 EXCEPT,不应把不带括号的长表达式按自然阅读顺序猜测。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>