第7章 PostgreSQL的特色SQL语法

第7章 PostgreSQL的特色SQL语法

7.1 视图

如果你的应用场景需要反复执行这种关联查询语句,可以考虑创建一个视图。
简单来说,视图就是持久化存储在数据库中的一个查询语句。优点是在表的本体之上增加了一个访问层,简化了权限管理且使得业务逻辑抽象更容易,缺点就是太麻烦。

如果你的视图是基于单表的,并且视图字段中包含了基础表的主键字段,那么就可以直接对此视图执行 UPDATE 操作,视图的基础表数据将随之更新。

物化视图将视图逻辑映射后的数据记录实际存储下来,这样访问物化视图时就省略了视图底层 SQL 的执行过程,就像访问一张本地表一样。一旦物化视图建立好以后,只有对它执行 REFRESH 操作时才会再次从基础表中读取数据。使用物化视图可以节省计算资源。但物化视图也有缺点,刷新不及时,且9.4 版开始才支持用户在物化视图刷新时也能对其进行访问。

7.1.1 单表视图

创建基于单表的视图

CREATE OR REPLACE VIEW census.vw_facts_2011 AS
SELECT fact_type_id, val, yr, tract_id FROM census.facts WHERE yr = 2011;

请注意,针对视图插入的数据可能不会被包含在视图可见范围内;针对视图的更新操作也会发生类似现象,即更新后的数据不再被包含在视图的可见范围内。

UPDATE census.vw_facts_2011 SET yr = 2012 WHERE yr = 2011;

希望更新后的数据仍然应该落在视图可见范围内,可以通过 9.4 版中引入的 WITH CHECK OPTION 子句来实现。创建视图时如果附带了此子句,那么此视图中插入的数据或者更新后的数据落在视图可见范围之外时,系统会报错,违反了该约束的操作会失败。

创建带有 WITH CHECK OPTION 约束的单表视图

CREATE OR REPLACE VIEW census.vw_facts_2011 AS
SELECT fact_type_id, val, yr, tract_id FROM census.facts
WHERE yr = 2011 WITH CHECK OPTION;

尝试执行以下更新操作:

UPDATE census.vw_facts_2011 SET yr = 2012 WHERE val > 2942;

你会看到这样的报错信息:

ERROR: new row violates WITH CHECK OPTION for view"vw_facts_2011"
DETAIL: Failing row contains (1, 25001010500, 2012, 2985.000, 100.00).

7.1.2 使用触发器来更新视图

如果视图的基础表有多张,那么直接更新该视图是不允许的,因为多张表必然带来的问题就是操作要落到哪个基础表上,PostgreSQL 是无法自动判定的。通过编写触发器来对这些操作进行转义处理。

创建 vw_facts 视图

CREATE OR REPLACE VIEW census.vw_facts AS
SELECT
y.fact_type_id, y.category, y.fact_subcats, y.short_name,
x.tract_id, x.yr, x.val, x.perc
FROM census.facts As x INNER JOIN census.lu_fact_types As y
ON x.fact_type_id = y.fact_type_id;

在 vw_facts 视图上创建一个对 INSERT、UPDATE、DELETE 操作进行转义处理的函数

CREATE OR REPLACE FUNCTION census.trig_vw_facts_ins_upd_del() RETURNS trigger AS
$$
BEGIN

IF (TG_OP = 'DELETE') THEN ➊
DELETE FROM census.facts AS f
WHERE
f.tract_id = OLD.tract_id AND f.yr = OLD.yr AND
f.fact_type_id = OLD.fact_type_id;
RETURN OLD;
END IF;

IF (TG_OP = 'INSERT') THEN ➋
INSERT INTO census.facts(tract_id, yr, fact_type_id, val, perc)
SELECT NEW.tract_id, NEW.yr, NEW.fact_type_id, NEW.val, NEW.perc;
RETURN NEW;
END IF;

IF (TG_OP = 'UPDATE') THEN ➌
IF
ROW(OLD.fact_type_id, OLD.tract_id, OLD.yr, OLD.val, OLD.perc) !=
ROW(NEW.fact_type_id, NEW.tract_id, NEW.yr, NEW.val, NEW.perc)
THEN ➍
UPDATE census.facts AS f
SET
tract_id = NEW.tract_id,
yr = NEW.yr,
fact_type_id = NEW.fact_type_id,
val = NEW.val,
perc = NEW.perc
WHERE
f.tract_id = OLD.tract_id AND
f.yr = OLD.yr AND
f.fact_type_id = OLD.fact_type_id;
RETURN NEW;

ELSE
RETURN NULL;
END IF;
END IF;
END;
$$
LANGUAGE plpgsql VOLATILE;

将触发器函数绑定到视图上

CREATE TRIGGER trig_01_vw_facts_ins_upd_del
INSTEAD OF INSERT OR UPDATE OR DELETE ON census.vw_facts
FOR EACH ROW EXECUTE PROCEDURE census.trig_vw_facts_ins_upd_del();

现在针对视图进行更新、删除或插入操作时,这些操作将更新基础 facts 表:

UPDATE census.vw_facts SET yr = 2012
WHERE yr = 2011 AND tract_id = '25027761200';

执行成功后 PostgreSQL 会输出以下信息:

Query returned successfully: 56 rows affected, 40 ms execution time.

如果试图更新的字段不在前面触发器函数中所列的可更新字段列表中,那么更新操作就不会命中任何记录:

UPDATE census.vw_facts SET short_name = 'test';

输出消息将如下所示:

Query returned successfully: 0 rows affected, 931 ms execution time.

可以使用 rules 机制(老版本)来实现视图数据的更新,但我们更推荐使用 INSTEAD OF 触发器机制。rules 机制和触发器机制的区别在于:rules 机制通过重写原始 SQL 达成目标;触发器通过拦截每行操作并改变其实现逻辑来达成目标。可以看到,当一个操作涉及多张表时,rules 系统无论是编写还是理解起来都比触发器困难得多。rules 机制还有一个缺陷,就是只能用 SQL 语言编写,其他过程式语言都不支持。

7.1.3 物化视图

物化视图会把视图可见范围内的数据在本地缓存下来,然后就可以当成一张本地表来使用。首次创建物化视图以及对其执行 REFRESH MATERIALIZED VIEW 刷新操作时,都会触发数据缓存动作,只不过前者是全量缓存,后者是增量刷新。(9.3版本开始有的功能)

物化视图最典型的应用场景是用于加速时效性要求不高的长时复杂查询,在 OLAP(在线分析与处理)领域,这种查询经常出现。物化视图的另一个特点就是支持建立索引以加快查询速度。

建立物化视图

CREATE MATERIALIZED VIEW census.vw_facts_2011_materialized AS
SELECT fact_type_id, val, yr, tract_id FROM census.facts WHERE yr = 2011;

在物化视图上建立索引

