前言
PostgreSQL 不但高度兼容 SQL 标准,同时还对很多语法进行了扩展,可以用于实现一些特殊的功能。今天我们就来介绍一下 PostgreSQL 数据库中 DISTINCT 关键字的 4 种不同用法。
DISTINCT
按照 SQL 标准,SELECT DISTINCT
可以在返回查询结果之前去除重复的记录,每个重复的数据组中只保留一条记录。例如:
SELECT DISTINCT dept_id, sex FROM employee; dept_id|sex| -------|---| 4|男 | 1|男 | 4|女 | 5|男 | 3|女 | 2|男 |
以上语句中的 DISTINCT 表示返回不同部门 id 和性别的组合值。我们也可以使用 GROUP BY 实现相同的结果:
SELECT dept_id, sex FROM employee GROUP BY dept_id, sex;
按照 SQL 标准,多个 NULL 值对于 DISTINCT 而言属于相同的分组。
DISTINCT ON
考虑一个问题:每个部门中月薪最高的员工都是谁?这个问题可以使用多种实现方法:
-- 子查询 SELECT dept_id, emp_name,salary FROM employee WHERE (dept_id, salary) IN ( SELECT dept_id, MAX(salary) FROM employee GROUP BY dept_id ); dept_id|emp_name|salary | -------|--------|--------| 1|刘备 |30000.00| 2|诸葛亮 |24000.00| 3|孙尚香 |12000.00| 4|赵云 |15000.00| 5|法正 |10000.00| -- 窗口函数 WITH ranked_employee AS ( SELECT dept_id, emp_name, salary, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) rn FROM employee ) SELECT * FROM ranked_employee WHERE rn = 1; dept_id|emp_name|salary |rn| -------|--------|--------|--| 1|刘备 |30000.00| 1| 2|诸葛亮 |24000.00| 1| 3|孙尚香 |12000.00| 1| 4|赵云 |15000.00| 1| 5|法正 |10000.00| 1|
其中,第一个语句使用了子查询;第二个语句使用了窗口函数,除了 ROW_NUMBER 之外,也可以使用 RANK 或者 DENSE_RANK 等函数。这两者都是 SQL 标准实现。
除此之外,PostgreSQL 提供了扩展的 DISTINCT ON 子句,可以更加方便地实现以上结果:
SELECT DISTINCT ON (dept_id) dept_id, emp_name, salary FROM employee ORDER BY dept_id, salary DESC; dept_id|emp_name|salary |rn| -------|--------|--------|--| 1|刘备 |30000.00| 1| 2|诸葛亮 |24000.00| 1| 3|孙尚香 |12000.00| 1| 4|赵云 |15000.00| 1| 5|法正 |10000.00| 1|
其中,DISTINCT ON (dept_id) 表示部门 id 相同的数据组,返回其中的第一条记录;ORDER BY 子句确保了返回的是每个部分中月薪最高的记录。DISTINCT ON 中的字段或表达式(可能多个)必须和 ORDER BY 最左侧的几个字段或表达式相同。
IS DISTINCT FROM
空值(NULL)是数据库中的一个特殊值,通常用于表示缺失值或者不适用的值。空值的比较是一个比较容易出错的问题。例如:
WITH t AS ( SELECT 1 AS a, 1 AS b UNION ALL SELECT 1, 2 UNION ALL SELECT NULL, 1 UNION ALL SELECT NULL, NULL ) SELECT a, b, a = b "a=b" FROM t; a |b |a=b | ----|----|-----| 1 |1 |true | 1 |2 |false| NULL|1 |NULL | NULL|NULL|NULL |
当我们使用比较运算符(=、<>、<、> 等)与 NULL 进行比较时,结果既不是真也不是假,而是未知;因为 NULL 表示未知,也就意味着可能是任何值;我们不能说两个未知的值相同,也不能说它们不相同。
为了比较 NULL 值,SQL 定义了两个专用的运算符:IS NULL和IS NOT NULL。例如:
SELECT 1 IS NULL "1 IS NULL", 1 IS NOT NULL "1 IS NOT NULL", NULL IS NULL "NULL IS NULL", NULL IS NOT NULL "NULL IS NOT NULL"; 1 IS NULL|1 IS NOT NULL|NULL IS NULL|NULL IS NOT NULL| ---------|-------------|------------|----------------| false |true |true |false |
因此,对于两个可能为空的字段进行比较的完整方法如下:
WITH t AS ( SELECT 1 AS a, 1 AS b UNION ALL SELECT 1, 2 UNION ALL SELECT NULL, 1 UNION ALL SELECT NULL, NULL ) SELECT a, b, (a IS NULL AND b IS NULL) OR (a IS NOT NULL AND b IS NOT NULL AND a = b) "a=b" FROM t; a |b |a=b | ----|----|-----| 1 |1 |true | 1 |2 |false| NULL|1 |false| NULL|NULL|true |
以上语句返回了我们期望的结果,但是读写都很不方便;为此,PostgreSQL 提供了扩展的 IS [NOT] DISTINCT FROM 运算符,支持 NULL 值的比较。例如:
WITH t AS ( SELECT 1 AS a, 1 AS b UNION ALL SELECT 1, 2 UNION ALL SELECT NULL, 1 UNION ALL SELECT NULL, NULL ) SELECT a, b, a IS NOT DISTINCT FROM b "a=b" FROM t; a |b |a=b | ----|----|-----| 1 |1 |true | 1 |2 |false| NULL|1 |false| NULL|NULL|true |
注意,IS NOT DISTINCT FROM 表示判断两个数据是否相同,IS DISTINCT FROM 表示判断两个数据是否不同;它们都将 NULL 看作已知的一个特殊值,而不是 SQL 标准中的未知值。显然这种语法更加言简意赅。
另外,PostgreSQL 还提供了一个配置变量 transform_null_equals,该参数默认为 off;如果设置为 on,PostgreSQL 会自动执行 convert x = NULL 到 x IS NULL 的转换。建议不要依赖这个参数的设置,而是应该修改应用程序
聚合函数与 DISTINCT
聚合函数(aggregate function)针对一组数据行进行运算,并且返回一条结果。PostgreSQL 支持的聚合函数包括 avg、COUNT、MAX/MIN、SUM、STRING_AGG、ARRAY_AGG 等。例如:
SELECT dept_id, count(*), avg(salary), string_agg(emp_name, ',' ORDER BY salary DESC) FROM employee GROUP BY dept_id ORDER BY dept_id; dept_id|count|avg |string_agg | -------|-----|----------------------|--------------------------------------| 1| 3| 26666.666666666667|刘备,关羽,张飞 | 2| 3|13166.6666666666666667|诸葛亮,黄忠,魏延 | 3| 2| 9000.0000000000000000|孙尚香,孙丫鬟 | 4| 9| 7577.7777777777777778|赵云,周仓,关兴,关平,赵氏,廖化,张苞,赵统,马岱| 5| 8| 5012.5000000000000000|法正,简雍,孙乾,糜竺,黄权,庞统,邓芝,蒋琬 |
以上语句返回了每个部门的员工人数、平均月薪以及所有员工姓名的连接字符串(按照月薪从高到低)。
PostgreSQL 不仅实现了分组聚合操作,还支持聚合函数中的 DISTINCT 选项,可以在进行汇总之前去除每个分组中的重复记录。例如:
SELECT dept_id, string_agg(sex, ','), string_agg(DISTINCT sex, ',') string_agg_distinct FROM employee GROUP BY dept_id ORDER BY dept_id; dept_id|string_agg |string_agg_distinct| -------|-----------------------|-------------------| 1|男,男,男 |男 | 2|男,男,男 |男 | 3|女,女 |女 | 4|男,女,男,男,男,男,男,男,男|女,男 | 5|男,男,男,男,男,男,男,男 |男 |