news 2026/9/20 10:50:58

SQL经典练习题解析:如何查询同时使用红色螺母和蓝色螺丝刀的工程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL经典练习题解析:如何查询同时使用红色螺母和蓝色螺丝刀的工程

在数据库圈子里混久了就会发现,教科书上的题往往比很多生产环境的SQL更能考验我们对关系运算的理解。前几天整理SQL题库时,我重新过了一遍供应商-零件-工程项目(S-P-J)这套经典关系数据库的练习题,其中一道题让我印象格外深,就是10-94这道:查询同时使用红色的螺母零件和蓝色的螺丝刀零件的工程。题面简洁到只有一句话,但它把集合语义、多表关联、子查询和去重时机全都串起来了,几乎可以当成SQL进阶的综合性考题。这篇文章就围绕这道题展开,从表结构设计到几种SQL写法,再到实际跑数据时的调优和踩坑,一次性聊透。正在准备面试的朋友可以当复习材料,平时写业务查询的开发同学,也能从“同时使用”这种看似简单却容易写错的条件里,找到一些通用的排查思路。

1. 从10-94这道题看教学数据库里的经典场景

1.1 SPJ到底在模拟什么业务

凡是系统学过关系数据库的人,基本都绕不开S-P-J这组表。供应商S、零件P、工程项目J,以及中间的供应关系SPJ,其实是在模拟一条非常典型的供应链业务:若干个供应商向不同的工程项目供货,每个项目需要多种零件,每种零件又可以由多个供应商提供。日常生产里的采购系统、物料系统、库存系统,本质上跟它是一样的,只不过表名换成了t_supplier、t_material、t_project之类。

这道题里特意把零件限定为“红色的螺母”和“蓝色的螺丝刀”,说明在同一套数据里,零件表P中同名或同颜色的零件可能不止一种,比如红色螺母可能有好几个规格,蓝色螺丝刀也可能有不同型号。而在SPJ关系表中,一个工程的编号(JNO)会对应多条供应记录,每条记录关联一种零件。要判断一个工程是否“同时使用”了两类零件,不能简单地在一条记录里找两个条件同时成立,因为一条SPJ记录只能对应一种零件,红色螺母和蓝色螺丝刀必然出现在不同的记录行里。

这也是教学题最有价值的地方:它逼着你从“行内条件叠加”的惯性思维里跳出来,切换到“集合之间的包含关系”这个维度。你找的不是某一行满足A且B,而是这个工程的零件集合,同时包含A类和B类。

1.2 题面里三个隐藏的查询要求

很多人一上来就写WHERE P.COLOR='红' AND P.PNAME='螺母' AND P.COLOR='蓝' AND P.PNAME='螺丝刀',这种写法一眼看过去就自相矛盾,因为一条记录不可能同时是红色又是蓝色。就算有人把两组条件拆开,用OR连接,那也只能得到“至少使用其中一种零件”的工程,跟题目的“同时使用”仍然不是一回事。

拆解一下“同时使用红色的螺母零件和蓝色的螺丝刀零件”这个句子,里面其实包含三个逻辑要求:

  • 第一,这个工程里必须存在至少一条供应记录,关联的零件是红色螺母;
  • 第二,这个工程里必须存在至少一条供应记录,关联的零件是蓝色螺丝刀;
  • 第三,上面两个条件针对的是同一个工程编号,说的是同一个JNO。

换句话说,题目的本质是求两个“工程编号集合”的交集。第一个集合是“使用过红色螺母的工程编号集合”,第二个集合是“使用过蓝色螺丝刀的工程编号集合”,取交集后再回到J表里把工程名称等信息查出来。只要把这个语义理清了,写SQL的思路就会非常清晰,后面各种写法都可以看作是对这个语义的不同实现方式。

2. 建表与测试数据准备

2.1 经典四表结构与约束