CREATE UNIQUE INDEX ix
ON census.vw_facts_2011_materialized (tract_id, fact_type_id, yr);

当物化视图中含大量记录时,为了加快对它的访问速度,我们需要对数据进行排序。要实现这一点,最简单的方法就是在创建物化视图时使用的 SELECT 语句中增加 ORDER BY 子句。另外一种方法就是对其执行聚簇排序操作,以使得记录的物理存储顺序与索引的顺序相同。

基于某索引对物化视图执行聚簇排序操作

CLUSTER census.vw_facts_2011_materialized USING ix; ➊
CLUSTER census.vw_facts_2011_materialized;

➊ 指定聚簇操作所依据的索引名。执行过以后,系统就会自动记下该表是依据哪个索引进行聚簇排序的,后面再次执行聚簇操作时系统会自动使用该索引,所以索引名仅在首次聚簇操作时需要,后续不再需要。
➋ 每次刷新过物化视图后,都需要重新对其进行一次聚簇排序操作。

相对于 CLUSTER 方案来说,ORDER BY 方案的优点在于,每次执行 REFRESH MATERIALIZED VIEW 时都会自动对记录进行重排序,但 CLUSTER 方案就必须手动执行;其缺点在于物化视图加了 ORDRE BY 以后,REFRESH 操作执行会耗时更久。

在 PostgreSQL 9.3 中,刷新物化视图的语法如下:

REFRESH MATERIALIZED VIEW census.vw_facts_2011_materialized;

在 PostgreSQL 9.4 中,为了解决物化视图刷新操作时不可访问的问题,可以使用以下语法:

REFRESH MATERIALIZED VIEW CONCURRENTLY census.vw_facts_2011_materialized;

物化视图有以下几个缺点(最有效的个人感觉触发器物化视图比较有效)

  • 不支持通过 CREATE OR REPLACE 语法来对一个现有的物化视图进行重建,要想重建只能先删除再重建,即使只做很小的改变也需要这么做。删除语法是 DROP MATERIALIZED VIEW + 视图名。删除以后,该视图上所有的索引都会丢失。
  • 每次刷新数据时都要执行一次 REFRESH MATERIALIZED VIEW 操作。PostgreSQL 不支持自动刷新物化视图。要想实现自动刷新,必须使用类似 crontab、pgAgent 定时任务或者是触发器之类的机制。
  • 视图在刷新期间是无法访问的。9.4 版中引入了一个新的 CONCURRENTLY 关键字,在 REFRESH 命令中增加了该关键字以后,可以解除锁定限制,前提条件是被刷新的物化视图上要有一个唯一索引。实现无锁刷新的代价就是,如果有人在刷新期间进行视图访问,那么刷新时间会变长。

7.2 灵活易用的PostgreSQL专有SQL语法

7.2.1 DISTINCT ON

最好用的一个专有语法就是 DISTINCT ON,其功能类似于 DISTINCT,但可以精确到更细的粒度。DISTINCT 会将结果集中完全重复的记录剔除,但 DISTINCT ON 可以将结果集中指定字段值的重复记录剔除,具体实现方法是先对结果集按照 DISTINCT ON 指定的字段进行排序,然后筛选出每个字段值第一次出现时所在的记录,其余的记录都剔除。

DISTINCT ON 的用法

SELECT DISTINCT ON (left(tract_id, 5))
left(tract_id, 5) As county, tract_id, tract_name
FROM census.lu_tracts
ORDER BY county, tract_id;
county 	| tract_id 	  | tract_name
--------+-------------+---------------------------------------------------
25001 	| 25001010100 | Census Tract 101, Barnstable County, Massachusetts
25003 	| 25003900100 | Census Tract 9001, Berkshire County, Massachusetts
25005 	| 25005600100 | Census Tract 6001, Bristol County, Massachusetts
25007 	| 25007200100 | Census Tract 2001, Dukes County, Massachusetts
25009 	| 25009201100 | Census Tract 2011, Essex County, Massachusetts

请注意,ON 修饰符支持设置多列,运算时将基于这多个列的总体唯一性来进行去重操作。同时,查询语句中 ORDER BY 子句的排序字段列表的最左侧必须是 DISTINCT ON 指定的字段列表,即保证整个结果集是按照这几个字段排序的。

7.2.2 LIMIT和OFFSET关键字

LIMIT 关键字指定了查询时仅返回指定数量的记录,OFFSET 关键字指定了从第几条记录开始返回。你可以将二者结合起来使用,也可以单独使用。一般来说,这两个关键字总是和 ORDER BY 联用的,因为只有在一个已经按照用户的意图排好序的结果集上指定返回特定的子结果集才有意义。如果不设置 OFFSET 的话,其值默认为 0 。

查询结果集仅返回从第 3 条开始的 3 条记录

SELECT DISTINCT ON (left(tract_id, 5))
left(tract_id, 5) As county, tract_id, tract_name
FROM census.lu_tracts
ORDER BY county, tract_id LIMIT 3 OFFSET 2;
county | tract_id    | tract_name
-------+-------------+-------------------------------------------------
25005  | 25005600100 | Census Tract 6001, Bristol County, Massachusetts
25007  | 25007200100 | Census Tract 2001, Dukes County
25009  | 25009201100 | Census Tract 2011, Essex County, Massachusetts

7.2.3 简化的类型转换语法

例如 CAST(‘2011-1-1’ AS date) 可以将文本 2011-1-1 转换为一个日期型数据。简写语法,使用两个冒号来表示转换关系,具体格式为:‘2011-1-1’::date。级联执行多个类型转换动作,例如 someXML::text::integer。(全文检索的数据类型不要使用冒号方式,可能会失效)

7.2.4 一次性插入多条记录

一次性插入多条记录

INSERT INTO logs_2011 (user_name, description, log_ts)
VALUES
('robe', 'logged in', '2011-01-10 10:15 AM EST'),
('lhsu', 'logged out', '2011-01-11 10:20 AM EST');

使用 VALUES 语法来模拟一个虚拟表

SELECT *
FROM (
VALUES
('robe', 'logged in', '2011-01-10 10:15 AM EST'::timestamptz),
('lhsu', 'logged out', '2011-01-11 10:20 AM EST'::timestamptz)
) AS l (user_name, description, log_ts);

7.2.5 使用ILIKE实现不区分大小写的查询

PostgreSQL 是一套区分大小写的系统,但它也可以实现不区分大小写的文本搜索,实现方法有两种。一种是将 ANSI LIKE 运算符两边的文本都用 upper 函数转为大写,但这样会导致用不上索引,或者必须单独建立一个基于 upper 函数的函数式索引才能使查询语句用上索引。另一种是使用 PostgreSQL 所特有的 ILIKE 运算符(~~*)。

SELECT tract_name FROM census.lu_tracts WHERE tract_name ILIKE '%duke%';
tract_name
------------------------------------------------
Census Tract 2001, Dukes County, Massachusetts
Census Tract 2002, Dukes County, Massachusetts
Census Tract 2003, Dukes County, Massachusetts
Census Tract 2004, Dukes County, Massachusetts
Census Tract 9900, Dukes County, Massachusetts

