您的位置:首页 > 数据库

PostgreSQL 9.4文档 第7章 查询

2015-02-06 22:01 218 查看
上一章介绍了如何创建表,如何填充数据,如何操作数据。本章我们将要讨论如何检索数据库中的数据。

7.1. 概述

检索数据的过程或者命令称为查询。在SQL中SELECT命令用于指定查询。SELECT命令的一般语法如下:

[WITH with_queries] SELECT select_list FROM table_expression [sort_specification]

随后的章节将会详细介绍查询列表,表表达式,以及排序规范。WITH查询最后进行讨论,因为它属于高级特性。

以下是一个简单的查询:

SELECT * FROM table1;

假设存在一个表table1,该命令将会检索出表table1的所有行和所有列。(检索方法取决于客户端应用。例如,psql程序自动在屏幕上显示一个ASCII艺术风格的表,而客户端程序库提供从查询结果获取单个值得功能。)查询列表中的*表示表表达式中的所有列。查询列表也可以选择可用列的一个子集或者基于这些列的计算值。例如,假设表table1包含列a,b和c(可能还包含其他列),可以执行以下查询:

SELECT a, b + c FROM table1;

(假设b和c的数据类型为数字)。更多细节参考7.3节。

FROM table1是一个简单的表表达式:它只读取一个表的数据。通常,表表达式可以是基表,连接,以及子查询的复杂结构。但是,你也可以省略整个表表达式,将SELECT命令作为一个计算器:

SELECT 3 * 4;

这对于选择列表中的表达式返回变化结果的情况更有用处。例如,你可以使用以下方式调用函数:

SELECT random();

7.2. 表表达式

表表达式的结果是一个表。表表达式包含一个FROM子句,以及可选的WHERE,GROUP BY以及HAVING子句。简单的表表达式只是引用磁盘上的一个表,即所谓的基表,但是我们可以通过不同的方式使用更复杂的表达式修改或者组合基表。

表表达式中可选的WHERE,GROUP BY,以及HAVING子句指定了一个在FROM的结果表上连续执行的转换管道。所有的这些转换最终产生一个虚拟表,提供了传递给查询列表计算输出行的数据。

7.2.1. FROM子句

FROM子句从一个逗号分隔的表引用列表生成一个虚拟表。

FROM table_reference [, table_reference [, ...]]

表引用可以是一个表名(可能包含模式限定符),或者一个派生表,例如子查询,JOIN结构,或这些对象的复杂组合。如果FROM子句中包含了多个表引用,这些表是交叉连接的(也就是行的笛卡儿积;参加下文)。FROM列表的结果是一个中间的虚拟表,然后通过WHERE,GROUP BY以及HAVING子句的转换,最终生成整个表表达式的结果。

如果一个表引用名是一个继承层次中的父表,该表引用不仅产生自己的行,还包含所有的子孙表的行,除非在表名前面使用了关键字ONLY。但是,该表引用只产生父表中的列,任何子表中额外的列都将被忽略。

与表名前面添加ONLY相反,你可以在表名后面添加*,明确指定包含子孙表的数据。*不是必须的,因为这是默认方式(除非修改了sql_inheritance配置项的值)。但是*可以用于强调将会查找附加的表。

7.2.1.1. 连接表

连接表是两个表(真实的或者派生的)根据特定连接类型的规则派生的表。连接类型包括内连接,外连接,以及交叉连接。表连接的一般语法如下:

T1 join_type T2 [ join_condition ]

所有类型的连接都可以进行串连或嵌套:T1和/或T2都可以是连接表。括号可以用于控制JOIN子句的连接顺序。如果没有括号,JOIN子句从左到右进行嵌套。

连接类型

交叉连接

T1 CROSS JOIN T2

对于T1和T2中的数据行的各种可能的组合(也就是笛卡儿积),连接表将会包含T1中所有的列加上T2中所有的列。如果这两个表分别有N行和M行,连接表将会包含N * M行。

FROM T1 CROSS JOIN T2等价于FROM T1 INNER JOIN T2 ON TRUE(参见下文),也等价于FROM T1, T2。

注意:第二个等价形式对于多于两个表的情况并不完全准确,因为JOIN比逗号的结合性更紧密。例如,FROM T1 CROSS JOIN T2 INNER JOIN T3 ON condition与FROM T1, T2 INNER JOIN T3 ON condition并不相同,因为第一种形式中的condition可以引用T1,而第二种形式不能。

条件连接