要跑通这道题,先得有一份可以反复折腾的实验环境。经典的S-P-J数据库包含四张表,在实际教学和练习中,通常这样建:

CREATE TABLE S ( SNO CHAR(2) PRIMARY KEY, SNAME VARCHAR(20), STATUS INT, CITY VARCHAR(20) ); CREATE TABLE P ( PNO CHAR(2) PRIMARY KEY, PNAME VARCHAR(20), COLOR VARCHAR(10), WEIGHT INT ); CREATE TABLE J ( JNO CHAR(2) PRIMARY KEY, JNAME VARCHAR(20), CITY VARCHAR(20) ); CREATE TABLE SPJ ( SNO CHAR(2), PNO CHAR(2), JNO CHAR(2), QTY INT, PRIMARY KEY (SNO, PNO, JNO) );

这里有几个细节值得注意。S表里的供应商编号是主键,P表里的零件编号是主键,J表里的工程编号是主键,而SPJ表则是用三个外键组合成联合主键。之所以联合主键能成立,是因为业务上允许同一个供应商给同一个工程供应同一种零件时,数据合并成一行,用QTY字段记录供应总量;如果允许同一对组合出现多条记录,那就还需要再引入流水号字段。

我个人在实际建表练习时,通常会顺手把外键约束也加上,虽然做查询题的时候外键不影响结果,但加外键能更真实地模拟生产环境,而且在做删除和更新操作时能避免把数据搞乱。需要注意的是,在学校教材里,SPJ表的字段名有时也写成PNUM、JNUM或者QTY之外的别名,做题目之前先看清题目给的表名和字段名。

2.2 造一套能复现题目的模拟数据

练习查询最重要的是数据能不能覆盖各种边界情况。我只造了几条关键数据来演算这道题,但如果你自己练,建议多造几组。

INSERT INTO P VALUES ('P1', '螺母', '红', 12); INSERT INTO P VALUES ('P2', '螺丝刀', '蓝', 20); INSERT INTO P VALUES ('P3', '螺母', '蓝', 14); INSERT INTO P VALUES ('P4', '螺丝刀', '红', 25); INSERT INTO P VALUES ('P5', '螺栓', '红', 18); INSERT INTO J VALUES ('J1', '造船工程', '上海'); INSERT INTO J VALUES ('J2', '桥梁工程', '武汉'); INSERT INTO J VALUES ('J3', '汽车生产线', '广州'); INSERT INTO J VALUES ('J4', '发电站建设', '成都'); INSERT INTO SPJ VALUES ('S1', 'P1', 'J1', 200); INSERT INTO SPJ VALUES ('S2', 'P2', 'J1', 150); INSERT INTO SPJ VALUES ('S3', 'P3', 'J2', 90); INSERT INTO SPJ VALUES ('S1', 'P4', 'J2', 60); INSERT INTO SPJ VALUES ('S2', 'P1', 'J3', 120); INSERT INTO SPJ VALUES ('S2', 'P5', 'J1', 80); INSERT INTO SPJ VALUES ('S1', 'P2', 'J4', 45); INSERT INTO SPJ VALUES ('S3', 'P4', 'J3', 70); INSERT INTO SPJ VALUES ('S1', 'P1', 'J4', 30); INSERT INTO SPJ VALUES ('S2', 'P2', 'J4', 55);

这套数据里特意设计了几种情况:J1同时用到了红色螺母P1和蓝色螺丝刀P2,是正确答案;J2用了蓝色螺母P3和红色螺丝刀P4,虽然颜色和工具类型都出现了,但颜色和名称的搭配对不上,不属于题目要求;J3只用了红色螺母,缺少蓝色螺丝刀;J4虽然同时用到了P1和P2,但如果某道题里只想查“红色螺母P1”和“蓝色螺丝刀P2”,那J4也符合。为了让结果更直观,我在原本的测试集里把J4也设计成同时具备这两类零件的工程,方便验证DISTINCT和JOIN组合是否会产生重复结果。