7.2.6 使用ANY运算符进行数组搜索

PostgreSQL 有一个专用于数组数据处理的 ANY 运算符,一般用于单个值与数组元素值的匹配比较场景,所以使用时还会需要有一个比较运算符或者比较关键字。ANY 运算符的效果是,只要数组中的任何元素与该行被比较的字段值匹配,则该行记录就被查询条件命中。

SELECT tract_name
FROM census.lu_tracts
WHERE tract_name ILIKE ANY(ARRAY['%99%duke%','%06%Barnstable%']::text[]);
tract_name
-----------------------------------------------------
Census Tract 102.06, Barnstable County, Massachusetts
Census Tract 103.06, Barnstable County, Massachusetts
Census Tract 106, Barnstable County, Massachusetts
Census Tract 9900, Dukes County, Massachusetts

7.2.7 可以返回结果集的函数

所谓返回结果集的函数就是能够一次性返回多行记录的函数。在一个复杂的 SQL 语句中使用返回结果集的函数很容易导致意外的结果,这是因为这类函数输出的结果集会与该语句其他部分生成的结果集产生笛卡儿积,从而生成更多的记录行。

在 SELECT 语句中使用返回结果集的函数

CREATE TABLE interval_periods (i_type interval);
INSERT INTO interval_periods (i_type)
VALUES ('5 months'), ('132 days'), ('4862 hours');
SELECT i_type,
generate_series('2012-01-01'::date,'2012-12-31'::date,i_type) As dt
FROM interval_periods;
i_type     | dt
-----------+-----------------------
5 months   | 2012-01-01 00:00:00-05
5 months   | 2012-06-01 00:00:00-04
5 months   | 2012-11-01 00:00:00-04
132 days   | 2012-01-01 00:00:00-05
132 days   | 2012-05-12 00:00:00-04
132 days   | 2012-09-21 00:00:00-04
4862 hours | 2012-01-01 00:00:00-05
4862 hours | 2012-07-21 15:00:00-04

7.2.8 限制对继承表的DELETE、UPDATE、INSERT操作的影响范围

如果表间是继承关系,那么查询父表时就会将子表中满足条件的记录也查出来。DELETE 和 UPDATE 操作也遵循类似逻辑,即对父表的修改操作也会影响到子表的记录。有时你可能希望操作仅限定于主表范围之内,并不希望子表受到波及。

PostgreSQL 提供了 ONLY 关键字以实现此功能。如果没有 ONLY 修饰符,我们最终将
从子表中删除先前可能已移动过的记录(即继承到子表的记录)。

7.2.9 DELETE USING语法

我们经常会遇到“只有当记录的字段值落在另外一个结果集中时,才需要删除该记录”的情况,那么此时就必须借助一次关联查询才能定位到要删除的目标记录。此时可以使用 USING 子句来指定需关联的表,然后在 WHERE 子句中编写 USING 子句的表和 FROM 子句的表之间的关联语句,来确定哪些记录需删除。USING 子句中可以指定多张表,中间用逗号分隔。

DELETE FROM census.facts
USING census.lu_fact_types As ft
WHERE facts.fact_type_id = ft.fact_type_id AND ft.short_name = 's01';

如果不使用该语法,一般的做法是在 WHERE 子句中使用笨拙的 IN 表达式。

7.2.10 将修改影响到的记录行返回给用户

通过 RETURNING 子句将在 DELETE 操作中被删除的记录返回给了用户。当然,INSERT 和 UPDATE 操作也是可以使用 RETURNING 的。对于带 serial 类型字段的表来说,RETURNING 语法是很有用的,因为向这类表中插入记录时,serial 字段是临时生成而非用户指定的;也就是说在插入动作完成之前,用户也不知道 serial 字段的值会是多少,除非是再查询一遍。而 RETURNING 语法使得用户不用再次查询就立即得到了 serial 字段的值。最常见的用法一般是 RETURNING *,即返回所有字段的值,但也可以指定仅返回特定字段。

在 UPDATE 语句中使用 RETURNING 子句返回修改过的记录

UPDATE census.lu_fact_types AS f
SET short_name = replace(replace(lower(f.fact_subcats[4]),' ','_'),':','')
WHERE f.fact_subcats[3] = 'Hispanic or Latino:' AND f.fact_subcats[4] > ''
RETURNING fact_type_id, short_name;
fact_type_id | short_name
-------------+--------------------------------------------------
96 			 | white_alone
97 			 | black_or_african_american_alone
98 			 | american_indian_and_alaska_native_alone
99 			 | asian_alone
100 		 | native_hawaiian_and_other_pacific_islander_alone
101 		 | some_other_race_alone
102 		 | two_or_more_races

7.2.11 UPSERT:INSERT时如果主键冲突则进行UPDATE

PostgreSQL 9.5 中引入了新的 INSERT ON CONFLICT 语法,业界一般也会将该功能称为 UPSERT。当无法确定即将插入的记录是否会与表中现有的记录冲突时,就可以使用 UPSERT 语法来确保不管是否有冲突都不会报错,该语法允许用户自定义发生冲突时的行为,要么更新现有记录,要么直接忽略。

使用该特性时,表上一定要有一个唯一性约束。这个约束可以是唯一性字段、主键、唯一索引或者排他性约束。一旦违反该约束,用户可以决定后续如何处理:用新记录覆盖现有记录,或者什么也不做。

CREATE TABLE colors(color varchar(50) PRIMARY KEY, hex varchar(6));

INSERT INTO colors(color, hex) VALUES('blue', '0000FF'), ('red', 'FF0000');

发生主键冲突时忽略冲突记录

INSERT INTO colors(color, hex)
VALUES('blue', '0000FF'), ('red', 'FF0000'), ('green', '00FF00')
ON CONFLICT DO NOTHING ;

如果有人插入了一条首字母大写的“Blue”的记录,由于与现有的“blue”并没有主键冲突,因此会插入成功,那么系统中实际上会出现两条同样表达“蓝色”的记录,这是我们不希望见到的。此时可以通过创建一个唯一索引来解决

CREATE UNIQUE INDEX uidx_colors_lcolor ON colors USING btree(lower(color));

此时再次插入“Blue”记录则会被唯一索引阻止,而且由于我们定义了 ON CONFLICT DO NOTHING,也就是冲突时什么都不干,所以效果相当于什么都没发生。如果希望用“Blue”来取代表中已有的“blue”。

INSERT INTO colors(color, hex)
VALUES('Blue', '0000FF'), ('Red', 'FF0000'), ('Green', '00FF00')
ON CONFLICT(lower(color))
DO UPDATE SET color = EXCLUDED.color, hex = EXCLUDED.hex;