T1 { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2 ON boolean_expression

T1 { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2 USING ( join column list )

T1 NATURAL { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2

关键字INNER和OUTER在所有形式中都是可选的。INNER是默认值;LEFT,RIGHT和FULL表示外连接。

连接条件在ON或USING子句中指定,或者由NATURAL隐含指定。连接条件决定两个源表中“匹配”的行,详细解释如下。

条件连接包括以下类型:

INNER JOIN

对于T1中的每一行R1,T2中满足与R1的连接条件的每一行都会在连接表中存在一行。

LEFT OUTER JOIN

首先,执行一个内连接。然后,对于T1中不匹配T2中任何行的每一行,将会生成一个连接行,对应T2的列值使用空值表示。因此,连接表对于T1中的每一行至少存在一行。

RIGHT OUTER JOIN

首先,执行一个内连接。然后,对于T2中不匹配T1中任何行的每一行,将会生成一个连接行,对应T1的列值使用空值表示。这与左连接的结果相反:结果表对于T2中的每一行至少存在一行。

FULL OUTER JOIN

首先,执行一个内连接。然后,对于T1中不匹配T2中任何行的每一行,将会生成一个连接行,对应T2的列值使用空值表示。同样,对于T2中不匹配T1中任何行的每一行,将会生成一个连接行,对应T1的列值使用空值表示。

ON子句是最通用的连接条件:它接受一个与WHERE子句中相同的布尔值表达式。如果ON表达式计算结果为真,表示T1和T2中的一个行对能够匹配。

USING子句是一个简写,可以用于连接条件两边的列名相同的特定场景。它接受一个由公共列名组成的逗号分隔的列表,并形成一个相等比较的连接条件。例如,使用USING (a, b)连接T1和T2将会产生连接条件ON T1.a = T2.a AND T1.b = T2.b。

此外,JOIN USING的输出中排除了重复的列:没有必要将匹配的列输出两次,因为它们的值完全相同。虽然JOIN ON将会输出T1的所有列和T2的所有列,但是JOIN USING对于每个列对(按照列出的顺序)只产生一个输出列,然后是T1中的剩余列,以及T2中的剩余列。

最后,NATURAL是USING的一个简写形式:它使用两个输入表中都存在的相同列名组成一个USING列表。与USING一样,这些列只在输出表中出现一次。如果不存在共同的列名,NATURAL与CROSS JOIN的结果类似。

注意:USING对于连接表中的列变更是相当安全的,因为只有明确列出的列才能关联。NATURAL相对更加危险,因为任何会产生新的列名匹配的模式变更,将会导致连接条件中包含新的列。

接下来一起演示这些连接类型,假设存在表t1:

num | name

-----+------

1 | a

2 | b

3 | c

以及表t2:

num | value

-----+-------

1 | xxx

3 | yyy

5 | zzz

不同连接类型的结果如下:

=> SELECT * FROM t1 CROSS JOIN t2;

num | name | num | value

-----+------+-----+-------

1 | a | 1 | xxx

1 | a | 3 | yyy

1 | a | 5 | zzz

2 | b | 1 | xxx

2 | b | 3 | yyy

2 | b | 5 | zzz

3 | c | 1 | xxx

3 | c | 3 | yyy

3 | c | 5 | zzz

(9 rows)

=> SELECT * FROM t1 INNER JOIN t2 ON t1.num = t2.num;

num | name | num | value

-----+------+-----+-------

1 | a | 1 | xxx

3 | c | 3 | yyy

(2 rows)

=> SELECT * FROM t1 INNER JOIN t2 USING (num);

num | name | value

-----+------+-------

1 | a | xxx

3 | c | yyy

(2 rows)

=> SELECT * FROM t1 NATURAL INNER JOIN t2;

num | name | value

-----+------+-------

1 | a | xxx

3 | c | yyy

(2 rows)

=> SELECT * FROM t1 LEFT JOIN t2 ON t1.num = t2.num;

num | name | num | value

-----+------+-----+-------

1 | a | 1 | xxx

2 | b | |

3 | c | 3 | yyy

(3 rows)

=> SELECT * FROM t1 LEFT JOIN t2 USING (num);

num | name | value

-----+------+-------

1 | a | xxx

2 | b |

3 | c | yyy

(3 rows)

=> SELECT * FROM t1 RIGHT JOIN t2 ON t1.num = t2.num;

num | name | num | value

-----+------+-----+-------

1 | a | 1 | xxx

3 | c | 3 | yyy

| | 5 | zzz

(3 rows)

=> SELECT * FROM t1 FULL JOIN t2 ON t1.num = t2.num;

num | name | num | value

-----+------+-----+-------

1 | a | 1 | xxx

2 | b | |

3 | c | 3 | yyy

| | 5 | zzz

(4 rows)

通过ON指定的连接条件也可以包含与连接没有直接关系的条件。这对于某些查询非常有用,但是需要考虑清楚。例如:

=> SELECT * FROM t1 LEFT JOIN t2 ON t1.num = t2.num AND t2.value = 'xxx';

num | name | num | value

-----+------+-----+-------

1 | a | 1 | xxx

2 | b | |

3 | c | |

(3 rows)

注意,如果使用WHERE子句进行限制,将会产生一个不同的结果:

=> SELECT * FROM t1 LEFT JOIN t2 ON t1.num = t2.num WHERE t2.value = 'xxx';

num | name | num | value

-----+------+-----+-------

1 | a | 1 | xxx

(1 row)

这是因为ON子句在连接之前处理,而WHERE子句在连接之后处理。这对于内连接没有什么区别,但是对于外连接非常重要。

7.2.1.2. 表别名和列别名

可以给表或者复杂的表引用指定一个临时名称,然后在查询的其他地方使用它引用这个派生表。这个临时名称被称为表别名。

表别名的创建方式如下:

FROM table_reference AS alias

或者写成:

FROM table_reference alias

关键字AS可选。alias可以是任意标识符。

表别名的一个典型应用就是为长表名指定一个短标识符,提高连接子句的可读性。例如:

SELECT * FROM some_very_long_table_name s JOIN another_fairly_long_name a ON s.id = a.num;

就当前查询而言,别名就是表引用的新名字,查询的其他地方不允许再引用原始的表名。因此,以下语句是无效的:

SELECT * FROM my_table AS m WHERE my_table.a > 5; -- wrong

表别名主要是为了便于标识,但是表的自连接必须使用别名,例如:

SELECT * FROM people AS mother JOIN people AS child ON mother.id = child.mother_id;

另外,如果表引用是一个子查询,必须使用表别名(参见7.2.1.3节)。

括号可以用于消除歧义。在下面的示例中,第一个语句将别名b赋予了my_table的第二个实例,但是第二个语句将别名赋予了连接的结果:

SELECT * FROM my_table AS a CROSS JOIN my_table AS b ...

SELECT * FROM (my_table AS a CROSS JOIN my_table) AS b ...

表别名的另一种形式为表的列和该表都指定了临时名称:

FROM table_reference [AS] alias ( column1 [, column2 [, ...]] )

如果只为部分列指定了别名,剩下的列使用原来的名称。这种语法对于自连接或者子查询尤其有用。

如果表别名用于JOIN子句的输出,该表名在JOIN中隐藏了原始的名称。例如:

SELECT a.* FROM my_table AS a JOIN your_table AS b ON ...

是一个有效的SQL,但是:

SELECT a.* FROM (my_table AS a JOIN your_table AS b ON ...) AS c

是无效的;表别名a在别名c的外面是不可见的。

7.2.1.3. 子查询

子查询规定了派生表必须使用括号包围i,并且必须指定一个表别名(参考7.2.1.2节)。例如:

FROM (SELECT * FROM table1) AS alias_name

这个示例等价于FROM table1 AS alias_name。当子查询包含分组和聚合时,将会出现更有趣的情况,这种情况无法简写为普通的连接。

子查询也可以是一个VALUES列表:

FROM (VALUES ('anne', 'smith'), ('bob', 'jones'), ('joe', 'blow'))

AS names(first, last)

同样,表别名是必须的。VALUES列表的列别名是可选的,不过是一个良好的实践。更多信息参见7.7节。

7.2.1.4. 表函数

表函数是结果为一组行的函数,行的值可以是基本数据类型(标量)或者复合数据类型(表行)。它们的使用方式类似于表,视图,或者FROM子句中的子查询。表函数返回的列可以像表,视图或子查询的列一样用于SELECT,JOIN或者WHERE子句。

多个表函数也可以通过ROWS FROM语法进行组合,结果为它们各自值的并列;结果行的数量为各个函数结果的最大值,行数少的结果使用空值表示。

function_call [WITH ORDINALITY] [[AS] table_alias [(column_alias [, ... ])]]

ROWS FROM( function_call [, ... ] ) [WITH ORDINALITY] [[AS] table_alias [(column_alias [, ... ])]]

如果指定了WITH ORDINALITY子句,将在函数的结果中添加一个额外的列,类型为bigint。该列将函数的结果行集进行了编号,从1开始。(这是针对SQL标准语法UNNEST ... WITH ORDINALITY的一个泛化。)默认情况下,该序号列被称为ordinality,但是可以使用AS子句为它指定一个其他的列名。

特殊的表函数UNNEST可以使用任意数量的数组参数,并且返回相同数量的列,就像单独使用每个参数调用UNNEST,然后使用ROWS FROM进行合并。

如果没有指定table_alias,函数名称将被当作表名;对于ROWS FROM ( )构造函数,将使用第一个函数名作为表名。

如果没有提供列别名,对于返回基本数据类型的函数,列名也和函数名相同。对于返回复合类型的函数,结果列的名称分别为该类型的各个属性名。

以下是一些示例:

CREATE TABLE foo (fooid int, foosubid int, fooname text);

CREATE FUNCTION getfoo(int) RETURNS SETOF foo AS $$

SELECT * FROM foo WHERE fooid = $1;

$$ LANGUAGE SQL;

SELECT * FROM getfoo(1) AS t1;

SELECT * FROM foo

WHERE foosubid IN (

SELECT foosubid

FROM getfoo(foo.fooid) z

WHERE z.fooid = foo.fooid

);

CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1);

SELECT * FROM vw_getfoo;

有时候,表函数需要定义为根据调用情况返回不同的列组。为了支持这个特性,表函数可以定义为返回伪类型record。在查询中使用这种函数时,必须在查询中指定想要返回的行结构,以便系统知道如何解析和计划该查询。这种语法格式如下:

function_call [AS] alias (column_definition [, ... ])

function_call AS [alias] (column_definition [, ... ])

ROWS FROM( ... function_call AS (column_definition [, ... ]) [, ... ] )

如果使用没有ROWS FROM ( )的语法,column_definition列表替代了原本添加到FROM的列别名;列定义的名称相当于列别名。如果使用ROWS FROM ( )语法,每个成员函数可以单独添加一个column_definition列表;或者只有一个成员函数并且没有使用WITH ORDINALITY子句时,可以在ROWS FROM ( )后面使用column_definition列表替代列别名。

考虑以下示例:

SELECT *

FROM dblink('dbname=mydb', 'SELECT proname, prosrc FROM pg_proc')

AS t1(proname name, prosrc text)

WHERE proname LIKE 'bytea%';

函数dblink(dblink模块的一部分)用于指定远程查询。它被声明为返回record,因为它可能被用于各种查询。在调用语句中必须指定实际的列组,以便解析器知道如何操作,例如*应该扩展为什么内容。

7.2.1.5. LATERAL子查询

FROM中的子查询前面可以添加关键字LATERAL。

7.2.2 WHERE子句

WHERE子句的语法如下:

WHERE search_condition

其中,search_condition可以是任何返回类型为boolean的值表达式(参见4.2节)。

在FROM子句处理完成之后,派生的虚拟表中的每一行都要经过该搜索条件的检查。如果结果为真,输出结果中保留该行,否则(即结果为假或者未知)丢弃该行。搜索条件通常至少引用FROM子句结果表中的一列;这不是必须的,但是如果不是这样的话,WHERE子句将会没有什么实际作用。

注意:内连接的连接条件既可以写在WHERE子句中,也可以写在JOIN子句中。例如,以下表表达式是等价的:

FROM a, b WHERE a.id = b.id AND b.val > 5

和:

FROM a INNER JOIN b ON (a.id = b.id) WHERE b.val > 5

甚至:

FROM a NATURAL JOIN b WHERE b.val > 5

使用以上那一种形式只是风格问题。FROM子句中的JOIN语法很可能无法迁移到其他SQL数据库系统,即使它属于SQL标准。外连接没有选择余地:连接条件必须写在FROM子句中。外连接的ON子句或USING子句不等于WHERE条件,因为它会导致在最终的结果中集排除了某些行,又添加了某些行(未匹配的输入行)。

以下是一些WHERE子句的示例:

SELECT ... FROM fdt WHERE c1 > 5

SELECT ... FROM fdt WHERE c1 IN (1, 2, 3)

SELECT ... FROM fdt WHERE c1 IN (SELECT c1 FROM t2)

SELECT ... FROM fdt WHERE c1 IN (SELECT c3 FROM t2 WHERE c2 = fdt.c1 + 10)

SELECT ... FROM fdt WHERE c1 BETWEEN (SELECT c3 FROM t2 WHERE c2 = fdt.c1 + 10) AND 100

SELECT ... FROM fdt WHERE EXISTS (SELECT c1 FROM t2 WHERE c2 > fdt.c1)

fdt是FROM子句中的派生表。不满足WHERE子句的搜索条件的行将被排除。注意其中的标量子查询被用作值表达式。与其他查询相同,子查询可以包含复杂的表表达式。还需要注意如何在子查询中引用fdt。只有当子查询的输入表中也存在名为c1的列时,才需要将c1限定为fdt.c1。但是这种列名限定可以使得代码更加清晰。这个示例演示了外部查询的字段命名范围如何扩展到内部查询。

7.2.3. GROUP BY和HAVING子句

通过WHERE过滤器之后生成的输入表可以使用GROUP BY子句进行分组,然后使用HAVING子句排除一些分组行。

SELECT select_list

FROM ...

[WHERE ...]

GROUP BY grouping_column_reference [, grouping_column_reference]...

GROUP BY子句可以将那些所有grouping_column_reference的值都相同的行分为一组。分组列的顺序无所谓。结果就是将具有相同值的行集合并为一个组行,代表该组中的所有行。这样可以去除输出中的冗余,并且/或者计算分组的聚合值。例如:

=> SELECT * FROM test1;

x | y

---+---

a | 3

c | 2

b | 5

a | 1

(4 rows)

=> SELECT x FROM test1 GROUP BY x;

x

---

a

b

c

(3 rows)

对于第二个查询,不能写成SELECT * FROM test1 GROUP BY x,因为对于字段y,没有对应到每个组的单一值。分组字段可以在查询列表中引用,因为它们在每个组中都只有一个值。

通常来说,如果一个表进行了分组查询,不在GROUP BY列表中的字段不能被引用,除非是在聚合表达式中。以下是一个使用聚合表达式的示例:

=> SELECT x, sum(y) FROM test1 GROUP BY x;

x | sum

---+-----

a | 4

b | 5

c | 2

(3 rows)

其中,sum是一个基于整个组计算出一个和值的聚合函数。关于可以使用的聚合函数,可以参考9.20节。

提示:不带聚合表达式的分组实际上是计算了一个字段中的不同值的组合。这个结果也可以通过DISTINCT子句获得(参考7.3.3小节)。

以下是另一个示例:它计算了每个产品的总销售额(而不是所有产品的总销售额):

SELECT product_id, p.name, (sum(s.units) * p.price) AS sales

FROM products p LEFT JOIN sales s USING (product_id)

GROUP BY product_id, p.name, p.price;

该示例中的字段product_id,p.name和p.price必须出现在GROUP BY子句中,因为它们在查询列表中被引用了(但是参见下文)。字段s.units必须要出现在GROUP BY列表中,因为它只用在聚合表达式中(sum(...)

),表示各个产品的销售额。对于每个产品,该查询返回一行关于该产品的总销售额的汇总。

如果产品表的product_id是主键,以上示例只需要按照product_id进行分组,因为产品名称和价格函数依赖于产品ID,因此对于每个产品ID组,产品名称和价格都是唯一的。

在严格的SQL标准中,GROUP BY只能按照源表中的字段进行分组,但是PostgreSQL进行了扩展,允许按照查询列表中的字段进行分组。同时也允许使用值表达式进行分组。

如果已经将一个表进行了分组,但是只对某些分组感兴趣,可以使用HAVING子句,和WHERE非常相似,从结果中排除某些组。该语法如下:

SELECT select_list FROM ... [WHERE ...] GROUP BY ... HAVING boolean_expression

HAVING子句中的表达式可以是分组表达式或者未分组表达式(后者必须是一个聚合函数)。

例如:

=> SELECT x, sum(y) FROM test1 GROUP BY x HAVING sum(y) > 3;

x | sum

---+-----

a | 4

b | 5

(2 rows)

=> SELECT x, sum(y) FROM test1 GROUP BY x HAVING x < 'c';

x | sum

---+-----

a | 4

b | 5

(2 rows)

以下是一个更真实的示例:

SELECT product_id, p.name, (sum(s.units) * (p.price - p.cost)) AS profit

FROM products p LEFT JOIN sales s USING (product_id)

WHERE s.date > CURRENT_DATE - INTERVAL '4 weeks'

GROUP BY product_id, p.name, p.price, p.cost

HAVING sum(p.price * s.units) > 5000;

在以上示例中,WHERE子句通过一个未分组的字段选择数据行(该表达式只对于最近4周的销售返回真),而HAVING子句将结果限制为总销售额大于5000的分组。注意HAVING中的聚合表达式不需要与查询部分的聚合表达式完全相同。

如果一个查询包含了聚合函数,但是没有GROUP BY子句,仍然会进行分组:结果将是单个分组行(如果该行被HAVING排除,将会返回零行)。如果查询包含一个HAVING子句,结果也是一样,甚至不需要聚合函数或GROUP BY子句。

7.2.4. 窗口函数处理

如果查询包含任何窗口函数(参考3.5,9.21,以及4.2.8),这些函数将会在分组,聚合,以及HAVING过滤之后进行计算。也就是说,如果查询使用了聚合,GROUP BY或者HAVING,窗口函数看到的行是组内的行,而不是来自FROM/WHERE的行。

如果使用了多个窗口函数,拥有句法等价的PARTITION BY和OREDER BY子句的窗口函数能够确保通过一次数据扫描获得计算结果。因此它们能够看到相同的排序顺序,即使ORDER BY不能唯一确定一个顺序。但是,对于具有不同PARTITION BY或ORDER BY的函数,不能获得保证。(这种情况下窗口函数计算的过程中通常需要一个排序步骤,而且对于ORDER BY认为相等的值,排序也不能保证维持行的顺序。)

目前,窗口函数总是需要预排序的数据,因此查询结果将会根据一个或者其他窗口函数的PARTITION BY/ORDER BY子句进行排序。但是,不推荐依赖于该结果。如果想要确保结果按照某种方式进行排序,可以在最外层查询中使用一个明确的ORDER BY子句。

7.3. 选择列表

如上一节所述,SELECT命令中的表表达式通过组合表,视图,排除行,分组等操作构建了一个中间虚拟表。该虚拟表最终传递给select list进行处理。选择列表决定了实际输出中间表的哪些字段。

7.3.1. 选择列表中的项

最简单的选择列表是*,它表示输出表表达式中的所有字段。除此之外,选择列表由一个逗号分隔的值表达式列表组成(定义参见4.2)。例如,它可能是一个字段名组成的列表:

SELECT a, b, c FROM ...

字段名a,b和c既可以是FROM子句中的表的真实字段名,也可以是为它们指定的别名,参见7.2.1.2。选择列表中可用的名字范围与WHERE子句相同,除非使用了分组操作,这种情况下名字范围与HAVING子句相同。

如果多个表都包含同名的字段,必须给出表名,例如:

SELECT tbl1.a, tbl2.a, tbl1.b FROM ...

当使用多个表时,表名还可以用于指定某个表的所有字段:

SELECT tbl1.*, tbl2.a FROM ...

(参见7.2.2。)

如果在选择列表中使用了任何表达式,将会为返回的结果表添加一个概念上的虚拟字段。该值表达式对于每个结果行计算一次,使用行的值替换字段引用。但是选择列表中的表达式不一定需要引用表表达式中的任何字段;比如,它们可以是常量算数表达式。

7.3.2. 字段标签

可以为选择列表中的项指定一个名称,用于后续的处理,例如在ORDER BY子句中使用,或者客户端应用显示。例如:

SELECT a AS value, b + c AS sum FROM ...

如果没有使用AS为输出字段指定名称,系统将会指定一个默认的字段名。对于简单的字段引用,该名称就是被引用字段的名称。对于函数调用,该名称就是函数的名称。对于复杂表达式,系统将会产生一个通用的名称。

关键字AS可选,但是只有当新的字段名不是任何PostgreSQL关键字(参考附录C)的时候才可以省略。为了避免意外的冲突,可以为字段名加上双引号。例如,VALUE是一个关键字,因此以下写法是错误的:

SELECT a value, b + c AS sum FROM ...

但是以下写法没有问题:

SELECT a "value", b + c AS sum FROM ...

为了防止与将来添加的关键字冲突,推荐总是使用AS或者为输出字段名加上双引号。

注释:这里的输出字段命名与FROM子句中的命名不同(参考7.2.1.2)。可以将相同的字段重命名两次,但是输出的是查询列表中指定的名称。

7.3.3. DISTINCT

在处理完选择列表之后,可以选择排除结果表中的重复行。关键字DISTINCT直接写在SELECT之后,用于排除重复行:

SELECT DISTINCT select_list ...

(除了DISTINCT之外,关键字ALL可以用于指定返回所有行,这是默认方式。)

显然,如果两个行至少存在一个不同的字段值,将会被认为是不同的。这种比较中空值被认为是相同的。

或者,可以使用表达式决定行是否相同:

SELECT DISTINCT ON (expression [, expression ...]) select_list ...

其中,expression是任意的值表达式,对于所有行都进行一次计算。所有表达式结果都相等的行被认为是重复的行,并且只会输出第一个重复的行。注意“第一行”是不确定的,除非查询在足够多的字段上进行了排序,能够确保DISTINCT处理时行的唯一顺序。(DISTINCT ON在ORDER BY排序之后进行处理。)

DISTINCT ON不属于标准SQL,并且有时候被认为是一个不好的风格,因为它的结果存在不确定性。如果能够合理使用GROUP BY和FROM中的子查询,可以避免使用这种方式,不过通常它却是最方便的方式。

7.4. 组合查询

两个查询的结果可以通过集合运算中的并,交,差进行合并。该语法如下:

query1 UNION [ALL] query2

query1 INTERSECT [ALL] query2

query1 EXCEPT [ALL] query2

query1和query2是能够使用当前已讨论的任何特性的查询。集合运算也可以嵌套和链接,例如:

query1 UNION query2 UNION query3

该操作将会按照以下方式执行:

(query1 UNION query2) UNION query3

UNION实际上是将query2的结果添加到query1的结果之后(但是不能保证实际返回结果按照这种顺序)。此外,将会从结果中排除重复的行,这种方式与DISTINCT一样,除非使用UNION ALL。

INTERSECT返回同时存在query1和query2的结果中的行。除非使用INTERSECT ALL,将会从结果中排除重复的行。

EXCEPT返回存在query1的结果中,但是不存在query2的结果中的行。(有时候被称为两个查询之间的差。)通用,如果不使用EXCEPT ALL,将会排除重复的行。

为了计算两个查询的并,交或差,这两个查询必须是“兼容的”,这意味这它们返回相同数量的字段,并且相应字段具有兼容的数据类型,参考10.5。

7.5. 结果排序

当查询产生一个输出表之后(查询列表处理完成之后),可以选择将结果进行排序。如果不进行排序,将会以未指定的顺序返回结果行。这种情况下的实际顺序取决于扫描顺序,连接计划类型,以及数据在磁盘上的顺序,但是这个顺序不可靠。只有明确指定了排序步骤才能确保特定的输出顺序。

ORDER BY子句用于指定排序顺序:

SELECT select_list

FROM table_expression

ORDER BY sort_expression1 [ASC | DESC] [NULLS { FIRST | LAST }]

[, sort_expression2 [ASC | DESC] [NULLS { FIRST | LAST }] ...]

排序表达式可以是在查询列表中有效的任何表达式。例如:

SELECT a, b FROM table1 ORDER BY a + b, c;

如果指定了多个表达式,后面的值用于对那些前面的值相等的行进行排序。每个表达式都可以带一个可选的ASC或DESC关键字,用于表示升序排序或者降序排序。ASC排序是默认方式。升序排序将更小的值排在前面,其中“更小”是由<运算符定义的。与此类似,降序排序是由>运算符决定的。

注释:实际上,PostgreSQL使用默认的B-tree运算符类确定表达式数据类型的ASC和DESC排序顺序。通常,数据类型创建时会使得<和>运算符符合这种排序顺序,但是用户定义数据类型时可选选择其他方式。

NULLS FIRST和NULLS LAST选项可以用于确定空值在排序时位于非空值之前还是之后。默认情况下,空值被认为大于任何非空值;也是就说,对于DESC顺序,默认为NULLS FIRST,否则默认为NULLS LAST。

注意排序选项对于每个排序字段都是独立的。例如ORDER BY x, y DESC意味着ORDER BY x ASC ,y DESC,它与ORDER BY x DESC ,y DESC 不同。

sort_expression也可以是字段标签或输出字段的编号,例如:

SELECT a + b AS sum, c FROM table1 ORDER BY sum;

SELECT a, max(b) FROM table1 GROUP BY a ORDER BY 1;

这两个都是按照第一个输出字段进行排序。注意排序的输出字段名称必须是独立的,也就是说,不能是表达式的一部分。例如,以下写法是不正确的:

SELECT a + b AS sum, c FROM table1 ORDER BY sum + c; -- wrong

这个限制是为了减少歧义。如果ORDER BY中的某一项能够同时匹配一个输出字段名和表表达式的一个字段名,仍然会存在歧义。这种情况下将会使用输出字段。只有当你使用AS将某个输出字段重命名为与其他表的字段名重复的名称时,才会引起混淆。

ORDER BY可以用于UNION,INTERSECT或者EXCEPT组合的结果,但是这种情况下只允许按照输出字段名或者编号进行排序,而不能使用表达式。

7.6. LIMIT和OFFSET

LIMIT和OFFSET允许只获取查询结果剩余的一部分行:

SELECT select_list

FROM table_expression

[ ORDER BY ... ]

[ LIMIT { number | ALL } ] [ OFFSET number ]

如果指定了一个限定数量,最多返回那么多的行(但是如果查询自身产生的行数更少,返回行也会更少)。LIMIT ALL与忽略LIMIT子句结果相同。

OFFSET表示在开始返回行之前跳过那么多行。OFFSET 0与忽略OFFSET子句结果相同,LIMIT NULL与忽略LIMIT子句结果相同。如果同时使用OFFSET和LIMIT,那么在计算返回LIMIT行之前将会跳过OFFSET行。

如果使用LIMIT,重要的是使用ORDER BY子句将结果行按照唯一顺序进行排序。否则,将会获得一个不确定的结果行的子集。你可能想要获取第十行到第二十行,但是按照什么排序呢?顺序是未知的,除非指定了ORDER BY。

查询优化器在产生查询计划时会考虑LIMIT,所以对于不同的LIMIT和OFFSET,很有可能得到不同的计划(产生不同的行排序)。因此,使用不同的LIMIT/OFFSET值选择查询结果的不同子集将会得到不一致的结果,除非使用ORDER BY强制一个可以预见的排序结果。这不是一个缺陷;这是因为SQL不承诺以任何特定顺序返回查询结果,除非使用ORDER BY强制输出顺序。

OFFSET子句跳过的行仍然需要在服务器内部进行计算;因此一个大的OFFSET可能效率不高。

7.7. VALUES列表

VALUES提供了一个产生可以用于查询的“常量表”的方法,不需要实际在磁盘上创建和填出一个表。该语法如下:

VALUES ( expression [, ...] ) [, ...]

每对括号中的表达式列表将会产生表的一个行。这些列表必须具有相同数量的元素(也是就说表的字段数量),并且相应的项必须具有兼容的数据类型。结果中的每个字段的实际数据类型使用与UNION相同的规则确定(参见10.5)。

例如:

VALUES (1, 'one'), (2, 'two'), (3, 'three');

将会返回一个2列3行的表。它实际上等价于:

SELECT 1 AS column1, 'one' AS column2

UNION ALL

SELECT 2, 'two'

UNION ALL

SELECT 3, 'three';

默认情况下,PostgreSQL将会为VALUES表的字段指定名字column1,column2等等。SQL标准没有指定这些字段的名称,不同的数据库系统实现方式不同,因此通常最好使用一个表的别名列表覆盖默认的名称,比如:

=> SELECT * FROM (VALUES (1, 'one'), (2, 'two'), (3, 'three')) AS t (num,letter);

num | letter

-----+--------

1 | one

2 | two

3 | three

(3 rows)

从语法上讲,带表达式列表的VALUES与以下语句等价:

SELECT select_list FROM table_expression

并且可以出现在任何SELECT使用的地方。例如,你可以将它作为UNION的一部分,或者给它添加一个sort_specification(ORDER BY,LIMIT和/或OFFSET)。VALUES最常见的用法是作为INSERT命令的数据源,其次就是作为一个子查询。

更多信息可以参考VALUES参考页面。

7.8. WITH查询(公用表表达式)

WITH提供了一个在更大的查询中编写辅助查询语句的方法。这些语句,通常被称为公用表表达式或CTE,可以被看作只存在于某个查询中的临时表。WITH子句中的每个辅助语句可以是SELECT,INSERT,UPDATE或者DELETE;WITH子句所属的主语句也可以是SELECT,INSERT,UPDATE或者DELETE。

7.8.1. WITH中的查询语句

WITH中的SELECT主要用于复杂查询的分解。例如:

WITH regional_sales AS (

SELECT region, SUM(amount) AS total_sales

FROM orders

GROUP BY region

), top_regions AS (

SELECT region

FROM regional_sales

WHERE total_sales > (SELECT SUM(total_sales)/10 FROM regional_sales)

)

SELECT region,

product,

SUM(quantity) AS product_units,

SUM(amount) AS product_sales

FROM orders

WHERE region IN (SELECT region FROM top_regions)

GROUP BY region, product;

以上查询只显示销量最好的那些区域中每个产品的总销售额。WITH子句定义了两个辅助语句:regional_sales和top_regions,其中regional_sales的输出用在了top_regions中,而top_regions的输出用在了主查询语句中。这个示例也可以不使用WITH,但是需要两层嵌套的子查询。这种WITH方式更容易理解。

可选的RECURSIVE修饰符使得WITH不仅仅是语法简单,同时还提供了标准SQL无法完成的一个功能。通过RECURSIVE,WITH查询可以引用它自己的输出。最简单的示例就是查询整数1到100的和:

WITH RECURSIVE t(n) AS (

VALUES (1)

UNION ALL

SELECT n+1 FROM t WHERE n < 100

)

SELECT sum(n) FROM t;

递归WITH查询的一般格式总是包含一个非递归项,然后是UNION(或UNION ALL),然后是一个递归项,其中只有递归项能够包含查询自身输出的引用。这种查询的执行过程如下:

递归查询求值

1. 计算非递归项。对于UNION(而非UNION ALL),去除重复的行。将剩余的行放入递归查询的结果,同时将它们放入一个临时的工作表。

2. 只要工作表非空,重复以下步骤:

a. 计算递归项,使用工作表中的当前内容替换递归中的自引用。对于UNION(而非UNION ALL),去除重复的行以及与前面的结果重复的行。将所有剩余的行放入递归查询的结果中,同时将它们放入一个临时的中间表中。

b. 使用中间表中的内容替换工作表中的内容,然后清空中间表。

注释:严格地说,这个过程是迭代而不是递归,但是RECURSIVE是SQL标准委员会选定的术语。

在以上示例中,工作表在每一步中只包含单行结果,并且它的值从1逐步上升到100。在第100步中,由于WHERE子句的限制,输出为空,因此查询结束。

递归查询通常用于处理分层或树状结构的数据。这种查询的一个很好的示例就是查找某个产品的所有直属子部分和间接子部分,直接给出一个显示所有包含内容的表:

WITH RECURSIVE included_parts(sub_part, part, quantity) AS (

SELECT sub_part, part, quantity FROM parts WHERE part = 'our_product'

UNION ALL

SELECT p.sub_part, p.part, p.quantity

FROM included_parts pr, parts p

WHERE p.part = pr.sub_part

)

SELECT sub_part, SUM(quantity) as total_quantity

FROM included_parts

GROUP BY sub_part

使用递归查询时,重点是要确保查询的递归部分最终会返回空结果,否则查询将会进入无限循环。有时候,使用UNION而不是UNION ALL可以通过排除与前面结果重复的输出行达到返回空行的结果。但是,通常一个循环的输出行不会完全重复:有可能需要检查某个或者一些字段,判断是否曾经到达过相同的点。处理这种问题的标准方法就是计算出一个已经访问过的值的数组。例如,考虑一下使用link字段搜索graph表的查询:

WITH RECURSIVE search_graph(id, link, data, depth) AS (

SELECT g.id, g.link, g.data, 1

FROM graph g

UNION ALL

SELECT g.id, g.link, g.data, sg.depth + 1

FROM graph g, search_graph sg

WHERE g.id = sg.link

)

SELECT * FROM search_graph;

如果link关系包含闭环,该查询将会陷入循环。因为我们需要一个“深度”输出,将UNION ALL改为UNION不能消除循环。相反,我们需要辨别出沿着某个特定链路查找时是否再次到达相同的行。我们为该循环查询添加两个字段:path和cycle。

WITH RECURSIVE search_graph(id, link, data, depth, path, cycle) AS (

SELECT g.id, g.link, g.data, 1,

ARRAY[g.id],

false

FROM graph g

UNION ALL

SELECT g.id, g.link, g.data, sg.depth + 1,

path || g.id,

g.id = ANY(path)

FROM graph g, search_graph sg

WHERE g.id = sg.link AND NOT cycle

)

SELECT * FROM search_graph;

除了防止循环之外,数组的值通常还可以用于表示到达任意特定行的“路径”。

通常情况下,如果识别一个循环需要检查多个字段时,使用一个行数组。例如,假设我们需要比较字段f1和f2:

WITH RECURSIVE search_graph(id, link, data, depth, path, cycle) AS (

SELECT g.id, g.link, g.data, 1,

ARRAY[ROW(g.f1, g.f2)],

false

FROM graph g

UNION ALL

SELECT g.id, g.link, g.data, sg.depth + 1,

path || ROW(g.f1, g.f2),

ROW(g.f1, g.f2) = ANY(path)

FROM graph g, search_graph sg

WHERE g.id = sg.link AND NOT cycle

)

SELECT * FROM search_graph;

提示:对于识别循环只需要检查一个字段的情况,可以省略ROW( )。这样允许使用一个简单的数组而不是一个复合类型的数组,效率更高。

提示:递归查询求值算法使用广度优先搜索的顺序产生输出结果。你可以通过将外部查询按照某个“路径”字段进行ORDER BY,使得结果按照深度优先搜索的顺序显示。

当你不确定查询是否会陷入循环时,一个有用的测试技巧就是在父查询中添加一个LIMIT。例如,以下查询如果没有LIMIT,将会无限循环:

WITH RECURSIVE t(n) AS (

SELECT 1

UNION ALL

SELECT n+1 FROM t

)

SELECT n FROM t LIMIT 100;

这个语句能够正常工作,因为PostgreSQL的实现方式是WITH查询只计算父查询最终需要返回的行数。不推荐在生产环境中使用这种方法,因为这些系统的工作方式有可能不同。另外,如果外部查询将递归查询的结果进行了排序,或者将它们与其他表进行连接,这种查询也不能正常工作,因为这种情况下外部查询将会尝试获取WITH查询的全部结果。

WITH查询的一个有用的属性就是父查询每次执行时它只计算一次,即使在它在父查询或者兄弟WITH查询中被多次引用。需要在多个地方使用的耗时的计算可以放入一个WITH查询中,避免重复的工作。另一个用法是防止多次计算存在副作用的函数。但是,不好的一方面就是相比较普通的子查询而言,优化器能够从父查询施加到WITH查询中的限制更少。WITH查询将会按照编写内容进行计算,而不能排除那些父查询随后将丢弃的行。(但是,如上所述,如果对该查询的引用只要求有限的行数,WITH计算可能提前停止。)

以上示例只演示了WITH和SELECT一起使用,但是它可以与INSERT,UPDATE或DELETE以相同的方式一起使用。每种情况下它都可以提供在主命令中进行引用的临时表。

7.8.2. WITH中的数据修改语句

你可以在WITH中使用数据修改语句(INSERT、UPDATE或DELETE)。这样你就可在同一个查询中执行多个不同的操作。例如:

WITH moved_rows AS (

DELETE FROM products

WHERE

"date" >= '2010-10-01' AND

"date" < '2010-11-01'

RETURNING *

)

INSERT INTO products_log

SELECT * FROM moved_rows;

这个查询实际上是将数据从products移到了products_log。WITH中的DELETE删除了products中指定的行,通过RETURNING子句返回了被删除的内容;然后主查询读取了该输出并将它插入products_log中。

以上示例的一个细微之处在于WITH子句属于INSERT,而不是INSERT中的子查询。这样是因为数据修改语句只允许出现相似属于顶层语句的WITH子句中。但是,同样会应用常规的WITH可见性规则,因此可以在子查询中引用WITH语句的输出结果。

WITH中的数据修改语句通常包含RETURNING子句,如上例所示。RETURNING子句的输出,而不是数据修改语句的目标表,将会形成可以在查询其他部分进行引用的临时表。如果WITH中的数据修改语句没有RETURNING子句,就不会形成临时表,并且不能在其他部分进行引用。不过这样的语句也会被执行。以下是一个没有特殊用途的示例:

WITH t AS (

DELETE FROM foo

)

DELETE FROM bar;

该示例将会删除foo和bar中的所以数据。返回给客户端的受影响的行数只包含bar中删除的行。

在数据修改语句中不允许使用递归自引用。有时候可以通过引用递归WITH的输出绕过这个限制,例如:

WITH RECURSIVE included_parts(sub_part, part) AS (

SELECT sub_part, part FROM parts WHERE part = 'our_product'

UNION ALL

SELECT p.sub_part, p.part

FROM included_parts pr, parts p

WHERE p.part = pr.sub_part

)

DELETE FROM parts

WHERE part IN (SELECT part FROM included_parts);

该查询将会删除某个产品的所以直接子部分和间接子部分。

WITH中的数据修改语句只会执行一次,并且总是能够完全,无论主查询是否会读取所有的输出(或者任何输出)。注意这个WITH中的SELECT规则不同:如上一节所述,SELECT只会根据主查询的需要执行。

WITH中的子语句之间以及主查询是并发执行的。因此,使用WITH中的数据修改语句时,各个更新语句的执行顺序是未知的。所有的语句都是使用相同的快照执行的(参见第13章),所以它们无法在目标表中“看到”相互之间的结果。这样可以减少未知更新顺序带来的影响,并且意味着RETURNING数据是不同WITH子语句之间以及主查询传递变更的唯一途径。这种用法的示例如下:

WITH t AS (

UPDATE products SET price = price * 1.05

RETURNING *

)

SELECT * FROM products;

外部的SELECT将会返回UPDATE操作之前的原始价格,但是对于

WITH t AS (

UPDATE products SET price = price * 1.05

RETURNING *

)

SELECT * FROM t;

外部的SELECT将会返回更新后的数据。

在单个语句中无法更新相同的数据行两次。只有其中的一个更新会执行,但是很难准确判断执行了哪个更新(有时候甚至无法判断)。这同样适用于删除在同一语句中已经被更新的行:只有更新操作会被执行。因此,通常应该避免在一个语句中修改数据行两次。尤其需要避免编写可能影响已经被主语句或兄弟语句修改过的数据行的WITH子语句。这种语句的结果无法预料。

当前,任何用作WITH中的数据修改语句的目标表不能包含扩展到多个语句的条件规则,ALSO规则或INSTEAD规则。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: