在SQLite数据库的实际使用中,多列组合去重并提取关联数据是非常常见的需求,比如统计不同用户在不同分类下的唯一操作记录,或者获取每个商品不同批次的唯一库存信息等。这类需求不能仅依靠单列去重实现,需要结合多列判断和关联查询逻辑。

多列组合去重的基础方法
使用DISTINCT实现多列组合去重
DISTINCT关键字可以直接对多个列的组合进行去重,返回所有列组合唯一的记录。这种方式适合只需要获取去重后的多列值,不需要额外聚合计算的场景。
假设我们有如下用户操作记录表user_action:
| id | user_id | action_type | action_time |
|---|---|---|---|
| 1 | 1001 | click | 2024-01-01 10:00:00 |
| 2 | 1001 | click | 2024-01-01 10:01:00 |
| 3 | 1001 | buy | 2024-01-01 10:02:00 |
| 4 | 1002 | click | 2024-01-01 10:03:00 |
如果需要获取所有唯一的user_id和action_type组合,可以使用以下查询:
-- 查询user_id和action_type的唯一组合 SELECT DISTINCT user_id, action_type FROM user_action;
执行上述语句后,会返回user_id为1001、action_type为click,user_id为1001、action_type为buy,user_id为1002、action_type为click这三条唯一组合记录,重复的user_id 1001和action_type click的第二行记录会被过滤掉。
使用GROUP BY实现多列组合去重
GROUP BY同样可以实现多列组合去重,并且支持对去重后的分组进行聚合计算,适合需要同时获取去重组合和统计信息的场景。
还是以上面的user_action表为例,如果需要获取每个user_id和action_type组合的最早操作时间,可以使用GROUP BY:
-- 查询每个用户操作类型组合的最早操作时间 SELECT user_id, action_type, MIN(action_time) AS first_action_time FROM user_action GROUP BY user_id, action_type;
这里GROUP BY user_id, action_type会先对两列的组合进行分组去重,然后MIN(action_time)会计算每个分组中的最小操作时间,也就是最早的操作时间。
多列去重后提取关联数据
很多时候我们完成多列去重后,还需要提取去重记录对应的其他关联字段,比如上面例子中如果还需要获取每个唯一组合对应的最早操作记录的完整id,就不能直接用DISTINCT或者简单的GROUP BY,需要结合子查询或者关联查询实现。
使用子查询提取关联数据
可以先通过GROUP BY获取每个多列组合的唯一标识,再关联原表获取完整的关联数据。比如要获取每个user_id和action_type组合对应的最早操作记录的完整信息:
-- 先获取每个组合的最早操作时间,再关联原表获取完整记录
SELECT ua.*
FROM user_action ua
INNER JOIN (
SELECT user_id, action_type, MIN(action_time) AS first_time
FROM user_action
GROUP BY user_id, action_type
) AS temp
ON ua.user_id = temp.user_id
AND ua.action_type = temp.action_type
AND ua.action_time = temp.first_time;
上述语句中,子查询先通过GROUP BY得到每个user_id和action_type组合的最早操作时间first_time,然后原表user_action和子查询结果通过user_id、action_type、action_time三个条件关联,就可以得到每个唯一组合对应的最早操作完整记录。
使用窗口函数提取关联数据
SQLite 3.25.0及以上版本支持窗口函数,使用ROW_NUMBER()可以更简洁地实现多列去重并提取关联数据的需求。还是上面的例子,获取每个user_id和action_type组合的最早操作完整记录:
-- 使用窗口函数给每个组合的记录排序,取排序第一的记录
SELECT id, user_id, action_type, action_time
FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY user_id, action_type
ORDER BY action_time ASC
) AS rn
FROM user_action
)
WHERE rn = 1;
这里PARTITION BY user_id, action_type就是按照两列组合进行分区,也就是去重的维度,ORDER BY action_time ASC会让每个分区内的记录按操作时间升序排列,ROW_NUMBER()会给每个分区的记录从1开始编号,最后取rn=1的记录,就是每个组合的最早操作记录。
两种方案的选择建议
- 如果只需要获取去重后的多列值,不需要其他关联字段,优先使用DISTINCT,语法更简洁。
- 如果需要去重后做聚合计算,比如计数、求和等,优先使用GROUP BY。
- 如果需要去重后提取完整的关联记录,且SQLite版本支持窗口函数,优先使用窗口函数方案,逻辑更清晰,扩展性更好。
- 如果SQLite版本较低不支持窗口函数,再使用子查询关联的方案。
注意事项
在使用多列组合去重时,需要注意列值为NULL的情况,SQLite中NULL和NULL在DISTINCT和GROUP BY的判断中会被视为相等,也就是多个NULL值的组合也会被去重。如果业务上需要区分NULL值的不同情况,需要提前处理NULL值,比如用COALESCE函数将NULL转换为特定的占位值。
另外,如果去重的多列组合中存在大字段,比如很长的文本字段,DISTINCT和GROUP BY的比较效率会比较低,建议提前对数据进行预处理,或者给多列组合添加联合索引提升查询效率。