You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何Query 3无输出?硬编码ID正常的SQL问题排查

问题描述

现有三个SQL查询:

  • Query 1通过窗口函数可确认Jade从未取得任何考试的最高分或最低分;
  • Query 2能查询出所有取得过高分或低分的student_id,其中Jade的ID不在列表内;
  • 但Query 3使用NOT IN子查询筛选从未得过高/低分的学生时无输出,手动硬编码Query 2的ID却能得到包含Jade的正确结果。

请求解释该现象原因,并提供更优实现方案。


原始查询语句

Query 1

WITH cte AS (
    SELECT 
        *,
        MAX(score) OVER (PARTITION BY exam_id) AS max_score,
        MIN(score) OVER (PARTITION BY exam_id) AS min_score
    FROM
        student JOIN exam
        USING (student_id)
    ORDER BY
            exam_id, student_id
) SELECT
        *
    FROM cte;

通过该查询可确认:Jade从未拿到过任何考试的最高分或最低分。

Query 2

WITH cte AS (
    SELECT 
        *,
        MAX(score) OVER (PARTITION BY exam_id) AS max_score,
        MIN(score) OVER (PARTITION BY exam_id) AS min_score
    FROM
        student JOIN exam
        USING (student_id)
    ORDER BY
            exam_id, student_id
) SELECT
        DISTINCT student_id
    FROM cte
    WHERE
        score = max_score OR score = min_score;

通过该查询可确认:Jade的student_id不在高分/低分获得者列表中。

Query 3

WITH cte AS (
    SELECT 
        *,
        MAX(score) OVER (PARTITION BY exam_id) AS max_score,
        MIN(score) OVER (PARTITION BY exam_id) AS min_score
    FROM
        student JOIN exam
        USING (student_id)
    ORDER BY
            exam_id, student_id
) SELECT
    DISTINCT student_id, student_name
    FROM cte
    WHERE
        student_id NOT IN ( SELECT DISTINCT student_id
                            FROM cte
                            WHERE score = max_score OR score = min_score );

疑问:为什么Query 3没有输出?但手动把Query 2得到的ID硬编码进去,就能得到包含Jade的正确结果?


原始表结构与数据

CREATE TABLE IF NOT EXISTS Student (student_id INT, student_name VARCHAR(30));
CREATE TABLE IF NOT EXISTS Exam (exam_id INT, student_id INT, score INT);

INSERT INTO Student (student_id, student_name) VALUES (1, 'Daniel');
INSERT INTO Student (student_id, student_name) VALUES (2, 'Jade');
INSERT INTO Student (student_id, student_name) VALUES (3, 'Stella');
INSERT INTO Student (student_id, student_name) VALUES (4, 'Jonathan');
INSERT INTO Student (student_id, student_name) VALUES (5, 'Will');

INSERT INTO Exam (exam_id, student_id, score) VALUES (10, 1, 70);
INSERT INTO Exam (exam_id, student_id, score) VALUES (10, 2, 80);
INSERT INTO Exam (exam_id, student_id, score) VALUES (10, 3, 90);
INSERT INTO Exam (exam_id, student_id, score) VALUES (20, 1, 80);
INSERT INTO Exam (exam_id, student_id, score) VALUES (30, 1, 70);
INSERT INTO Exam (exam_id, student_id, score) VALUES (30, 3, 80);
INSERT INTO Exam (exam_id, student_id, score) VALUES (30, 4, 90);
INSERT INTO Exam (exam_id, student_id, score) VALUES (40, 1, 60);
INSERT INTO Exam (exam_id, student_id, score) VALUES (40, 2, 70);
INSERT INTO Exam (exam_id, student_id, score) VALUES (40, 4, 80);

原因分析

1. 无效的CTE ORDER BY干扰执行计划

Query 3的CTE中添加了ORDER BY exam_id, student_id,但该子句在无LIMIT配合的情况下是无效的——大多数SQL数据库会忽略CTE定义中的ORDER BY(除非用于分页)。这种无效的排序指令可能干扰数据库优化器的执行计划,导致子查询与主查询对CTE数据的处理出现异常,最终无法匹配到Jade的记录。

2. NOT IN的NULL陷阱(潜在风险)

虽然本次案例中子查询结果无NULL,但NOT IN的逻辑特性需要注意:如果子查询结果包含NULL值,value NOT IN (...)会返回UNKNOWN(因为SQL中value != NULL始终为UNKNOWN),导致所有行被过滤。这是NOT IN的常见坑点,也是推荐用NOT EXISTS替代的原因。


更优实现方案

方案一:用窗口函数直接标记学生是否得过最值

该方案通过窗口函数一次性统计每个学生是否有过最值记录,逻辑清晰且效率更高,还能包含未参加考试的学生:

WITH student_exam_stats AS (
    SELECT 
        s.student_id,
        s.student_name,
        -- 统计该学生是否有过任何一次考试的最值
        MAX(CASE WHEN score = MAX(score) OVER (PARTITION BY exam_id) 
                  OR score = MIN(score) OVER (PARTITION BY exam_id) 
             THEN 1 ELSE 0 END) OVER (PARTITION BY s.student_id) AS has_extreme_score
    FROM student s
    LEFT JOIN exam e ON s.student_id = e.student_id
)
SELECT student_id, student_name
FROM student_exam_stats
WHERE has_extreme_score = 0 OR has_extreme_score IS NULL;
-- has_extreme_score IS NULL对应未参加任何考试的学生

方案二:用NOT EXISTS替代NOT IN

避免NOT IN的NULL陷阱,同时逻辑更直观:

WITH cte AS (
    SELECT 
        *,
        MAX(score) OVER (PARTITION BY exam_id) AS max_score,
        MIN(score) OVER (PARTITION BY exam_id) AS min_score
    FROM student 
    JOIN exam USING (student_id)
)
SELECT DISTINCT c.student_id, c.student_name
FROM cte c
WHERE NOT EXISTS (
    SELECT 1
    FROM cte c2
    WHERE c2.student_id = c.student_id
    AND (c2.score = c2.max_score OR c2.score = c2.min_score)
);

方案三:修复原Query 3(去掉无效ORDER BY)

如果坚持使用原逻辑,只需去掉CTE中的无效ORDER BY即可:

WITH cte AS (
    SELECT 
        *,
        MAX(score) OVER (PARTITION BY exam_id) AS max_score,
        MIN(score) OVER (PARTITION BY exam_id) AS min_score
    FROM
        student JOIN exam
        USING (student_id)
) SELECT
    DISTINCT student_id, student_name
    FROM cte
    WHERE
        student_id NOT IN ( SELECT DISTINCT student_id
                            FROM cte
                            WHERE score = max_score OR score = min_score );

内容的提问来源于stack exchange,提问作者Vibhu Mishra

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 13:05:53