SQL技巧:合并LEFT JOIN冗余行,清晰展示数据
2025-03-14 16:53:44
合并 LEFT JOIN 产生的冗余行:一个 SQL 技巧
遇到多表 LEFT JOIN 后出现同一用户多条部分 null 的记录? 就像这样:
user_id | country | purpose_id
2 CA NULL
2 NULL 367
2 NG NULL
2 NULL 368
目标是将这些零散的信息整合成更清晰的形式:
user_id | country | purpose_id
2 CA 367
2 NG 368
但是,常规的 JOIN 操作似乎行不通。 这篇博客聊聊怎么解决这个问题。
问题根源:LEFT JOIN 的特性
先捋一下问题的原因。 问题的核心在于 LEFT JOIN 和数据结构。LEFT JOIN 会保留左表的所有行,即使右表中没有匹配项(用 NULL 填充)。 上面 SQL 查询的多次 LEFT JOIN,针对 'country' 和 'purpose' 这两个分类分别进行关联。
假设一条数据,既有国家信息,又有用途信息。但是,数据库将两者分开存储, 这两次 LEFT JOIN 会产生两条记录:一条 country 有值,purpose_id 为 NULL;另一条 country 为 NULL,purpose_id 有值。 当用户属于多个国家或有多个用途时,情况会更复杂,产生更多行。
解决方案:巧妙利用聚合函数
要实现数据的合并,关键在于利用 SQL 的聚合功能,把同一个 user_id 的多行记录合并成一行。
方案一:MAX() 或 MIN() 函数
由于我们知道 country 和 purpose_id 不会同时存在, 可以直接对它们用聚合操作.
SELECT
cs.user_id,
MAX(UPPER(country_term.slug)) AS country,
MAX(purpose_term.term_id) AS purpose_id
FROM
wp_user_services cs
INNER JOIN wp_posts v ON
v.ID = cs.visa_id AND v.post_status = 'publish'
INNER JOIN wp_term_relationships tr ON
tr.object_id = v.ID
INNER JOIN wp_term_taxonomy tt ON
tr.term_taxonomy_id = tt.term_taxonomy_id AND tt.taxonomy IN('country', 'purpose')
LEFT JOIN wp_terms country_term ON
tt.term_id = country_term.term_id AND tt.taxonomy = 'country'
LEFT JOIN wp_terms purpose_term ON
purpose_term.term_id = tt.term_id AND tt.taxonomy = 'purpose'
WHERE
cs.user_id = 2
GROUP BY
cs.user_id;
-
原理:
对于每个user_id
,MAX(country_term.slug)
会在所有行中找到非 NULL 的 country 值(因为 NULL 在比较中通常被认为是最小值)。同理,MAX(purpose_term.term_id)
会找到非 NULL 的 purpose_id。 -
代码解释:
跟原始 SQL 比, 最主要的修改是在 SELECT 列表和添加了GROUP BY
子句。GROUP BY cs.user_id
将所有 user_id 相同的行分组。 -
使用
MIN
也能达到一样的效果
方案二:COALESCE() 函数 + 子查询 (更具通用性)
如果数据逻辑发生变化, country 和 purpose_id 同时有值. 或者您希望对关联过程进行控制, 方案一的方式就不够灵活.
SELECT
cs.user_id,
COALESCE(country_sub.country, purpose_sub.country) AS country,
COALESCE(purpose_sub.purpose_id, country_sub.purpose_id) AS purpose_id
FROM
wp_user_services cs
INNER JOIN wp_posts v ON
v.ID = cs.visa_id AND v.post_status = 'publish'
LEFT JOIN (
SELECT
tr.object_id,
UPPER(t.slug) AS country,
NULL AS purpose_id -- 添加一个purpose_id字段占位
FROM
wp_term_relationships tr
INNER JOIN wp_term_taxonomy tt ON
tr.term_taxonomy_id = tt.term_taxonomy_id AND tt.taxonomy = 'country'
INNER JOIN wp_terms t ON
tt.term_id = t.term_id
) AS country_sub ON
country_sub.object_id = v.ID
LEFT JOIN (
SELECT
tr.object_id,
NULL AS country,-- 添加一个country字段占位
t.term_id AS purpose_id
FROM
wp_term_relationships tr
INNER JOIN wp_term_taxonomy tt ON
tr.term_taxonomy_id = tt.term_taxonomy_id AND tt.taxonomy = 'purpose'
INNER JOIN wp_terms t ON
tt.term_id = t.term_id
) AS purpose_sub ON
purpose_sub.object_id = v.ID
WHERE
cs.user_id = 2;
-
原理:
- 分离查询: 将 'country' 和 'purpose' 的关联逻辑分别放在两个子查询(
country_sub
和purpose_sub
)中。 每个子查询只负责获取对应分类的数据. 注意, 这里为不涉及的字段添加了NULL
占位符, 目的是为了统一两个子查询返回的字段. - 主查询合并: 在主查询中,使用
COALESCE()
函数.COALESCE()
函数会返回第一个非 NULL 的参数。
- 分离查询: 将 'country' 和 'purpose' 的关联逻辑分别放在两个子查询(
-
代码解释:
两个子查询分别处理
country
和purpose
, 子查询结构清晰, 可以更好的分别进行管理和修改。
在主查询使用COALESCE(), 如果您需要保留两者的数据,COALESCE提供了一种更简单的方式。 -
进阶使用技巧:
如果, 不再仅仅满足于优先级的选择,而是需要保留country和purpose的对应关系, 需要略作调整.
核心在于修改group by. 让其既能够区分不同的country,又能够区分不同的purpose.
SELECT
cs.user_id,
country_sub.country,
purpose_sub.purpose_id
FROM
wp_user_services cs
INNER JOIN wp_posts v ON
v.ID = cs.visa_id AND v.post_status = 'publish'
LEFT JOIN (
SELECT
tr.object_id,
UPPER(t.slug) AS country,
NULL AS purpose_id -- 添加一个purpose_id字段占位
FROM
wp_term_relationships tr
INNER JOIN wp_term_taxonomy tt ON
tr.term_taxonomy_id = tt.term_taxonomy_id AND tt.taxonomy = 'country'
INNER JOIN wp_terms t ON
tt.term_id = t.term_id
) AS country_sub ON
country_sub.object_id = v.ID
LEFT JOIN (
SELECT
tr.object_id,
NULL AS country,-- 添加一个country字段占位
t.term_id AS purpose_id
FROM
wp_term_relationships tr
INNER JOIN wp_term_taxonomy tt ON
tr.term_taxonomy_id = tt.term_taxonomy_id AND tt.taxonomy = 'purpose'
INNER JOIN wp_terms t ON
tt.term_id = t.term_id
) AS purpose_sub ON
purpose_sub.object_id = v.ID
WHERE
cs.user_id = 2
group by cs.user_id, country_sub.country, purpose_sub.purpose_id;
输出
user_id | country | purpose_id
2 CA NULL
2 NULL 367
2 NG NULL
2 NULL 368
由于country_sub.country, purpose_sub.purpose_id不可能同时有值, 所以该聚合查询输出和未聚合的输出一模一样.
假设有表如下
user_id | visa_id | ...
2 | 1 | ...
ID | post_status | ...
1 | publish | ...
object_id |term_taxonomy_id|
1 1
1 2
term_taxonomy_id | term_id | taxonomy |
1 1 country
2 2 purpose
term_id | slug |
1 CA
2 367
根据新表进行联表查询有:
SELECT
cs.user_id,
UPPER(country_term.slug) as country,
purpose_term.term_id as purpose_id
FROM
wp_user_services cs
INNER JOIN wp_posts v ON
v.ID = cs.visa_id AND v.post_status = 'publish'
INNER JOIN wp_term_relationships tr ON
tr.object_id = v.ID
INNER JOIN wp_term_taxonomy tt ON
tr.term_taxonomy_id = tt.term_taxonomy_id AND tt.taxonomy IN('country', 'purpose')
LEFT JOIN wp_terms country_term ON
tt.term_id = country_term.term_id AND tt.taxonomy = 'country'
LEFT JOIN wp_terms purpose_term ON
purpose_term.term_id = tt.term_id AND tt.taxonomy = 'purpose'
WHERE
cs.user_id = 2;
有:
user_id | country | purpose_id
2 CA 367
在此条件下,聚合查询
SELECT
cs.user_id,
country_sub.country,
purpose_sub.purpose_id
FROM
wp_user_services cs
INNER JOIN wp_posts v ON
v.ID = cs.visa_id AND v.post_status = 'publish'
LEFT JOIN (
SELECT
tr.object_id,
UPPER(t.slug) AS country,
NULL AS purpose_id -- 添加一个purpose_id字段占位
FROM
wp_term_relationships tr
INNER JOIN wp_term_taxonomy tt ON
tr.term_taxonomy_id = tt.term_taxonomy_id AND tt.taxonomy = 'country'
INNER JOIN wp_terms t ON
tt.term_id = t.term_id
) AS country_sub ON
country_sub.object_id = v.ID
LEFT JOIN (
SELECT
tr.object_id,
NULL AS country,-- 添加一个country字段占位
t.term_id AS purpose_id
FROM
wp_term_relationships tr
INNER JOIN wp_term_taxonomy tt ON
tr.term_taxonomy_id = tt.term_taxonomy_id AND tt.taxonomy = 'purpose'
INNER JOIN wp_terms t ON
tt.term_id = t.term_id
) AS purpose_sub ON
purpose_sub.object_id = v.ID
WHERE
cs.user_id = 2
group by cs.user_id, country_sub.country, purpose_sub.purpose_id;
仍然会合并为
user_id | country | purpose_id
2 CA 367
因此这种情况下和直接max
的效果相同。
方案三:调整数据模型 (治本之策)
上面两种方案, 更像是一种临时补救措施。 更好的方式是从源头上优化。 如果应用场景允许, 可以考虑调整数据模型, 直接避免冗余。 例如:
- 合并表: 如果 'country' 和 'purpose' 信息总是与同一个用户服务(
wp_user_services
)相关联,可以考虑将它们直接存储在wp_user_services
表中,或者创建一个新的关联表,包含user_id
、country_id
和purpose_id
。 - JSON/数组字段(适用性评估): 如果数据库支持 JSON 或数组类型(如 PostgreSQL),可以将多个 country 或 purpose 存储在一个字段中。但这可能增加查询复杂性。
如果进行数据模型调整, 请提前备份,并在非高峰时段操作。 还要全面测试,确保修改后的模型符合业务需求, 同时没有引入新问题。
这几种方法都能有效解决 LEFT JOIN 带来的数据冗余问题. 根据具体的情况和需求,灵活运用这些 SQL 技巧,可以让数据更清晰、查询更高效。