3. 三种可行的SQL写法与思路拆解

3.1 直接用JOIN连接零件条件和工程条件

最容易被初学者接受的是把SPJ表自连接两次,每次连接一种零件条件,然后关联到工程表。

SELECT DISTINCT J.JNO, J.JNAME FROM J JOIN SPJ AS SPJ_RED ON J.JNO = SPJ_RED.JNO JOIN P AS P_RED ON SPJ_RED.PNO = P_RED.PNO AND P_RED.PNAME = '螺母' AND P_RED.COLOR = '红' JOIN SPJ AS SPJ_BLUE ON J.JNO = SPJ_BLUE.JNO JOIN P AS P_BLUE ON SPJ_BLUE.PNO = P_BLUE.PNO AND P_BLUE.PNAME = '螺丝刀' AND P_BLUE.COLOR = '蓝';

这段SQL的基本逻辑是:先把J和SPJ_RED连接,筛选出用了红色螺母的工程;再把这个结果集和SPJ_BLUE连接,筛选出同时用了蓝色螺丝刀的工程。最后加DISTINCT,是因为同一工程可能有多个供应商供应红色螺母,或者有多条SPJ记录,直接JOIN可能产生重复的工程号。

从执行过程的角度理解,这种写法本质上是做了两次过滤。第一次过滤得到集合A,也就是使用红色螺母的工程列表;第二次过滤是在A的基础上继续关联蓝色螺丝刀,得到的是同时满足两个条件的工程。整个过程非常直观,而且如果WHERE条件不小心写进了JOIN子句,执行计划也不会报错,但结果可能完全不同。

需要注意,这里我把P表中的颜色和名称条件直接写在JOIN的ON子句里,而不是写成WHERE。这两种位置对INNER JOIN来说结果往往是一样的,但由于SQL的语义是先连接后过滤,ON和WHERE的作用时机不同。在实际业务中,如果用的是LEFT JOIN,ON和WHERE的结果差异就会非常大。养成好习惯,把连接条件和过滤条件分层写,逻辑会清楚很多。

3.2 用IN加AND表达集合交集

如果不习惯自连接,可以用两个IN子查询加AND来实现。

SELECT J.JNO, J.JNAME FROM J WHERE J.JNO IN ( SELECT SPJ_RED.JNO FROM SPJ SPJ_RED JOIN P P_RED ON SPJ_RED.PNO = P_RED.PNO WHERE P_RED.PNAME = '螺母' AND P_RED.COLOR = '红' ) AND J.JNO IN ( SELECT SPJ_BLUE.JNO FROM SPJ SPJ_BLUE JOIN P P_BLUE ON SPJ_BLUE.PNO = P_BLUE.PNO WHERE P_BLUE.PNAME = '螺丝刀' AND P_BLUE.COLOR = '蓝' );

这种写法在逻辑上最接近“集合求交集”的语义。第一个子查询返回所有用过红色螺母的工程编号,第二个子查询返回所有用过蓝色螺丝刀的工程编号,外层J表只保留那些同时出现在两个集合里的编号。

它的一个好处是:不需要考虑DISTINCT问题。因为IN子查询天然只关心“是否存在”,即使同一个工程在子查询结果里出现多次,也不会影响外层判断。这在数据量大的时候尤其省心,不用反复去思考JOIN之后结果集的行数增长情况。另一个好处是写法容易扩展。如果题目再加一个条件,比如还要使用绿色的扳手,只需要继续在后面加一行AND J.JNO IN (SELECT ...),代码改动的成本极低。

当然,这种写法也有短板。在市面上常见的关系型数据库里,如果子查询的结果集特别庞大,而优化器能力又有限,可能出现子查询被反复执行的情况。生产环境里遇到这种问题时,可以先在子查询里把结果集算小,或者考虑改成临时表、CTE,这些方案我们在第六节里讨论。

3.3 用EXISTS判断存在性

