SQL 查询修复:解决好友提醒筛选与屏蔽内容的冲突
2025-04-10 13:57:22
修复 SQL 查询:在筛选好友提醒时排除已屏蔽内容
咱们在用 SQL 从数据库捞数据的时候,经常会遇到需要组合多个条件、关联好几张表的情况。有时候,一个看似简单的需求,比如“我要看我自己的提醒,还有我朋友发的提醒,但别给我看我屏蔽掉的那些”,写起来就可能踩坑。
这不,最近就碰上一个哥们儿,他想实现上面这个功能,结果加了个屏蔽条件,SQL 就跑不起来了。咱们就来分析分析,看看问题出在哪儿,顺便聊聊几种解决办法。
问题来了:好友提醒查不全,屏蔽的还捣乱?
先看看这位朋友的基础数据长啥样:
用户表 (USERS)
ID | ACCOUNT |
---|---|
1 | 80001 |
2 | 90001 |
3 | 70001 |
好友关系表 (FRIENDS)
ID | ACCOUNT | TO_ACCOUNT |
---|---|---|
1 | 80001 | 70001 |
(这里假设关系是双向的,或者至少在查询时需要考虑双向)
提醒表 (ALERTS)
ID | ACCOUNT | ALERT | ALERTID |
---|---|---|---|
1 | 80001 | Hi | XD3000 |
2 | 70001 | Hey | YZ4000 |
一开始,他写了个查询,用来找出“自己”(假设是 80001
)或者“朋友”(和 80001
在 FRIENDS
表里有关联的账号,比如 70001
)发的提醒。这查询跑得好好的:
-- 原始查询(能跑通)
SELECT snd.ACCOUNT
FROM ALERTS AS m
JOIN USERS AS snd ON snd.ACCOUNT = m.ACCOUNT
WHERE snd.ACCOUNT IN (
-- 找出朋友关系中的一方是 80001 或 70001 的 ACCOUNT
SELECT ACCOUNT
FROM FRIENDS
WHERE ACCOUNT = '80001' OR TO_ACCOUNT = '70001' -- (这里的逻辑可能需要根据实际需求微调,先按原样分析)
)
UNION -- 合并两部分结果
SELECT snd.ACCOUNT
FROM ALERTS AS m
JOIN USERS AS snd ON snd.ACCOUNT = m.ACCOUNT
WHERE snd.ACCOUNT IN (
-- 找出朋友关系中的另一方是 70001 或 80001 的 TO_ACCOUNT
SELECT TO_ACCOUNT
FROM FRIENDS
WHERE TO_ACCOUNT = '70001' OR ACCOUNT = '80001' -- (同上,逻辑待确认)
);
旁白:这个原始查询逻辑有点绕,而且 UNION
两边的子查询条件看起来有点重复,但先不管这个,它至少能跑。它尝试找出与 '80001' 或 '70001' 相关的朋友账号,然后筛选出这些账号发送的提醒。
接着,麻烦来了。他想加个功能:不看自己屏蔽掉的提醒。他新建了张屏蔽表:
屏蔽记录表 (ALERTS_BLOCKED)
ID | ACCOUNT | ALERTID |
---|---|---|
1 | 80001 | XD3000 |
然后,他信心满满地修改了查询,试图在原来的基础上加上 NOT IN
条件:
-- 修改后的查询(跑不通!)
SELECT snd.ACCOUNT
FROM ALERTS AS m
JOIN USERS AS snd ON snd.ACCOUNT = m.ACCOUNT
WHERE snd.ACCOUNT IN (
SELECT ACCOUNT
FROM FRIENDS
WHERE ACCOUNT = '80001' OR TO_ACCOUNT = '70001'
)
-- 问题在这里!
AND WHERE m.ALERTID NOT IN (
SELECT ALERTID
FROM ALERTS_BLOCKED
WHERE ACCOUNT = '80001' -- 屏蔽自己账户 80001 屏蔽的
)
UNION
SELECT snd.ACCOUNT
FROM ALERTS AS m
JOIN USERS AS snd ON snd.ACCOUNT = m.ACCOUNT
WHERE snd.ACCOUNT IN (
SELECT TO_ACCOUNT
FROM FRIENDS
WHERE TO_ACCOUNT = '70001' OR ACCOUNT = '80001'
)
-- 问题也在这里!
AND WHERE m.ALERTID NOT IN (
SELECT ALERTID
FROM ALERTS_BLOCKED
WHERE ACCOUNT = '70001' -- 屏蔽账户 70001 屏蔽的???
);
结果,这 SQL 直接报错,根本跑不起来。为啥呢?
刨根问底:为啥查询会“罢工”?
这个“加料”后的查询失败,主要是两个原因捣的鬼:一个是语法不过关,另一个是逻辑可能没理顺。
语法错误:重复的 “WHERE”
这是最直接的原因。你看,他在 WHERE
子句后面,又跟了个 AND WHERE
。
WHERE condition1
AND WHERE condition2 -- 这样是错的!
SQL 语法规定,一个 SELECT
语句块里(或者 UNION
的每一个子查询里),WHERE
只能出现一次。如果你有多个过滤条件,应该用 AND
或者 OR
来连接它们,像这样:
WHERE condition1 AND condition2 AND condition3 ...
或者
WHERE condition1 OR condition2
所以,那个 AND WHERE
彻底把 SQL 解析器给整懵了,直接就报错说语法不对。
逻辑不清:屏蔽条件放错地儿?
就算咱们把语法修正了(比如改成 WHERE ... AND m.ALERTID NOT IN (...)
),原来的逻辑也可能有点问题。
-
屏蔽主体混乱 :
在UNION
的第一部分,他加的屏蔽条件是ALERTS_BLOCKED
表里ACCOUNT = '80001'
的记录。这看起来合理,假设用户80001
在操作,那么他应该看自己设置的屏蔽列表。
但是在UNION
的第二部分,他加的屏蔽条件却是ALERTS_BLOCKED
表里ACCOUNT = '70001'
的记录。这就奇怪了,用户80001
查看提醒,为啥要根据用户70001
的屏蔽列表来过滤呢?这通常不符合逻辑。除非需求特殊,否则屏蔽规则应该是基于当前查看者(这里是80001
)来的。 -
过滤时机 :
这个查询结构是先通过UNION
合并两批“可能相关”的提醒发送者账号,然后再(试图)在各自部分内部进行屏蔽过滤。这样做有点割裂,不够清晰。一个更自然的思路或许是:先找出所有“我或我朋友发的”提醒,然后作为一个整体,再排除掉“我屏蔽的”提醒。
效率考量:UNION 和 子查询
虽然不是直接导致错误的原因,但原始查询的结构——使用 UNION
合并两个相似度很高的查询,并且在 WHERE
子句里嵌套 IN
子查询——在大数据量下可能不是最高效的。数据库可能需要执行两次相似的子查询和连接操作。后面咱们看解决方案时,也会顺便考虑一下效率问题。
对症下药:修复查询的几种姿势
既然知道了问题所在,咱们来开方子。下面提供几种解决思路,从简单修复到结构优化,总有一款适合你。假设当前操作的用户是 80001
。
方案一:修正语法,合并条件 (简单直接)
这是最“头痛医头脚痛医脚”的方法,直接在原有结构上修改语法错误,并统一屏蔽逻辑。
原理:
保留 UNION
结构,将 AND WHERE
改成 AND
,并且确保两个部分的屏蔽条件都使用当前用户(80001
)的屏蔽列表。
代码示例:
-- 假设当前用户是 '80001'
SELECT snd.ACCOUNT, m.ALERT, m.ALERTID -- 多选点字段看看效果
FROM ALERTS AS m
JOIN USERS AS snd ON snd.ACCOUNT = m.ACCOUNT
WHERE snd.ACCOUNT IN (
-- 查找好友逻辑 (这里的逻辑最好也优化下,但先保持和原问题一致)
SELECT ACCOUNT FROM FRIENDS WHERE ACCOUNT = '80001' OR TO_ACCOUNT = '70001' -- 保持原始逻辑
)
AND m.ALERTID NOT IN ( -- 使用 AND 连接,而不是 AND WHERE
-- 只根据当前用户 80001 的屏蔽列表过滤
SELECT ALERTID FROM ALERTS_BLOCKED WHERE ACCOUNT = '80001'
)
UNION -- 注意:UNION 默认会去重,如果需要保留所有,用 UNION ALL
SELECT snd.ACCOUNT, m.ALERT, m.ALERTID
FROM ALERTS AS m
JOIN USERS AS snd ON snd.ACCOUNT = m.ACCOUNT
WHERE snd.ACCOUNT IN (
-- 查找好友逻辑 (同上)
SELECT TO_ACCOUNT FROM FRIENDS WHERE TO_ACCOUNT = '70001' OR ACCOUNT = '80001' -- 保持原始逻辑
)
AND m.ALERTID NOT IN ( -- 使用 AND 连接
-- 同样,根据当前用户 80001 的屏蔽列表过滤
SELECT ALERTID FROM ALERTS_BLOCKED WHERE ACCOUNT = '80001'
);
解释:
这个改动最小,直接修正了语法错误,并且统一了屏蔽规则(都按 80001
屏蔽)。它能工作,解决了眼前的报错问题和逻辑偏差。但还是那个问题,UNION
两边的查询逻辑相似度高,可能不够高效。
进阶使用技巧:
- 如果确定
UNION
两边的结果集不可能有重复(或者允许重复),可以考虑使用UNION ALL
,它通常比UNION
快,因为它省去了去重的步骤。但在这个场景下,由于筛选的是发送者ACCOUNT
,同一个发送者可能因为满足不同的朋友关系条件出现在两侧,用UNION
去重可能是需要的。 - 检查
FRIENDS
表的索引情况,ACCOUNT
和TO_ACCOUNT
列最好有索引,能加速子查询。
方案二:先 UNION/组合 后过滤 (逻辑更清晰)
换个思路,咱们先把所有潜在要看的提醒(自己发的 + 朋友发的)找出来,形成一个临时的结果集,然后再对这个结果集应用屏蔽规则。这样逻辑更顺畅。可以用 公共表表达式 (CTE) 来实现。
原理:
- 创建一个 CTE (例如
PotentialAlerts
),在这个 CTE 内部,合并所有来自用户自己和其朋友的提醒记录(包含ALERTID
)。这里需要改进好友查找逻辑,确保准确找到所有相关账号。 - 主查询从
PotentialAlerts
中选取数据。 - 在主查询的
WHERE
子句中,使用NOT IN
或NOT EXISTS
结合ALERTS_BLOCKED
表来排除被当前用户屏蔽的提醒。
代码示例 (使用 CTE):
-- 假设当前用户是 '80001'
WITH RelevantAccounts AS (
-- 找出 '80001' 自己以及所有和他有好友关系的人的账号
-- 改进好友查找逻辑:
SELECT '80001' AS ACCOUNT -- 添加自己
UNION -- 用 UNION 去重账号
SELECT TO_ACCOUNT FROM FRIENDS WHERE ACCOUNT = '80001'
UNION
SELECT ACCOUNT FROM FRIENDS WHERE TO_ACCOUNT = '80001'
),
PotentialAlerts AS (
-- 从 ALERTS 表中找出由这些 RelevantAccounts 发出的提醒
SELECT m.ACCOUNT AS SenderAccount, m.ALERT, m.ALERTID
FROM ALERTS AS m
WHERE m.ACCOUNT IN (SELECT ACCOUNT FROM RelevantAccounts)
)
-- 主查询:从潜在提醒中筛选,并排除被屏蔽的
SELECT pa.SenderAccount, pa.ALERT, pa.ALERTID
FROM PotentialAlerts AS pa
WHERE pa.ALERTID NOT IN (
-- 排除当前用户 '80001' 屏蔽的 ALERTID
SELECT ALERTID FROM ALERTS_BLOCKED WHERE ACCOUNT = '80001'
);
解释:
RelevantAccounts
CTE:先搞清楚哪些人的提醒是咱们关心的(自己+所有好友)。这里的好友查找逻辑比原查询更清晰直接。PotentialAlerts
CTE:基于上面确定的账号列表,去ALERTS
表里捞出他们发的所有提醒,记下ALERTID
很关键。- 最后的主查询:非常简单,就是从
PotentialAlerts
里选,并且用WHERE pa.ALERTID NOT IN (...)
把80001
屏蔽的踢出去。
这种方式结构清晰,每一步干什么一目了然,便于理解和维护。
进阶使用技巧:
NOT IN
在处理子查询结果包含NULL
值时可能有坑,如果ALERTS_BLOCKED.ALERTID
可能为NULL
,建议使用NOT EXISTS
,通常更健壮也可能更快:
-- 使用 NOT EXISTS 替代 NOT IN
SELECT pa.SenderAccount, pa.ALERT, pa.ALERTID
FROM PotentialAlerts AS pa
WHERE NOT EXISTS (
SELECT 1
FROM ALERTS_BLOCKED AS b
WHERE b.ACCOUNT = '80001' AND b.ALERTID = pa.ALERTID
);
- 如果
RelevantAccounts
可能会非常大,WHERE m.ACCOUNT IN (SELECT ...)
的效率也需要关注。有时用JOIN
可能效果更好(见方案三)。
方案三:优化好友查找与 JOIN (性能更优)
这个方案在方案二的基础上,尝试进一步优化性能,特别是连接(JOIN)和过滤部分。它避免了多次子查询,并且用性能通常更好的 LEFT JOIN ... IS NULL
或 NOT EXISTS
来处理屏蔽逻辑。
原理:
- 同样,先用 CTE (或子查询) 得到所有相关账号 (
RelevantAccounts
)。 - 直接将
ALERTS
表与RelevantAccounts
进行JOIN
,筛选出相关账号发的提醒。 - 使用
LEFT JOIN
将上一步结果与ALERTS_BLOCKED
表关联(条件是当前用户的ACCOUNT
和匹配的ALERTID
)。 - 在最终的
WHERE
子句中,只保留那些LEFT JOIN
没有匹配到屏蔽记录的行(即ALERTS_BLOCKED
表对应列为NULL
)。
代码示例 (使用 LEFT JOIN):
-- 假设当前用户是 '80001'
WITH RelevantAccounts AS (
-- 找出 '80001' 自己以及所有和他有好友关系的人的账号 (同方案二)
SELECT '80001' AS ACCOUNT
UNION
SELECT TO_ACCOUNT FROM FRIENDS WHERE ACCOUNT = '80001'
UNION
SELECT ACCOUNT FROM FRIENDS WHERE TO_ACCOUNT = '80001'
)
-- 主查询:直接 JOIN 并使用 LEFT JOIN + IS NULL 过滤
SELECT m.ACCOUNT AS SenderAccount, m.ALERT, m.ALERTID
FROM ALERTS AS m
-- 步骤1: 先 JOIN 相关账号,确保提醒是来自这些人
INNER JOIN RelevantAccounts ra ON m.ACCOUNT = ra.ACCOUNT
-- 步骤2: LEFT JOIN 屏蔽表,尝试找到当前用户对这条提醒的屏蔽记录
LEFT JOIN ALERTS_BLOCKED AS b
ON m.ALERTID = b.ALERTID AND b.ACCOUNT = '80001' -- 关键:匹配提醒ID 且 屏蔽者是当前用户
-- 步骤3: 筛选出没有匹配到屏蔽记录的提醒 (即 b.ID 或 b.ACCOUNT 等非空列为 NULL)
WHERE b.ID IS NULL; -- 或者 b.ACCOUNT IS NULL, 任何 b 表中本应非空的列都行
解释:
INNER JOIN RelevantAccounts
: 确保只处理来自自己或好友的提醒,替代了方案二中的IN
子查询。LEFT JOIN ALERTS_BLOCKED
: 尝试为每一条相关提醒找到匹配的屏蔽记录。如果找到了(表示这条提醒被80001
屏蔽了),b
表的相关列(如b.ID
)就会有值;如果没找到(没被屏蔽),b
表的列就是NULL
。WHERE b.ID IS NULL
: 这就是点睛之笔,它筛掉了所有成功匹配到屏蔽记录的行,只留下那些没被屏蔽的提醒。
这种方式通常被认为性能较好,因为它把过滤逻辑整合到了 JOIN
操作中,数据库优化器可能更容易处理。
安全建议:
- SQL 注入防护 : 代码示例中的
'80001'
是硬编码的。在实际应用中,这个用户 ID 应该是动态传入的。务必使用参数化查询 (Prepared Statements),绝对不要直接拼接字符串来构造 SQL 语句,否则极易受到 SQL 注入攻击。例如,在后端代码中,查询会写成类似WHERE b.ACCOUNT = ?
的形式,然后把用户 ID 作为参数绑定。 - 权限控制 : 确保数据库用户只有执行所需查询的最小权限。
进阶使用技巧:
- 索引优化 :
USERS
表:ACCOUNT
列应有索引(通常是主键)。FRIENDS
表:ACCOUNT
和TO_ACCOUNT
列都应该有索引,最好是组合索引或者各自有独立索引,以加速好友查找。ALERTS
表:ACCOUNT
列(用于 JOINRelevantAccounts
)和ALERTID
列(用于 JOINALERTS_BLOCKED
)应有索引。ALERTS_BLOCKED
表:(ACCOUNT, ALERTID)
的复合索引会非常有用,因为它正好匹配LEFT JOIN
的ON
条件和WHERE
子句(如果用NOT EXISTS
的话)。
- 理解
LEFT JOIN ... IS NULL
vsNOT EXISTS
: 两者效果相似,性能也接近,具体哪个更快可能取决于数据库版本、数据分布和索引情况。可以实际测试一下。NOT EXISTS
有时被认为语义更清晰地表达“不存在满足条件的记录”。
-- 使用 NOT EXISTS 的等价写法 (接在 RelevantAccounts CTE 之后)
SELECT m.ACCOUNT AS SenderAccount, m.ALERT, m.ALERTID
FROM ALERTS AS m
INNER JOIN RelevantAccounts ra ON m.ACCOUNT = ra.ACCOUNT
WHERE NOT EXISTS (
SELECT 1
FROM ALERTS_BLOCKED AS b
WHERE b.ACCOUNT = '80001' AND b.ALERTID = m.ALERTID
);
总结与思考
这次 SQL 查询失败,主要是因为一个基础的语法错误(AND WHERE
)和一个潜在的逻辑问题(屏蔽规则应用混乱)。通过分析,咱们找到了几种修复和优化的方法:
- 简单修复 : 直接修改语法,统一逻辑,改动最小,但不一定最高效。
- CTE + 后过滤 : 利用 CTE 拆分步骤,先找全量再过滤,逻辑清晰,易于维护。
- JOIN 优化 : 改进好友查找,用
JOIN
替代IN
,用LEFT JOIN ... IS NULL
或NOT EXISTS
处理屏蔽,通常性能更优。
选择哪种方案,取决于你对代码可读性、性能的要求以及团队的技术偏好。但无论如何,理解 SQL 的基本语法、掌握不同的查询模式(如 CTE、JOIN 类型、子查询替代方案),以及关注安全(参数化查询)和性能(索引)总是没错的。下次再遇到类似的多条件、多表关联查询,思路或许就能更清晰些了。