在轻量级数据库SQLite里,做数据查询时我们经常会碰到重复数据的问题。比如一张订单表里,同一个用户可能在不同时间下了多笔订单,城市字段有大量重复值。如果我们直接对这些列做求和、计数,得到的结果往往偏大。这时候就需要用到DISTINCT去重,以及SUM、COUNT、AVG这类聚合函数。它们单独用都不难,但放到一条SQL里配合时,很多人会搞混执行逻辑,导致统计数字不对。本文从底层行为讲清楚它们怎么一起工作。

DISTINCT的基础去重机制与单独用法
SQLite里的DISTINCT是一个查询修饰符,放在SELECT关键字后面。它的作用是:对最终选出的所有列的组合进行比对,如果两行在所有被选列上值都一样,就只保留其中一行。注意它是基于“选出列”的整体去重,而不是只看着某一列。比如SELECT DISTINCT name, age FROM user,只有当name和age同时相同时才会被合并。
单独使用DISTINCT时,它不改变列的数量,只是减少行数。底层上SQLite会对结果集做一次排序或哈希,把重复元组剔除。如果表很大,这个操作会消耗临时内存和CPU。但它不需要配合GROUP BY就能完成基础去重,写法比分组统计简单。下面是一段建表和普通去重查询的例子:
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER,
city TEXT,
amount REAL
);
INSERT INTO orders (user_id, city, amount) VALUES
(1, '北京', 100.0),
(1, '北京', 200.0),
(2, '上海', 150.0),
(1, '上海', 300.0);
SELECT DISTINCT user_id, city FROM orders;
上面这段代码执行后,user_id为1且city为北京的两行会变成一行。这就是最直观的DISTINCT行为。它没有做任何计算,只是把肉眼看到的重复组合消掉。当我们要看“有哪些用户到过哪些城市”这类明细时,这样写就足够了。
聚合函数与DISTINCT的组合书写形式
聚合函数用来把多行数据算成一个值。SQLite支持COUNT、SUM、AVG、MIN、MAX等。当聚合函数的参数前加上DISTINCT,意思是“先对该列去重,再聚合”。语法是聚合函数(DISTINCT 列名)。这和在外层SELECT用DISTINCT完全不同:外层去重是整行维度,聚合内去重是单列维度。
以COUNT为例,COUNT(city)会统计所有非NULL的city行数,而COUNT(DISTINCT city)只统计出现过几个不同的city。对于订单表,前者算的是订单笔数,后者算的是涉及城市数。下面代码演示两者差异:
-- 统计订单总笔数(含重复城市) SELECT COUNT(city) FROM orders; -- 统计涉及的不同城市数量 SELECT COUNT(DISTINCT city) FROM orders; -- 统计每个用户去过几个不同城市 SELECT user_id, COUNT(DISTINCT city) AS city_cnt FROM orders GROUP BY user_id;
从执行计划看,SQLite在处理COUNT(DISTINCT col)时,会对col建一个临时去重结构,再计数。它不会先全表聚合再剔重。因此当你需要“唯一值的数量”或“唯一值的和”时,必须把DISTINCT写在聚合函数括号里。写在外面是达不到目的的,比如SELECT DISTINCT COUNT(city)只是把单行计数结果去重,毫无意义。
执行顺序误区与性能优化建议
不少初学者以为SQLite会先跑聚合再去做重,其实对于聚合(DISTINCT col)这种写法,去重发生在聚合计算之前,作用范围仅限那个列。而如果是SELECT DISTINCT 聚合(col),则先算出聚合值,再对结果行去重,一般配合GROUP BY才有讨论价值。混淆这两者,是统计错误的高发原因。
在性能上,DISTINCT聚合会用到临时B树或内存表。如果列上有索引,SQLite有时能借用索引有序性加速去重;如果没有,就要自己排序。对于大表,推荐先缩小数据范围,比如加上WHERE条件过滤时间区间,再去做COUNT(DISTINCT)。另外,MIN和MAX本身对重复不敏感,加DISTINCT没有效果反而浪费资源,应避免画蛇添足。
-- 不推荐:对MAX使用DISTINCT,无意义 SELECT MAX(DISTINCT amount) FROM orders; -- 推荐:直接取最大金额 SELECT MAX(amount) FROM orders; -- 推荐:先过滤再去重统计,提升效率 SELECT COUNT(DISTINCT city) FROM orders WHERE amount > 100;
还有一个实际坑点:TEXT类型的字段去重受大小写和编码影响。SQLite默认按字节比较,如果同一城市写成“北京”和“BEIJING”不会被认定重复。若业务要求无视大小写,需要用LOWER(city)包一层再DISTINCT,即COUNT(DISTINCT LOWER(city))。这样逻辑才严谨,也不会漏统计。
常见业务场景与完整示例
假设我们要分析用户消费广度:每人去了几个城市、在不同城市的消费总额唯一值之和。这时DISTINCT和聚合就要混用。下面示例用CTE先把每人每城市汇总,再统计城市数,并演示错误与正确写法对比。
WITH user_city AS (
SELECT user_id, city, SUM(amount) AS total
FROM orders
GROUP BY user_id, city
)
SELECT
user_id,
COUNT(DISTINCT city) AS city_count,
SUM(total) AS all_spend
FROM user_city
GROUP BY user_id;
这段逻辑中,内层GROUP BY已经把同用户同城市合并,外层COUNT(DISTINCT city)其实等价于COUNT(city),因为city在组内已唯一。但如果是直接在原表上写SUM(DISTINCT amount),就会把相同金额的不同订单合并,导致总金额少算,这是典型误用。正确做法是先按维度聚合,再求和,不要对金额用DISTINCT。
总结来说,SQLite的DISTINCT和聚合函数配合核心只有一条:DISTINCT写在聚合括号内,就对那一列去重后算;写在外层,就对整行结果去重。弄清作用域与顺序,多用EXPLAIN QUERY PLAN观察临时结构,就能写出既准又快的统计SQL。