在面试中非常受青睐的写法是EXISTS,它把“同时使用”翻译成“存在一条记录满足红色螺母,并且存在另一条记录满足蓝色螺丝刀”。

SELECT J.JNO, J.JNAME FROM J WHERE EXISTS ( SELECT 1 FROM SPJ SPJ_RED JOIN P P_RED ON SPJ_RED.PNO = P_RED.PNO WHERE SPJ_RED.JNO = J.JNO AND P_RED.PNAME = '螺母' AND P_RED.COLOR = '红' ) AND EXISTS ( SELECT 1 FROM SPJ SPJ_BLUE JOIN P P_BLUE ON SPJ_BLUE.PNO = P_BLUE.PNO WHERE SPJ_BLUE.JNO = J.JNO AND P_BLUE.PNAME = '螺丝刀' AND P_BLUE.COLOR = '蓝' );

EXISTS和IN的区别在语义上很微妙:IN是拿外层工程编号去子查询结果集里找匹配,EXISTS则是针对外层每一行,去判断子查询是否非空。对于这道题而言,因为子查询里都有J.JNO的相关条件,EXISTS写法是一种“相关子查询”,每条J记录执行时都会把自身编号带进子查询。

很多经验丰富的开发者在处理“是否存在”类型的业务时,会更倾向EXISTS。尤其在子查询的关联字段存在索引的情况下,EXISTS往往能提前终止扫描,只要找到一条满足条件的记录就会返回真,不必把整个子查询结果集全部算完。比如判断某个工程有没有使用红色螺母时,数据库一旦在SPJ表上通过JNO索引找到第一条匹配P1的记录,就可以立刻得出结论,不需要继续扫描这个工程的其他供应记录。

但要注意,EXISTS这种相关子查询的方案,最大的风险是外层表数据量很大时,理论上可能造成逐行循环。不过在工程J表通常不会太大、而SPJ表会很大的场景下,配合索引使用,EXISTS往往表现不错。具体性能差异,放到执行计划部分一起对比。

3.4 三种写法到底怎么选

先给一张对照表总结三种写法的特点,后面在性能部分会再结合实际执行计划展开。

写法核心语义去重处理可读性性能场景适用情况
JOIN连接把两个条件当成两条“路径”,在关联中完成过滤容易产生重复,需要DISTINCT最直观,但逻辑容易被JOIN顺序带偏数据量中等,索引合理结果集字段需要来自多表时推荐
IN子查询两个集合求交集天然去重,无需DISTINCT最好理解,翻译成自然语言几乎无差别子查询结果可控时稳定条件叠加较多时扩展方便
EXISTS相关子查询逐行判断是否存在满足条件的记录天然去重稍难理解,但表达“存在”语义最准确外层表小、内层表大且索引好时最优生产环境里判断存在性常用

我个人的习惯是:写业务代码时优先用EXISTS,因为它的“存在即真”语义跟业务描述完全一致;写一次性分析SQL时优先用IN子查询,因为逻辑清晰不容易出错;只有在需要同时返回零件名称、颜色等冗余字段时,才会考虑用JOIN。没有绝对的最优写法,只有最适合当前场景的写法。

4. 关系代数视角:这道题背后的“除法”影子

4.1 为什么教材总爱出这种题

从关系代数来看,这道题并不单纯是等值连接,它其实已经踩到了“除运算”的门槛。所谓除运算,通常表达的是“找出满足所有要求”的记录。比如“查询使用了全部零件的工程编号”,这才是标准的关系除法。而“同时使用红色螺母和蓝色螺丝刀”,要求的数量是确定的两种,本质上可以看作除法的一个特例,也可以看作“按指定零件集合做包含关系判断”的简化版。

这也是为什么教学数据库喜欢拿它当经典题。它不会像纯除运算那么抽象,但又能让学习者接触到“把目标拆成多个集合,再通过交集或嵌套来完成判断”的思维方式。理解了这道题,再去啃“查询使用了全部零件的工程”“查询至少使用了P1和P2两种零件的工程”之类题目,思路会顺很多。

4.2 用关系代数表达式拆解执行路径

用关系代数来描述一下题目的运算步骤,对理解SQL的执行也非常有帮助。设使用红色螺母的工程集合为A,使用蓝色螺丝刀的工程集合为B,则有:

A = π_JNO(σ_PNAME='螺母' AND COLOR='红'(P) ⋈ SPJ)

B = π_JNO(σ_PNAME='螺丝刀' AND COLOR='蓝'(P) ⋈ SPJ)

最终结果 = π_JNO,JNAME(J ⋈ (A ∩ B))

这里面最关键的运算就是π_JNO,也就是投影。无论中间做了多少次连接,最后留在集合里的永远只有工程编号这一列。正因为每一步都给JNO做了投影去重,集合A和集合B里的元素才不会重复,交集运算的结果才准确。如果没有投影就直接求交集,可能会因为重复元组的存在,让结果集的含义变成“匹配次数大于0”,一旦写SQL时忽略DISTINCT,就会出现重复工程号,这也是为什么前面反复强调去重时机。

在当年没有可视化数据库的年代,这本书的练习题就是用这样的关系代数表达式一步步推演的。现在我们有各种SQL工具,但回归关系代数去理解,仍然能帮我们避开很多“看SQL结果好像对,其实是靠DISTINCT兜底”的坑。

5. 实际跑通题目:结果验证与常见错误排查

5.1 验证结果集是否正确

按我上一节造的数据执行查询,正确的结果应该只有J1和J4。J1的供应记录里有红色螺母P1和蓝色螺丝刀P2,J4也有这两类零件,两者都符合题目条件。J2虽然有螺母和螺丝刀,但颜色和名称的搭配是“蓝色螺母”加“红色螺丝刀”,不符合条件。J3只有红色螺母,缺少蓝色螺丝刀,也不符合条件。

如果你用JOIN写法,在加上DISTINCT之前,查询结果里很可能出现重复的J1和J4。造成这种重复的原因很常见:J1工程同时有多条记录,比如S1供应了P1,S2也供应了P1,多条记录都指向同一个JNO,JOIN之后结果集自然就会膨胀。这时候DISTINCT就不是一个可选项,而是必选项。很多初学者看到JOIN版本结果正确,就以为DISTINCT只是锦上添花,其实一旦数据量变大,漏掉DISTINCT会导致严重的重复统计。

用IN或EXISTS版本执行时,因为子查询的判断天然去重,结果就不会有重复问题。这也是我在给自己写的SQL做自检时的一个习惯:同一道题至少用两种写法跑一遍,对比结果是否一致。如果出现差异,一定是某种写法在条件或去重上出了问题。

5.2 最容易犯的几个SQL错误

针对这种“同时使用A和B”的题目,我总结了几个从初学者到有一定经验的人都很容易踩的坑:

第一个坑,条件放错位置。比如把红色螺母的条件放在JOIN的ON里,把蓝色螺丝刀的过滤条件放在WHERE里,结果也可能正确,但错误风险更高。尤其在使用LEFT JOIN时,放错位置会让结果产生完全不同的语义,排查起来非常麻烦。

第二个坑,忘记DISTINCT。JOIN写法中,同一个工程因为多条供应记录而重复,这是最隐蔽的错误。光是看前几行结果,很容易以为没问题,一旦统计工程数量,就会比实际多。建议任何通过JOIN筛选“存在性”的查询,都先问自己一句:这个JOIN会不会让结果集行数膨胀?如果会,就必须去重。

第三个坑,把“同时使用”写成OR。用OR表示的是“只要用了其中任意一个就符合”,这就把题目的条件从交集扩大成了并集。用这个写法查出来的结果会包含只用了红色螺母、没用到蓝色螺丝刀的工程,业务含义完全变了。

第四个坑,把条件写成同一条记录里的AND。像前面说的,一条SPJ记录只对应一种零件,在同一行里要求既是红色螺母又是蓝色螺丝刀,是绝对矛盾的。如果表的视图里能同时看到零件颜色和名称两列,很容易被误导,反而忘记了行与行之间的关系。

第五个坑,子查询里漏掉关联条件。EXISTS写法里面,如果忘了写SPJ_RED.JNO = J.JNO,子查询就变成了一个与外部无关的独立查询,只要表里存在任意一个红色螺母记录,就会返回所有工程,结果必然错误。这类问题往往在数据量小时不容易发现,因为有些数据库优化器会做特殊处理,但在生产环境很容易爆炸。我的自检方法是,把子查询单独拿出来执行,看返回的行数和内容是否符合预期,再结合外层表去判断。

5.3 用GROUP BY和HAVING也能做吗

聊到这里,你可能已经想到,这类问题也可以用GROUP BY和HAVING来写。思路是:先找到所有用到了红色螺母或蓝色螺丝刀的工程,按工程编号分组,然后统计每个组内符合条件的零件种类数,最后用HAVING要求种类数等于2。

SELECT SPJ.JNO FROM SPJ JOIN P ON SPJ.PNO = P.PNO WHERE (P.PNAME = '螺母' AND P.COLOR = '红') OR (P.PNAME = '螺丝刀' AND P.COLOR = '蓝') GROUP BY SPJ.JNO HAVING COUNT(DISTINCT P.PNO) = 2;

这种写法在思路上很巧妙,把“同时使用两种零件”转换成了“满足条件的零件种类数等于2”。它用COUNT(DISTINCT P.PNO)来统计,避免了同一零件有多条记录导致的数量误判。不过这种写法也有一个前提,就是题目中的两种零件,它们的PNO必须不同。在本题中,红色螺母和蓝色螺丝刀显然是两种不同的零件,所以PNO不同,用COUNT(DISTINCT P.PNO)没问题。如果两种零件规格相同但颜色不同,PNO可能一样,那统计方式就要换成COUNT(DISTINCT P.COLOR)或者COUNT(DISTINCT CONCAT(COLOR, PNAME)),具体问题要具体分析。

GROUP BY方案的优点在于,一旦题目条件多到三种、四种零件,代码行数也不会线性增长,只需要在WHERE里继续加OR条件,再把HAVING里的数字改掉就行。缺点是可读性比IN写法稍差,而且如果WHERE条件写错或者零件种类数统计口径不对,排查难度也比较大。我一般把这种写法作为第三种验证方案,跟其他写法交叉核对结果。

6. 性能优化与生产环境落地经验

6.1 索引设计:别让查询输在起跑线上

在窄表上跑这种关联查询,索引设计直接决定执行计划的好坏。SPJ表作为关系表,业务上高频的查询条件无非是供应商编号、零件编号、工程编号,以及QTY。这里有一个非常经典的索引建议,就是把外键字段分别建上索引。

CREATE INDEX idx_spj_jno ON SPJ(JNO); CREATE INDEX idx_spj_pno ON SPJ(PNO); CREATE INDEX idx_spj_sno ON SPJ(SNO);

联合主键(SNO, PNO, JNO)本身可以覆盖一些走前缀的查询,比如以SNO开头的组合查询。但如果查询经常以JNO或PNO作为过滤条件,就必须额外建索引。拿这道题举例,EXISTS写法里内层子查询是SPJ_RED.JNO = J.JNO AND SPJ_RED.PNO = P_RED.PNO,如果只靠联合主键而JNO不是左前缀,就没法高效定位。所以实际落地时,我会针对这种查询把索引建为(JNO, PNO),这样一次索引查找就能同时过滤工程和零件条件。

