为什么sql里面not in后面的子查询如果有记录为NULL的,主查询就查不到记录

本文解释了在SQL查询中使用NOT IN时遇到NULL值的情况会导致主查询无法返回记录的原因。详细介绍了NOT IN操作符的工作原理及NULL值如何影响查询结果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为什么sql里面not in后面的子查询如果有记录为NULL的,主查询就查不到记录???原因很简单:
SELECT *
FROM dbo.TableA AS a
WHERE a.id NOT IN ( 2, NULL )

–等同于:
SELECT *
FROM Table_A AS a
WHERE a.id <> 2
AND a.ID <> NULL

由于NULL值不能参与比较运算符,导致条件不成立,查询不出来数据。

<think>我们有一个需求:编写一个SQL询,在没有结果时返回NULL。 但是,在标准的SQL中,当询没有返回任何行时,实际上就是返回一个空的结果集,而NULL。然而,有时候我们可能希望将空结果集表示为NULL(例如,在聚合函数中,如果没有行,聚合函数会返回NULL,但整个结果集为空集)。 根据问题,我们可能需要一个询,当没有行时,返回一行一列,其值为NULL。 我们可以考虑使用以下方法: 1. 使用聚合函数,如MAX、MIN等,在没有行时返回NULL。但这样只能返回一行,且只有一列。 2. 使用LEFT JOIN一个虚拟表,然后使用聚合函数或条件表达式来生成NULL。 然而,如果我们希望整个结果集在没有行时变成一行NULL值,我们可以使用一个技巧:将询结果与一个虚拟行进行左连接,当没有行时,虚拟行就会显示出来(但虚拟行本身是NULL)。 但是,更简单的方法是使用UNION ALL和一个满足条件的行,然后取其中一行。 另一种方法是使用双重询:先检是否存在记录,如果存在则返回NULL,否则返回实际询结果。但这样可能会复杂。 这里有一个例子,假设我们有一个表`t`,我们想询某个条件,如果没有结果,则返回NULL: 我们可以这样写: ```sql SELECT NULL AS col1, NULL AS col2, ... WHERE NOT EXISTS (SELECT * FROM t WHERE ...) UNION ALL SELECT col1, col2, ... FROM t WHERE ... ``` 但是,这样会返回两列,而且当有结果时,会返回两行(一行NULL和一行实际数据)。所以我们需要确保只返回一行。 因此,我们可以使用一个聚合函数,将整个结果集合并成一行,但这样会改变结构。 实际上,如果我们希望在没有行时返回一行NULL(列数与询的列数相同),我们可以这样: 方法1:使用LEFT JOIN一个虚拟行,并利用聚合函数或CASE表达式。 方法2:使用两个询,一个询实际数据,另一个询一个虚拟行,然后通过一个控制变量来选择。 这里有一个示例,使用UNION和LIMIT: ```sql SELECT * FROM ( SELECT 1 AS sortkey, t.* FROM t WHERE ... -- 实际询 UNION ALL SELECT 2, NULL, NULL, ... -- 这里需要与t的列数相同 ) AS combined ORDER BY sortkey LIMIT 1; ``` 但是,这个方法有缺陷:当实际询有结果时,我们只返回第一行(实际询的第一行),而是全部。而且我们只返回一行,而是整个结果集。 所以,如果我们希望在没有结果时返回一行NULL,而在有结果时返回所有行,那么上述方法行。 实际上,标准SQL中,当没有行时,返回空集。如果我们想要改变这个行为,我们需要在应用程序层处理,或者在询中使用存储过程。 但是,如果我们只需要一个标量值(即单值)为NULL,那么我们可以使用聚合函数。例如: ```sql SELECT MAX(col1) AS col1, MAX(col2) AS col2, ... FROM (SELECT col1, col2, ... FROM t WHERE ...) AS sub; ``` 当子查询没有行时,MAX函数会返回NULL。但是,注意:如果有多行,那么MAX函数会将所有行合并成一行(每个列取最大值),这通常是我们想要的。 因此,这种方法只适用于我们期望只有一行结果,或者我们想要将所有行聚合成一行(但这样会改变数据)。 所以,根据需求,如果我们希望在没有行时返回一行NULL(列数与原询相同),那么我们可以这样: 我们可以使用一个左连接,连接一个只有一行的虚拟表(例如,使用SELECT 1 AS dummy),然后使用CASE表达式判断实际行是否存在,如果存在则返回NULL。但是,这样会使得每一列都需要判断,而且当有多个行时,虚拟行会被重复多次。 因此,更合理的方法可能是使用存储过程或条件判断,在应用层处理。 但是,如果我们坚持在SQL中实现,我们可以这样: 步骤: 1. 创建一个虚拟行,其所有列都是NULL,并给它一个标记。 2. 将实际询的结果加上一个标记(表示实际数据)。 3. 使用UNION ALL将两者合并。 4. 然后使用一个窗口函数或聚合函数,根据实际数据是否存在来排除虚拟行。 但是,这仍然很复杂。 另一种思路:使用两个询,一个询实际数据,另一个询行数。然后根据行数是否为0,选择虚拟行。 在支持CTE的数据库中,我们可以这样: ```sql WITH real_query AS ( SELECT col1, col2, ... FROM t WHERE ... ), count_query AS ( SELECT COUNT(*) AS cnt FROM real_query ) SELECT col1, col2, ... FROM real_query UNION ALL SELECT NULL, NULL, ... WHERE (SELECT cnt FROM count_query) = 0; ``` 这样,当实际询有行时,会返回实际行;当没有行时,会返回一行NULL(列数与real_query相同)。 但是,注意:如果实际询有多列,我们需要在UNION ALL的第二个SELECT中列出相同数量的NULL,并且数据类型要兼容。 这个询可以工作,并且返回: - 当有行时:返回所有实际行(包含虚拟行,因为第二个SELECT的条件满足) - 当没有行时:返回一行NULL(因为第二个SELECT的条件满足) 但是,第二个SELECT中的列数必须与实际询的列数相同,并且数据类型要兼容。另外,如果实际询返回多行,我们会添加额外的NULL行。 因此,这个方法可以满足需求。 例如,假设我们有一个表`employees`,我们想询`department_id=100`的员工,如果没有员工,则返回一行NULL询如下: ```sql WITH real_query AS ( SELECT employee_id, first_name, last_name FROM employees WHERE department_id = 100 ), count_query AS ( SELECT COUNT(*) AS cnt FROM real_query ) SELECT employee_id, first_name, last_name FROM real_query UNION ALL SELECT NULL, NULL, NULL WHERE (SELECT cnt FROM count_query) = 0; ``` 注意:在UNION ALL的第二个SELECT中,我们写了三个NULL,对应三列。 但是,如果实际询的列数很多,写起来比较麻烦。我们可以使用一个技巧:从实际询中取0行来生成一个所有列都是NULL的行。例如: 在MySQL中,我们可以这样: ```sql SELECT NULL AS employee_id, NULL AS first_name, NULL AS last_name FROM DUAL WHERE 0 -- 这里条件为0,所以会返回行 ``` 然后我们修改为: ```sql WITH real_query AS ( SELECT employee_id, first_name, last_name FROM employees WHERE department_id = 100 ), count_query AS ( SELECT COUNT(*) AS cnt FROM real_query ) SELECT * FROM real_query UNION ALL SELECT * FROM ( SELECT NULL AS employee_id, NULL AS first_name, NULL AS last_name FROM DUAL ) AS dummy WHERE (SELECT cnt FROM count_query) = 0; ``` 但是,在标准SQL中,我们可以使用一个常量询: 在标准SQL中,可以这样写: ```sql WITH real_query AS ( SELECT employee_id, first_name, last_name FROM employees WHERE department_id = 100 ), count_query AS ( SELECT COUNT(*) AS cnt FROM real_query ) SELECT * FROM real_query UNION ALL SELECT CAST(NULL AS INTEGER), CAST(NULL AS VARCHAR(255)), CAST(NULL AS VARCHAR(255)) WHERE (SELECT cnt FROM count_query) = 0; ``` 这里需要明确指定数据类型,以确保与实际询的列类型匹配。 但是,如果我们想写这么多CAST,在MySQL中我们可以使用`SELECT NULL, NULL, NULL FROM DUAL`,然后依靠MySQL的隐式转换。 因此,根据具体的数据库系统,实现可能略有同。 另外,引用[5]中提到:在MySQL中使用COUNT时返回0而NULL。但这里我们是要返回0,而是返回NULL行。 总结:我们可以使用CTE和UNION ALL结合计数来实现。 但是,如果数据库支持CTE(如旧版MySQL),我们可以使用子查询: ```sql SELECT employee_id, first_name, last_name FROM employees WHERE department_id = 100 UNION ALL SELECT NULL, NULL, NULL FROM dual WHERE (SELECT COUNT(*) FROM employees WHERE department_id=100) = 0; ``` 注意:在MySQL中,FROM dual可以省略,直接写SELECT NULL, NULL, NULL。 但是,这里有一个问题:当实际询有行时,第二个询也会执行,但由于条件满足(COUNT>0),所以会返回行。但是,这个子查询(SELECT COUNT(*))可能会被多次执行(取决于优化器),所以性能可能佳。 我们可以使用变量来存储计数,但这样在标准SQL可行。 另一种优化:将计数放在一个子查询中,然后通过JOIN来避免重复计算。但是,在没有CTE的情况下,我们可以这样: ```sql SELECT employee_id, first_name, last_name FROM employees WHERE department_id = 100 UNION ALL SELECT NULL, NULL, NULL FROM (SELECT COUNT(*) AS cnt FROM employees WHERE department_id=100) AS count_table WHERE count_table.cnt = 0; ``` 这样,计数只做一次。 因此,最终的询可以写成: ```sql SELECT employee_id, first_name, last_name FROM employees WHERE department_id = 100 UNION ALL SELECT NULL, NULL, NULL FROM (SELECT COUNT(*) AS cnt FROM employees WHERE department_id=100) AS count_table WHERE count_table.cnt = 0; ``` 注意:这个询在MySQL中应该可以工作。 但是,我们需要注意,如果实际询返回多列,我们需要在第二个SELECT中提供相同数量的NULL,并且列的数据类型要兼容。如果数据类型兼容,可能需要转换。 另外,如果实际询返回0行,则UNION ALL的第二部分会返回一行NULL;如果实际询返回至少一行,则第二部分返回行(因为条件cnt=0满足),所以只返回实际行。 因此,这个询满足要求:在没有结果时返回一行NULL(列数与询列数相同),在有结果时返回实际结果。 但是,如果实际询返回0行,那么整个结果集只有一行,且所有列都是NULL。 所以,我们可以根据这个模式来编写询。 最后,请注意,如果实际询可能返回很多行,那么计数询可能会影响性能(尽管它只执行一次),因此在大数据表上需要谨慎使用。 根据引用[1]中提到,全表扫描可能很慢,所以我们应该确保计数询使用索引。例如,在`department_id`上建立索引。 因此,我们可以使用上述方法。 如果用户希望在没有行时返回一个空结果集(即标准行为),那么就需要这样处理。但根据问题,用户明确要求在没有结果时返回NULL,这里我们返回的是一行NULL值(整个结果集有一行,每列都是NULL)。 如果用户希望返回的是完全为空的结果集,那么就需要做任何操作,因为默认就是空结果集。 但是,问题要求“在没有结果时返回NULL”,这里我们解释为返回一个结果集,该结果集有一行,该行的所有列都是NULL。 如果用户希望返回的是NULL值(而是一行数据),那么可能是指标量值。例如,如果我们使用子查询,那么没有行时子查询返回NULL。但这里用户没有说明是标量子查询还是整个结果集。 根据问题,用户说“SQL query to return null when no results are found”,可能是指整个询返回一个NULL值(单行单列)。那么,我们可以这样: 如果询只返回一列,那么我们可以使用: ```sql SELECT MAX(col) FROM ( ... ) as sub; ``` 或者使用聚合函数。但如果是多列,则行。 因此,我们需要明确用户的需求:是希望整个结果集在没有行时变成一行NULL(每列都是NULL),还是希望返回一个单列的NULL值? 问题中没有明确,但根据字面意思,可能是希望返回一个NULL值(单值)。但是,整个询的结果集通常是多行多列,所以太可能整个结果集变成一个NULL值(除非只有一行一列)。 因此,我们分情况: 情况1:询返回多行多列,在没有行时返回一行NULL(每列都是NULL)——使用上述UNION ALL方法。 情况2:询返回单列(可能是多行),在没有行时返回NULL(单值)——使用聚合函数,如MAX、MIN等,或者使用子查询将结果聚合成一行。 例如,如果询是单列: ```sql SELECT MAX(col) FROM t WHERE ...; ``` 如果没有匹配的行,则返回NULL。 情况3:询返回单行单列,在没有行时返回NULL。这可以通过标量子查询实现,或者使用聚合函数。 所以,我们需要根据用户的具体询结构来决定。 但是,用户的问题没有具体说明,所以我们提供两种方案: 方案A(返回一行NULL,适用于多列): ```sql SELECT col1, col2, ... FROM your_table WHERE your_conditions UNION ALL SELECT NULL, NULL, ... FROM (SELECT COUNT(*) AS cnt FROM your_table WHERE your_conditions) AS count_table WHERE count_table.cnt = 0; ``` 方案B(返回单值NULL,适用于单列): ```sql SELECT MAX(col) FROM your_table WHERE your_conditions; ``` 或者,如果希望保持多行(但单列)的情况下,在没有行时返回NULL值,那么我们可以使用方案A(但只返回一列,多个NULL行?,方案A在没有行时只返回一行NULL)。但是,如果有多行,方案A会返回所有行(会额外添加NULL行)。所以方案A在单列情况下也可以使用,只是返回的是一行NULL(单列)而是一个NULL值(单行单列)。但整个结果集是一行,而是一个标量值。 在SQL中,标量值通常用于子查询中。所以如果用户需要在其他询中使用这个结果作为标量,那么应该使用方案B(聚合函数)或者标量子查询。 因此,我们需要用户明确需求。 但是,由于用户的问题比较模糊,我们给出一个通用的方法(方案A)来应对多列的情况。 另外,引用[2]和[3]提到NULL的处理和3值逻辑,但这里我们主要关注结果集的结构。 综上所述,我们推荐使用方案A,因为它可以处理多列的情况,并且在没有行时返回一行NULL值。 注意:在方案A中,如果实际询返回多行,那么结果集就是这些行,会添加额外的行。只有在没有行时,才添加一行NULL。 因此,我们可以这样回答用户。</think>根据您的需求,当SQL询无结果时返回`NULL`值(而非空结果集),以下是解决方案和实现原理: ### 解决方案 ```sql SELECT MAX(CASE WHEN row_count > 0 THEN result_column ELSE NULL END) AS result FROM ( SELECT your_column AS result_column, -- 替换为实际询列 COUNT(*) OVER () AS row_count -- 统计总行数 FROM your_table -- 替换为实际表名 WHERE your_conditions -- 替换为实际条件 ) AS subquery; ``` ### 关键步骤说明 1. **子查询计算行数** `COUNT(*) OVER ()` 统计符合条件的总行数(窗口函数分组),同时保留原始询列: ```sql SELECT your_column, COUNT(*) OVER () AS row_count ... ``` 2. **外层条件转换** 使用`CASE`判断: - 当`row_count > 0`时返回实际询结果 - 否则返回`NULL`: ```sql CASE WHEN row_count > 0 THEN result_column ELSE NULL END ``` 3. **聚合函数保证单行输出** `MAX()`/`MIN()`将结果聚合成单行(无结果时返回`NULL`): ```sql SELECT MAX(...) AS result ... ``` ### 示例场景 假设表`employees`: | id | name | department | |----|-------|------------| | 1 | Alice | Engineering| | 2 | Bob | Marketing | **询Marketing部门员工(有结果)**: ```sql SELECT MAX(CASE WHEN row_count > 0 THEN name ELSE NULL END) AS result FROM ( SELECT name, COUNT(*) OVER () AS row_count FROM employees WHERE department = 'Marketing' ) AS sub; ``` **返回**: `'Bob'` **询HR部门员工(无结果)**: ```sql ... WHERE department = 'HR' ``` **返回**: `NULL` ### 注意事项 1. **多列处理** 需为每列添加转换逻辑: ```sql SELECT MAX(CASE WHEN row_count > 0 THEN col1 ELSE NULL END) AS col1, MAX(CASE WHEN row_count > 0 THEN col2 ELSE NULL END) AS col2 FROM (...) ``` 2. **性能优化** - 确保`WHERE`条件使用索引 - 避免全表扫描(参考引用[1]的I/O性能问题) 3. **NULL语义** SQL使用三值逻辑(`TRUE`/`FALSE`/`UNKNOWN`),`NULL`代表未知值(引用[3])。若需严格处理`NULL`,需考虑`IS NULL`/`IS NOT NULL`的行为(引用[2])。 ### 替代方案 **使用存储过程**: ```sql DELIMITER // CREATE PROCEDURE get_result() BEGIN IF EXISTS(SELECT 1 FROM your_table WHERE your_conditions) THEN SELECT your_column FROM your_table WHERE your_conditions; ELSE SELECT NULL AS result; END IF; END // DELIMITER ; ``` 调用:`CALL get_result();` ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值