当使用 ON CONFLICT DO UPDATE 这种搭配时,ON CONFLICT 后需要跟唯一性字段或者唯一约束名。如果后跟唯一性约束,则需要使用 ON CONFLICT ON CONSTRAINT + 约束名的语法。

INSERT INTO colors(color, hex)
VALUES('Blue', '0000FF'), ('Red', 'FF0000'), ('Green', '00FF00')
ON CONFLICT ON CONSTRAINT colors_pkey
DO UPDATE SET color = EXCLUDED.color, hex = EXCLUDED.hex;

只有当违反主键约束、唯一索引或者唯一键值约束时,DO 子句才会被触发,整体语句不会报错。但如果发生了数据类型错误或是违反检查约束等别的错误,整体语句还是会报错,DO UPDATE 子句不会被触发。

7.2.12 在查询中使用复合数据类型

PostgreSQL 会在创建表时自动创建一个结构与表完全相同的数据类型,其中包含了多个其他数据类型的成员字段,因此也会被称为复合数据类型。

SELECT x FROM census.lu_fact_types As x LIMIT 2;

你可能认为我们漏写了一个 .*,但请看一下该语句的执行结果:

x
------------------------------------------------------------------
(86,Population,"{D001,Total:}",d001)
(87,Population,"{D002,Total:,""Not Hispanic or Latino:""}",d002)

复合数据类型可以作为多个很有用的函数的输入,比如 array_agg 和 hstore(hstore 扩展包提供的一个函数,可以将一行记录转换为 hstore 的一个键值对象)等。

如果你正在开发 Web 应用,那么建议你充分利用 PostgreSQL 原生支持的 JSON 和 JSONB 数据类型的强大能力。

SELECT array_to_json(array_agg(f)) As catFROM (
SELECT MAX(fact_type_id) As max_type, category ➋
FROM census.lu_fact_types
GROUP BY category
) As f;

cats
----------------------------------------------------
[{
    
    "max_type":102,"category":"Population"},
{
    
    "max_type":153,"category":"Housing"}]

➊ 定义一个名为 f 的子查询,可以从中查出记录。
➋ 使用 array_agg 函数将子查询返回的结果集聚合为一个数组,然后将这个结果集数组通过 array_to_json 转变为一个 JSON 字段。

PostgreSQL 9.3 版提供了一个名为 json_agg 的函数,该函数的效果相当于上面示例中 array_to_json 和 array_agg 联用的效果,但执行速度更快,使用起来也更方便。

使用 json_agg 将查询结果转为 JSON 格式。

SELECT json_agg(f) As cats
FROM (
SELECT MAX(fact_type_id) As max_type, category
FROM census.lu_fact_types
GROUP BY category
) As f;

7.2.13 使用$文本引用符

PostgreSQL 支持使用 $$ 来将任意长度的文本包起来,这样就可以不用对文本中的
单引号做转义处理。$ 引用符还可用于执行动态 SQL 的场景,例如 exec(某 sql)。

如果需要用一个 SQL 来将两个字符串拼接起来,而这两个字符串中又含有很多单引号。

SELECT 'It''s O''Neil''s play. ' || 'It''ll start at two o''clock.'

使用 $$ 引用符看起来是这样的:

SELECT $$It's O'Neil's play. $$ || $$It'll start at two o'clock.$$

7.2.14 DO

DO 命令可以执行一个基于过程式语言的匿名代码段,可以将其看作一个一次性使用的匿名函数。示例中的匿名代码段是用 PL/pgSQL 编写的,但你也可以使用别的语言编写。

首先执行建表操作:

set search_path=census;
DROP TABLE IF EXISTS lu_fact_types CASCADE;
CREATE TABLE lu_fact_types (
fact_type_id serial,
category varchar(100),
fact_subcats varchar(255)[],
short_name varchar(50),
CONSTRAINT pk_lu_fact_types PRIMARY KEY (fact_type_id)
);

使用 DO 命令来生成动态 SQL:

DO language plpgsql
$$
DECLARE var_sql text;
BEGIN
var_sql := string_agg(
$sql$ ➊
INSERT INTO lu_fact_types(category, fact_subcats, short_name)
SELECT
'Housing',
array_agg(s$sql$ || lpad(i::text,2,'0')
|| ') As fact_subcats,'
|| quote_literal('s' || lpad(i::text,2,'0')) || ' As short_name
FROM staging.factfinder_import
WHERE s' || lpad(I::text,2,'0') || $sql$ ~ '^[a-zA-Z]+' $sql$, ';'
)
FROM generate_series(1,51) As I; ➋
EXECUTE var_sql;END
$$;

❶ 这里使用了 $ 引用符,因此不需要为后面的 Housing 前后的单引号进行转义。因为 DO 之后的命令 是用 $$ 引用符封装起来的,所以这里需要使用命名的 $ 引用符而不能使用 $$。我们选择的是 $sql$这个命名引用符。
❷ 使用 string_agg 函数让一组 SQL 语句形成单一字符串的形式 INSERT INTO
lu_fact_type(…) SELECT … WHERE s01 ~ ‘[a-zA-Z]+’;。
❸ 执行该 SQL。

7.2.15 适用于聚合操作的FILTER子句

9.4 版中新引入了用于聚合操作的 FILTER 子句,这是近期 ANSI SQL 标准中新加入的一个关键字。该关键字用于替代同为 ANSI SQL 标准语法的 CASE WHEN 子句,使聚合操作的语法得以简化。

在 AVG 聚合函数中使用 CASE WHEN

SELECT student,
AVG(CASE WHEN subject ='algebra' THEN score ELSE NULL END) As algebra,
AVG(CASE WHEN subject ='physics' THEN score ELSE NULL END) As physics
FROM test_scores
GROUP BY student;

AVG 聚合函数与 FILTER 子句的配合使用

SELECT student,
AVG(score) FILTER (WHERE subject ='algebra') As algebra,
AVG(score) FILTER (WHERE subject ='physics') As physics
FROM test_scores
GROUP BY student;

FILTER 子句的优势在于写法比较清晰简洁,并且操作大数据量时速度比较快。CASE 语句对于筛选掉的字段值是当成 NULL 处理的,因此对于 array_agg 这种会处理 NULL 值的聚合函数来说,使用 CASE WHEN 子句就不止是写法繁琐的问题了,还会导致输出不想要的结果。

CASE WHEN 子句与 array_agg 函数配合使用

SELECT student,
array_agg(CASE WHEN subject ='algebra' THEN score ELSE NULL END) As algebra,
array_agg(CASE WHEN subject ='physics' THEN score ELSE NULL END) As physics
FROM test_scores
GROUP BY student;
student | algebra                   | physics
--------+---------------------------+-------------------------------
jojo    | {
    
    74,NULL,NULL,NULL,74,..} | {
    
    NULL,83,NULL,NULL,NULL,79,..}
jdoe    | {
    
    75,NULL,NULL,NULL,78,..} | {
    
    NULL,72,NULL,NULL,NULL,72..}
robe    | {
    
    68,NULL,NULL,NULL,77,..} | {
    
    NULL,83,NULL,NULL,NULL,85,..}
lhsu    | {
    
    84,NULL,NULL,NULL,80,..} | {
    
    NULL,72,NULL,NULL,NULL,72,..}
(4 rows)

出的成绩列表中含有很多的 NULL 值,FILTER 子句与 array_agg 函数的配合使用

SELECT student,
array_agg(score) FILTER (WHERE subject ='algebra') As algebra,
array_agg(score) FILTER (WHERE subject ='physics') As physics
FROM test_scores
GROUP BY student;
student | algebra | physics
--------+---------+--------
jojo    | {
    
    74,74} | {
    
    83,79}
jdoe    | {
    
    75,78} | {
    
    72,72}
robe    | {
    
    68,77} | {
    
    83,85}
lhsu    | {
    
    84,80} | {
    
    72,72}

7.2.16 查询百分位数与最高出现频率数

PostgreSQL 9.4 中开始支持用于计算百分位数、中位数(等同于 0.5 百分位数)和最高出现频率数的统计函数。percentile_disc 和 percentile_cont 的区别在于二者处理同一百分位命中多条记录的方法。前者会取该百分位范围内的第一条命中记录的值,因此记录顺序会对最终返回哪条记录产生影响;后者会把百分位范围内命中的所有记录取均值后返回。中位数就是百分位值等于 0.5 的百分位数,因此计算中位数不需要一个专门的函数。mode 函数的作用是取分组中出现频率最高的值,如果最高频率的值有多个,则取排序后的第一个,因此排序方法会对 mode 计算的结果有影响。

计算中位数和出现频率最高的成绩(离散模式、连续模式)

SELECT
student,
percentile_cont(0.5) WITHIN GROUP (ORDER BY score) As cont_median,
percentile_disc(0.5) WITHIN GROUP (ORDER BY score) AS disc_median,
mode() WITHIN GROUP (ORDER BY score) AS mode,
COUNT(*) As num_scores
FROM test_scores
GROUP BY student
ORDER BY student;
student | cont_median | disc_median | mode | num_scores
--------+-------------+-------------+------+------------
alex    | 78          | 77          | 74   | 8
leo     | 72          | 72          | 72   | 8
regina  | 76          | 76          | 68   | 9
sonia   | 73.5        | 72          | 72   | 8
(4 rows)

一般的聚合函数是直接把要进行聚合运算的目标字段作为入参,但前面介绍的这几个函数不同,它们的直接入参是百分位数或者为空,目标聚合字段是通过后面的 WITHIN GROUP 子句中的 ORDER BY 修饰符来指定的。前述两个中位数计算函数还有一种用法,即可以输入多个数字作为百分位数,从而实现一次查询返回匹配多个百分位数的多个目标记录。

一次性计算多个百分位数

SELECT
student,
percentile_cont('{0.5,0.60,1}'::float[])
WITHIN GROUP (ORDER BY score) AS cont_median,
percentile_disc('{0.5,0.60,1}'::float[])
WITHIN GROUP (ORDER BY score) AS disc_median,
COUNT(*) As num_scores
FROM test_scores
GROUP BY student
ORDER BY student;

student | cont_median    | disc_median | num_scores
--------+----------------+-------------+------------
alex    | {
    
    78,79.2,84}   | {
    
    77,79,84}  | 8
leo     | {
    
    72,73.6,84}   | {
    
    72,72,84}  | 8
regina  | {
    
    76,76.8,90}   | {
    
    76,77,90}  | 9
sonia   | {
    
    73.5,75.6,86} | {
    
    72,75,86}  | 8
(4 rows)

如同其他聚合函数一样,你可以将这几个函数与聚合函数中一些通用的修饰符联用。

SELECT
student,
percentile_disc(0.5) WITHIN GROUP (ORDER BY score)
FILTER (WHERE subject = 'algebra') AS algebra,
percentile_disc(0.5) WITHIN GROUP (ORDER BY score)
FILTER (WHERE subject = 'physics') AS physics
FROM test_scores
GROUP BY student
ORDER BY student;

student | algebra | physics
--------+---------+--------
alex    | 74      | 79
leo     | 80      | 72
regina  | 68      | 83
sonia   | 75      | 72
(4 rows)

7.3 窗口函数

通过使用窗口函数,可以在当前记录行中访问到与其存在特定关系的其他记录行,相当于在每行记录上都开了一个访问外部数据的窗口,这也是“窗口函数”这个名称的由来。“窗口”就是当前行可见的外部记录行的范围。通过窗口函数可以把当前行的“窗口”区域内的记录的聚合运算结果附加到当前记录行。如果不借助窗口函数而又想要达到相同的效果,就只能使用关联操作和子查询。

通过使用窗口函数,可以在单个 SELECT 语句中同时获取到符合 fact_type_id=86 条件的记录的均值计算结果,以及原始记录的详细信息。请注意,语句执行时总是先筛选 WHERE 条件再计算窗口函数,因为这样显然可以避免做无用功。

SELECT tract_id, val, AVG(val) OVER () as val_avg
FROM census.facts
WHERE fact_type_id = 86;

tract_id    | val      | val_avg
------------+----------+----------------------
25001010100 | 2942.000 | 4430.0602165087956698
25001010206 | 2750.000 | 4430.0602165087956698
25001010208 | 2003.000 | 4430.0602165087956698
25001010304 | 2421.000 | 4430.0602165087956698
:

OVER 子句限定了窗口中的可见记录范围。本例中的 OVER 子句未设定任何条件,因此从该窗口中能看见全表的所有记录,所以 AVERAGE 运算的结果就是表中所有符合 fact_type_id=86 条件的记录中 val 字段的平均值。

PostgreSQL 在遍历每一行记录时都会基于全表记录进行一次 AVG 运算,然后将得到的均值作为当前行的一个字段输出。由于窗口数据域内包含多条记录,这意味着窗口函数运算的结果一定在多条记录上都是重复的。事实上,窗口函数实现了无须 GROUP BY 的聚合运算,还实现了无须 JOIN 的关联操作,从而将窗口函数的运算结果回填到记录行中。

7.3.1 PARTITION BY子句

窗口函数的窗口可见记录范围是可设置的,可以是全表记录,也可以是与当前行有关联关系的特定记录行。窗口可见记录范围的设置是通过 PARTITION BY 子句实现的,它可以指示 PostgreSQL 仅在满足条件的特定记录集上执行聚合操作。

使用县级编号作为窗口可见记录范围的筛选条件

SELECT tract_id, val, AVG(val) OVER (PARTITION BY left(tract_id,5)) As val_avg_county
FROM census.facts
WHERE fact_type_id = 2 ORDER BY tract_id;

tract_id    | val      | val_avg_county
------------+----------+----------------------
25001010100 | 1765.000 | 1709.9107142857142857
25001010206 | 1366.000 | 1709.9107142857142857
25001010208 | 984.000  | 1709.9107142857142857
:
25003900100 | 1920.000 | 1438.2307692307692308
25003900200 | 1968.000 | 1438.2307692307692308
25003900300 | 1211.000 | 1438.2307692307692308

7.3.2 ORDER BY子句

窗口函数的 OVER 子句中还可以使用 ORDER BY 子句,其作用可以理解为对窗口可见范围内的所有记录进行排序,并且窗口可见记录域是从结果集的第一条记录开始到当前记录为止的范围内。

使用 ROW_NUMBER 窗口函数进行编号操作

SELECT ROW_NUMBER() OVER (ORDER BY tract_name) As rnum, tract_name
FROM census.lu_tracts
ORDER BY rnum LIMIT 4;

rnum | tract_name
-----+-------------------------------------------------
1    | Census Tract 1, Suffolk County, Massachusetts
2    | Census Tract 1001, Suffolk County, Massachusetts
3    | Census Tract 1002, Suffolk County, Massachusetts
4    | Census Tract 1003, Suffolk County, Massachusetts

有两个 ORDER BY,前一个在 OVER 子句内生效,表明窗口可见区内的记录顺序,后一个针对整句生效,表明返回记录的整体顺序。请不要将二者的作用域混淆。

联用 PARTITION BY 和 ORDER BY

SELECT tract_id, val,
SUM(val) OVER (PARTITION BY left(tract_id,5) ORDER BY val) As sum_county_ordered
FROM census.facts
WHERE fact_type_id = 2
ORDER BY left(tract_id,5), val;

tract_id     | val      | sum_county_ordered
-------------+----------+-----------------
25001014100  | 226.000  | 226.000
25001011700  | 971.000  | 1197.000
25001010208  | 984.000  | 2181.000
:
25003933200  | 564.000  | 564.000
25003934200  | 593.000  | 1157.000
25003931300  | 606.000  | 1763.000

可以看到,上面输出的合计值是逐行累加的,这就是在 OVER 子句中应用了 ORDER BY 后的效果,即窗口可见域是从排序后的记录集的头条记录开始,到 ORDER BY 字段值与当前记录值匹配的那行记录为止,因此最终会呈现为动态累加的效果。

但请一定要牢记,OVER 子句中的 ORDER BY 与整句尾部的 ORDER BY 的作用是完全不同的。还可以通过 RANGE 或者 ROWS 关键字来显式指定窗口的可见记录域。例如:ROWS BETWEEN CURRENT ROW AND 5 FOLLOWING。

PostgreSQL 还支持建立命名窗口,该功能适用于在同一个查询中使用了多个窗口函数,且每个窗口函数的窗口定义都相同的情况。

命名窗口以及 LEAD 和 LAG 函数的用法

SELECT * FROM (
SELECT
ROW_NUMBER() OVER( wt ) As rnum, ➊
substring(tract_id,1, 5) As county_code,
tract_id,
LAG(tract_id,2) OVER wt As tract_2_before,
LEAD(tract_id) OVER wt As tract_after
FROM census.lu_tracts
WINDOW wt AS (PARTITION BY substring(tract_id,1, 5) ORDER BY tract_id)) As x
WHERE rnum BETWEEN 2 and 3 AND county_code IN ('25007','25025')
ORDER BY county_code, rnum;

rnum | county_code | tract_id    | tract_2_before | tract_after
-----+-------------+-------------+----------------+------------
2    | 25007       | 25007200200 |                | 25007200300
3    | 25007       | 25007200300 | 25007200100    | 25007200400
2    | 25025       | 25025000201 |                | 25025000202
3    | 25025       | 25025000202 | 25025000100    | 25025000301

❶ 直接复用窗口名,而不需要把窗口的完整定义再输一遍。
❷ 将我们的窗口命名为 wt 窗口。

LEAD 和 LAG 函数都有一个可选的 step 参数,该参数可以是正数也可以是负数,代表需要从当前记录开始向前或者向后跳几条记录才能访问到目标记录。当 LEAD 和 LAG 在寻找目标记录的过程中跳出了当前窗口的可见域时,就会返回 NULL。这种情况经常会遇到。

请注意:在 PostgreSQL 中,系统自带的以及用户自定义的聚合函数都可以作为窗口函数使用,但其他数据库一般仅支持 AVG、SUM、MIN、MAX 这些系统内置人聚合函数。

7.4 CTE表达式

公用表表达式(CTE)本质上来说就是在一个非常庞大的 SQL 语句中,允许用户通过一个子查询语句先定义出一个临时表,然后在这个庞大的 SQL 语句的不同地方都可以直接使用这个临时表。CTE 本质上就是当前语句执行期间内有效的临时表,一旦当前语句执行完毕,其内部的 CTE 表也随之失效。

基本 CTE
  这是最普通的 CTE,它可以使 SQL 语句的可读性更高,同时规划器在解析到这种 CTE 时会判定其查询代价是否很高,如果是的话,会考虑将其查询结果临时物化存储下来(此处概念跟物化视图非常类似),这样整个 SQL 语句的其他部分再访问此 CTE 时就会更快。
可写 CTE
  这是对基本 CTE 的一个功能扩展,其内部可以执行 UPDATE、INSERT 或者 DELETE 操作。该类 CTE 最后一般会返回修改后的记录集。
递归 CTE
  该类 CTE 在普通 CTE 的基础上增加了一个循环操作。在执行过程中,递归 CTE 返回的结果集会有所变化。

7.4.1 基本CTE用法介绍

WITH 关键字后面跟着的就是 CTE 表达式,外围的 SQL 语句会将该 CTE 作为一个临时
表来使用。

基本 CTE

WITH cte AS (
SELECT
tract_id, substring(tract_id,1, 5) As county_code,
COUNT(*) OVER(PARTITION BY substring(tract_id,1, 5)) As cnt_tracts
FROM census.lu_tracts
)
SELECT MAX(tract_id) As last_tract, county_code, cnt_tracts
FROM cte
WHERE cnt_tracts > 100
GROUP BY county_code, cnt_tracts;

单个 SQL 语句中可以创建多个 CTE,CTE 之间使用逗号分隔,所有的 CTE 表达式都要落在 WITH 子句范围内。

WITH

cte1 AS(
SELECT
ract_id,
ubstring(tract_id,1, 5) As county_code,
OUNT(*) OVER (PARTITION BY substring(tract_id,1,5)) As cnt_tracts
FROM census.lu_tracts
),

cte2 AS (
SELECT
MAX(tract_id) As last_tract,
county_code,
cnt_tracts
FROM cte1
WHERE cnt_tracts < 8 GROUP BY county_code, cnt_tracts
)

SELECT c.last_tract, f.fact_type_id, f.val
FROM census.facts As f INNER JOIN cte2 c ON f.tract_id = c.last_tract;

7.4.2 可写CTE用法介绍

首先创建一个子表:

CREATE TABLE logs_2011_01_02 (
PRIMARY KEY (log_id),
CONSTRAINT chk
CHECK (log_ts >= '2011-01-01' AND log_ts < '2011-03-01')
)
INHERITS (logs_2011);

父表包含了 2011 年全年的数据,子表包含了 2011 年 1 月和 2 月的数据,将父表的部分数据迁移到子表中。

WITH t AS (
DELETE FROM ONLY logs_2011 WHERE log_ts < '2011-03-01' RETURNING *
)
INSERT INTO logs_2011_01_02 SELECT * FROM t;

7.4.3 递归CTE用法介绍

如果要将一个基本 CTE 转换为递归 CTE,需要在 WITH 后加上 RECURSIVE 修饰符。递归 CTE 常用于表达消息线程和其他树状结构。通过查询系统 catalog 来展示数据库中的级联表关系。

WITH RECURSIVE tbls AS (
SELECT
c.oid As tableoid,
n.nspname AS schemaname,
c.relname AS tablename ➊
FROM
pg_class c LEFT JOIN
pg_namespace n ON n.oid = c.relnamespace LEFT JOIN
pg_tablespace t ON t.oid = c.reltablespace LEFT JOIN
pg_inherits As th ON th.inhrelid = c.oid
WHERE
th.inhrelid IS NULL AND
c.relkind = 'r'::"char" AND c.relhassubclass
UNION ALL
SELECT
c.oid As tableoid,
n.nspname AS schemaname,
tbls.tablename || '->' || c.relname AS tablename ➋ ➌
FROM
tbls INNER JOIN
pg_inherits As th ON th.inhparent = tbls.tableoid INNER JOIN
pg_class c ON th.inhrelid = c.oid LEFT JOIN
pg_namespace n ON n.oid = c.relnamespace LEFT JOIN
pg_tablespace t ON t.oid = c.reltablespace
)
SELECT * FROM tbls ORDER BY tablename; ➍
tableoid | schemaname | tablename
---------+------------+---------------------------------------
3152249  | public     | logs
3152260  | public     | logs->logs_2011
3152272  | public     | logs->logs_2011->logs_2011_01_02

❶ 查询出所有有子表而无父表的表。
❷ 这是递归查询部分,查询出了所有位于 tbls 临时表中的表的子表。
❸ 输出时在父表名之后附加子表的名称。
❹ 输出父表和所有子表。因为语句中要求输出结果按照表名排序,而每一层级的表名是将父表的名称排在子表之前,所以排序后的效果就是子表记录紧跟在其父表记录之后输出。

7.5 LATERAL横向关联语法

9.3 版中新支持。例如,下面的查询语句会报错,因为 L.year=2011 不是位于关联的右侧的一个列。

SELECT *
FROM
census.facts L
INNER JOIN
(
SELECT *
FROM census.lu_fact_types
WHERE category = CASE WHEN L.yr = 2011
THEN 'Housing' ELSE category END
) R
ON L.fact_type_id = R.fact_type_id;

加上了 LATERAL 关键字后就不会再报错:

SELECT *
FROM
census.facts L INNER JOIN LATERAL
(
SELECT *
FROM census.lu_fact_types
WHERE category = CASE WHEN L.yr = 2011
THEN 'Housing' ELSE category END
) R
ON L.fact_type_id = R.fact_type_id;

通过使用 LATERAL 语法,可以在一个 FROM 子句中跨两个表共享多列中的数据。但有个限制就是仅支持单向共享,即右侧的表可以提取左侧表中的数据,但反过来不行。

关联关系中左侧的一个列充当了右侧 generate_series 函数的一个形参。

CREATE TABLE interval_periods(i_type interval);
INSERT INTO interval_periods (i_type)
VALUES ('5 months'), ('132 days'), ('4862 hours');
SELECT i_type, dt
FROM
interval_periods CROSS JOIN LATERAL
generate_series('2012-01-01'::date, '2012-12-31'::date, i_type) AS dt
WHERE NOT (dt = '2012-01-01' AND i_type = '132 days'::interval);

i_type     | dt
-----------+-----------------------
5 mons     | 2012-01-01 00:00:00-05
5 mons     | 2012-06-01 00:00:00-04
5 mons     | 2012-11-01 00:00:00-04
132 days   | 2012-05-12 00:00:00-04
132 days   | 2012-09-21 00:00:00-04
4862:00:00 | 2012-01-01 00:00:00-05
4862:00:00 | 2012-07-21 15:00:00-04

通过关联关系左侧的数据来限制右侧的查询结果集中包含的记录数量。

SELECT u.user_name, l.description, l.log_ts
FROM
super_users AS u CROSS JOIN LATERAL (
SELECT description, log_ts
FROM logs
WHERE
log_ts > CURRENT_TIMESTAMP - interval '100 days' AND
logs.user_name = u.user_name
ORDER BY log_ts DESC LIMIT 5
) AS l;

虽然你也可以通过窗口函数来实现相同的效果,但 LATERAL 关联执行速度更快,语法也更简洁。在同一条 SQL 语句中可以多次使用 LATERAL 关联,当需要关联多个子查询时,甚至可以级联使用LATERAL。在 Oracle 中,横向关联通过管道函数实现,在 SQL Server 中使用 CROSS APPLY 或者 OUTER APPLY 语法来实现。

7.6 WWITH ORDINALITY子句

PostgreSQL 9.4 开始支持,WITH ORDINALITY 子句的作用是为函数返回的结果集中的每一行自动附加一个序列号字段。

在普通的表查询语句和子查询语句中不可以使用 WITH ORDINALITY 语法,但可以使用 ROW_NUMBER 窗口函数,效果相同。WITH ORDINALITY 经常与 generate_series、unnest 之类可以将复合数据类型和数组展开的函数联用。事实上,WITH ORDINALTY 语法可与任何返回多条记录的函数联用,包括用户自定义的函数。

对函数返回的多条记录结果进行编号

SELECT dt.*
FROM generate_series('2016-01-01'::date,'2016-12-31'::date,interval '1 month')
WITH ORDINALITY As dt;
dt 					   | ordinality
-----------------------+-----------
2016-01-01 00:00:00-05 | 1
2016-02-01 00:00:00-05 | 2
2016-03-01 00:00:00-05 | 3
2016-04-01 00:00:00-04 | 4
2016-05-01 00:00:00-04 | 5
2016-06-01 00:00:00-04 | 6
2016-07-01 00:00:00-04 | 7
2016-08-01 00:00:00-04 | 8
2016-09-01 00:00:00-04 | 9
2016-10-01 00:00:00-04 | 10
2016-11-01 00:00:00-04 | 11
2016-12-01 00:00:00-05 | 12
(12 rows)

WITH ORDINALITY 会在查询结果的最后增加一个名为 ordinality 的字段,WITH ORDINALITY 子句只能出现在 SQL 语句的 FROM 子句中。

将 WITH ORDINALITY 与 LATERAL 联用

SELECT d.ord, i_type, d.dt
FROM
interval_periods CROSS JOIN LATERAL
generate_series('2012-01-01'::date, '2012-12-31'::date, i_type)
WITH ORDINALITY AS d(dt,ord)
WHERE NOT (dt = '2012-01-01' AND i_type = '132 days'::interval);

ord | i_type     | dt
----+------------+-----------------------
1   | 5 mons     | 2012-01-01 00:00:00-05
2   | 5 mons     | 2012-06-01 00:00:00-04
3   | 5 mons     | 2012-11-01 00:00:00-04
2   | 132 days   | 2012-05-12 00:00:00-04
3   | 132 days   | 2012-09-21 00:00:00-04
1   | 4862:00:00 | 2012-01-01 00:00:00-05
2   | 4862:00:00 | 2012-07-21 15:00:00-04
(7 rows)

WITH ORDINALITY 与返回多行记录的函数联用,它只能在 WHERE 子句之前的 FROM 部分生效,因此查询得到的最终结果中会出现序号非连续的情况(比如上例中的 132 days 对应的序号 1 就没有了),其原因是有的记录被 WHERE 条件过滤掉了。

7.7 GROUPING SETS、CUBE和ROLLUP语法

如果你需要创建一个包含总计和分子类总计的报表,GROUPING SETS 就是为这种场景量身定做的。

统计每个学生的综合平均分以及每个学生分科目的平均分

SELECT student, subject, AVG(score)::numeric(10,2)
FROM test_scores
WHERE student IN ('leo','regina')
GROUP BY GROUPING SETS ((student),(student,subject))
ORDER BY student, subject NULLS LAST;

student  | subject 	 | avg
---------+-----------+-------
leo      | algebra   | 82.00
leo 	 | calculus  | 65.50
leo 	 | chemistry | 75.50
leo 	 | physics   | 72.00
leo 	 | NULL      | 73.75
regina   | algebra   | 72.50
regina   | calculus  | 64.50
regina   | chemistry | 73.50
regina   | economics | 90.00
regina   | physics   | 84.00
regina   | NULL      | 75.44
(11 rows)

通过单个 SQL 语句就统计出了每个学生所有科目的平均分以及每个学生每个科目的平均分。

统计每个学生的综合平均分、每个学生分科目的平均分以及某个科目所有学生的平均分。

SELECT student, subject, AVG(score)::numeric(10,2)
FROM test_scores
WHERE student IN ('leo','regina')
GROUP BY GROUPING SETS ((student,subject),(student),(subject))
ORDER BY student NULLS LAST, subject NULLS LAST;

student  | subject 	 | avg
---------+-----------+-------
leo 	 | algebra   | 82.00
leo 	 | calculus  | 65.50
leo 	 | chemistry | 75.50
leo 	 | physics   | 72.00
leo 	 | NULL      | 73.75
regina   | algebra   | 72.50
regina   | calculus  | 64.50
regina   | chemistry | 73.50
regina   | economics | 90.00
regina   | physics   | 84.00
regina   | NULL      | 75.44
NULL 	 | algebra   | 77.25
NULL 	 | calculus  | 65.00
NULL 	 | chemistry | 74.50
NULL 	 | economics | 90.00
NULL 	 | physics   | 78.00
(16 rows)

GROUPING SETS((student),(student, subject),()),这种递进式的分组聚合可以用 ROLLUP(student,subject) 来表达。

统计每个学生的综合平均分、每个学生分科目的平均分以及全局综合平均分。

SELECT student, subject, AVG(score)::numeric(10,2)
FROM test_scores
WHERE student IN ('leo','regina')
GROUP BY ROLLUP (student,subject)
ORDER BY student NULLS LAST, subject NULLS LAST;

student  | subject   | avg
---------+-----------+-------
leo 	 | algebra   | 82.00
leo 	 | calculus  | 65.50
leo 	 | chemistry | 75.50
leo 	 | physics   | 72.00
leo 	 | NULL 	 | 73.75
regina   | algebra   | 72.50
regina   | calculus  | 64.50
regina   | chemistry | 73.50
regina   | economics | 90.00
regina   | physics   | 84.00
regina   | NULL 	 | 75.44
NULL     | NULL 	 | 74.65
(12 rows)

如果把 ROLLUP 的入参字段顺序反过来,那么得到的是每个学生分科目的平均分、每个科目的平均分、全局综合平均分。统计每个学生分科目的平均分、每个科目的平均分、全局综合平均分。

SELECT student, subject, AVG(score)::numeric(10,2)
FROM test_scores
WHERE student IN ('leo','regina')
GROUP BY ROLLUP (subject,student)
ORDER BY student NULLS LAST, subject NULLS LAST;

student  | subject   | avg
---------+-----------+-------
leo 	 | algebra   | 82.00
leo 	 | calculus  | 65.50
leo 	 | chemistry | 75.50
leo 	 | physics   | 72.00
regina   | algebra   | 72.50
regina   | calculus  | 64.50
regina   | chemistry | 73.50
regina   | economics | 90.00
regina   | physics   | 84.00
NULL 	 | algebra   | 77.25
NULL 	 | calculus  | 65.00
NULL 	 | chemistry | 74.50
NULL 	 | economics | 90.00
NULL 	 | physics   | 78.00
NULL 	 | NULL 	 | 74.65
(15 rows)

如果希望把前两种统计的结果综合一下,即包含每个学生所有科目的综合平均分、每个科目所有学生的综合平均分、每个学生每个科目的平均分以及全局不分科目和学生的综合平均分,那么可以写成 GROUPING SETS((student), (student, subject), (subject), ()),或者可以使用CUBE(student, subject) 语法。

SELECT student, subject, AVG(score)::numeric(10,2)
FROM test_scores
WHERE student IN ('leo','regina')
GROUP BY CUBE (student, subject)
ORDER BY student NULLS LAST, subject NULLS LAST;

student  | subject   | avg
---------+-----------+-------
leo 	 | algebra   | 82.00
leo 	 | calculus  | 65.50
leo 	 | chemistry | 75.50
leo 	 | physics   | 72.00
leo 	 | NULL 	 | 73.75
regina   | algebra   | 72.50
regina   | calculus  | 64.50
regina   | chemistry | 73.50
regina   | economics | 90.00
regina   | physics   | 84.00
regina   | NULL 	 | 75.44
NULL 	 | algebra   | 77.25
NULL 	 | calculus  | 65.00
NULL 	 | chemistry | 74.50
NULL 	 | economics | 90.00
NULL 	 | physics   | 78.00
NULL 	 | NULL 	 | 74.65
(17 rows)

猜你喜欢

转载自blog.csdn.net/qq_42226855/article/details/110290784