P表方面,零件名称和颜色字段如果是经常共同出现的过滤条件,可以建一个复合索引(PNAME, COLOR)。虽然这张表通常很小,全表扫描代价也不高,但在生产环境里零件可能多达几十万行,这时一个合适的复合索引就能把嵌套循环连接的成本大幅压下去。

6.2 大数据量下的执行计划对比

为了了解不同写法的真实性能,我在一张模拟数据量较大的环境里跑过三种写法的执行计划。具体数据量是SPJ表约100万行,P表约1万行,J表约5000行。结果大致如下:

  • JOIN写法:优化器选择从P表过滤出两个零件,然后分别走SPJ表的索引嵌套循环连接。由于两个零件条件过滤后的中间结果集不大,整体代价可控。但如果没有在SPJ.JNO上建索引,最后还是避免不了对J表或中间结果的排序或哈希操作。
  • IN写法:优化器在多数数据库里会把IN子查询改写成半连接(SEMI JOIN),执行路径和JOIN写法很像,性能也不错。但某些优化器不强的数据库里,子查询可能被物化成临时表,如果子查询结果集很大,物化开销就上去了。
  • EXISTS写法:相关子查询在SQL Server、Oracle这类数据库里往往会被优化为半连接,性能不比JOIN差。在MySQL里,具体版本不同,优化效果差异也很大。我实测过MySQL 8.0版本,EXISTS写法的执行计划常常会变成NESTED LOOP SEMI JOIN,配合(JNO, PNO)复合索引,单次查询耗时非常稳定。

有一个普遍适用的经验是:不要凭直觉猜测性能,要看执行计划。同一道题,在不同数据库、不同数据分布下,最优写法可能完全不同。生产环境里,我的标准动作是:先用EXPLAIN看执行计划,找到瓶颈节点,再对比调整。只要索引到位,三种写法的性能差距通常不会超过一个数量级,真正拉开差距的是有没有重点优化“关联字段的索引”。

6.3 CTE拆解与临时表方案

写复杂查询时,我更喜欢用CTE(公用表表达式)把问题拆得清晰一点。以这道题为例,先分别算两个集合,再求交集,逻辑一目了然。

WITH red_nut_jnos AS ( SELECT DISTINCT SPJ.JNO FROM SPJ JOIN P ON SPJ.PNO = P.PNO WHERE P.PNAME = '螺母' AND P.COLOR = '红' ), blue_screwdriver_jnos AS ( SELECT DISTINCT SPJ.JNO FROM SPJ JOIN P ON SPJ.PNO = P.PNO WHERE P.PNAME = '螺丝刀' AND P.COLOR = '蓝' ) SELECT J.JNO, J.JNAME FROM J WHERE J.JNO IN (SELECT JNO FROM red_nut_jnos) AND J.JNO IN (SELECT JNO FROM blue_screwdriver_jnos);

CTE的好处是逻辑分层,每个集合单独验证,排错时可以直接把CTE段落单独执行。在MySQL 8.0、PostgreSQL、SQL Server、Oracle里,CTE都是标准功能。如果你的数据库版本太老,也可以用临时表替代。实际工作中,当我需要为一个临时报表写一条很长的SQL时,CTE几乎是我唯一的选择,它能极大降低“一眼望不到头”的复杂度。

6.4 生产日志里的慢查询排查思路

如果这种查询在生产环境变慢了,最优先排查的应该是索引和统计信息,而不是改写SQL。我遇到过很多次类似场面:开发同学说“SQL执行了十几秒”,一查执行计划,发现某个关键的关联字段没有索引,或者索引失效了。加上索引之后,同样的SQL瞬间降到几十毫秒。

统计信息过期也是慢查询的常见元凶。数据库优化器依赖统计信息估算行数和连接顺序,如果统计信息不准,可能选错执行计划。遇到这种情况,刷新统计信息往往比改SQL更有效。再者,如果查询里用到的P表或J表本身有大量历史数据,但业务上只需要“当前有效”的数据,那就要在关联之前先做一次过滤,尽量缩小参与JOIN的数据集。这也是为什么我在前面的SQL里,总是尽量提前用WHERE条件把P表缩小到红色螺母或蓝色螺丝刀两类记录,而不是先做大的连接再过滤。

7. 实操心得与扩展思考

7.1 用“分组求交”的思路应对题目变体

这类“同时使用多种零件”的题,最常见的变体就是增加条件数量。比如变成“查询同时使用P1、P2、P3三种零件的工程”,或者变成“查询使用了全部零件的工程”。后一种就是标准的关系除法,解决的思路也比单一题要更系统。

对于确定数量的零件组合,用IN子查询叠加是最稳的,每增加一个条件就多加一个IN。但如果零件数量不确定,或者需要判断“是否覆盖某个零件集合”,分组计数法更合适。把目标零件集合先限定出来,按JNO分组,用COUNT(DISTINCT P.PNO)统计满足条件的零件种类数,再和目标种类数比较。这个思路在面试里经常被追问,实际业务里做套餐组合校验时也能用上。

7.2 自检SQL的四个习惯

最后分享几个我在工作中一直在用的自检习惯。第一个习惯,任何涉及存在性的查询,都先想清楚“结果集行数”会不会膨胀。第二个习惯,至少用两种写法执行同一需求,对比结果集是否完全一致。第三个习惯,子查询单独执行验证,先看子查询的返回内容,再组合外层逻辑。第四个习惯,看执行计划,尤其是关联字段的索引有没有被正确使用。这四点基本能覆盖绝大多数SQL逻辑问题和性能隐患。

回到10-94这道题本身,它的价值不在于题目有多难,而在于它把“集合思维”真正落到了SQL语法层面。能用JOIN写出结果,说明你掌握了表连接;能用IN或EXISTS写出结果,说明你理解了集合语义;能解释清楚三种写法的差异和性能取舍,说明你对数据库执行机制有了自己的判断。这道题我从第一次见到现在,每次回头看都会有新的体会,也推荐你把文中的几种写法实际跑一遍,感受一下不同写法的执行计划差异,这比单纯背答案要有用得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 5:13:49

STM32智能车实战:从PID调参到硬件级C语言控制

1. 这不是“学完就能跑”的速成课,而是一条踩过二十多届智能车队员脚印的实战路径如果你刚在实验室门口看到一辆小车自己拐弯、加速、过坡,心里冒出“我也想做一台”的念头——恭喜,你已经站在了智能车世界的入口。但别急着抄起开发板就焊电路…

作者头像 李华
网站建设 2026/9/20 8:25:15

ensp校园网络规划落地指南:从需求分析到配置排障全流程

简介:基于eNSP的岭南职业技术学院校园网络规划论文,是一份可直接参考的毕业设计文稿,面向网络工程和计算机科学类学生,尤其适合正在筹备校园网络方向毕设或课程设计的读者。论文以岭南职院网络改造为背景,采用接入层、…

作者头像 李华
网站建设 2026/9/20 6:09:27

MATLAB转C/C++实战:mcc与Matcom选型、配置与部署全解析

简介:面向具备Matlab与C语言基础、希望脱离Matlab环境部署算法的开发人员,这份docx文档系统讲解了将Matlab程序转换为C语言的两类主流实现路径:一是基于Matlab Compiler与mcc命令生成独立可执行文件,二是借助MATcom v4.5将m文件转…

作者头像 李华
网站建设 2026/9/20 11:53:47

agent-plugins在macOS首启被拦截?Gatekeeper隔离标志完整解决方案

agent-plugins在macOS首启被拦截?Gatekeeper隔离标志完整解决方案 【免费下载链接】agent-plugins 项目地址: https://gitcode.com/GitHub_Trending/skills16/agent-plugins agent-plugins 是 Flutter 团队维护的 AI Agent 插件合集,为 Claude C…

作者头像 李华