3.1. 介绍
在前面的章节中,我们介绍了使用 SQL 保存和访问在 PostgreSQL 里的数据的基本方法。 我们现在将讨论一些 SQL 更高级的特性,这些特性可以简化管理和避免你的数据的丢失或损坏。 最后,我们将看看一些PostgreSQL 的扩展。
本章将不时引用在 Chapter 2 里建立的例子, 并且对它们进行修改和提高,因此如果你已经看过那章会更好。 本章的一些例子也可以在教程目录里的 advanced.sql 文件里找到。 这个文件还包括一些要装载的例子数据,这些数据在这里没有介绍。 (请参考 Section 2.1 获取如何使用该文件的方法。)
CREATE VIEW myview AS SELECT city, temp_lo, temp_hi, prcp, date, location FROM weather, cities WHERE city = name; SELECT * FROM myview;
3.2.视图
回头看看在 Section 2.6 里的查询例子。 假设你的应用对天气记录和城市位置的组合列表特别感兴趣, 而你又不想每次键入这些查询。那么你可以在这个查询上创建一个视图, 它给这个查询一个名字,你可以像普通表那样引用它。
CREATE VIEW myview AS SELECT city, temp_lo, temp_hi, prcp, date, location FROM weather, cities WHERE city = name; SELECT * FROM myview;
自由地运用视图是好的 SQL 数据库设计的一个关键要素。 视图允许我们把表结构的细节封装起来,尽管这些表可能因你的应用的进化而变化,而这些变化却可以躲在一致的接口后面。
视图几乎可以在一个真正的表可以使用的任何地方使用。 在其它视图上面再建造视图也并非罕见。
3.3. 外键
回忆一下 Chapter 2 里的 weather 和 cities 表。思考下面的问题:你想确保没有人可以在 weather 表里插入一条在 cities 表里没有匹配记录的数据行。 这就叫维护你的表的参考完整性。 在简单的数据库系统里,实现(如果也叫实现)这个特性的方法 通常是先看看 cities 表里是否有匹配的记录, 然后插入或者拒绝新的 weather 记录。 这个方法有许多问题,而且非常不便,因此 PostgreSQL 可以为你做这些。
新的表声明看起来会象下面这样:
CREATE TABLE cities ( city varchar(80) primary key, location point );
CREATE TABLE weather ( city varchar(80) references cities(city), temp_lo int, temp_hi int, prcp real, date date );
然后我们试图插入一条非法的记录:
INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28'); ERROR: insert or update on table "weather" violates foreign key constraint "weather_city_fkey" DETAIL: Key (city)=(Berkeley) is not present in table "cities".
外键的使用可以改进你的应用。在这份教程里我们就不再多说这个简单的例子了,而是请你参考Chapter 5获取更多的信息。 正确使用外键无疑将改进你的数据库应用,所以我们强烈建议你学习它们。
3.4.事务
事务是所有数据库系统的一个基本概念。 一次事务的要点就是它把多个步骤捆绑成了一个单一的,原子的操作。 其它并发的事务是看不到在这些步骤之间的中间状态的,并且如果其中一些操作失败, 导致该事务无法完成,那么所有这些步骤都完全不会影响数据库。
例如,假设一个银行的数据库包含各种客户帐户的余额,以及每个分行的总余额。 假设我们要记录一次从 Alice 的帐户到 Bob 的帐户的金额为 $100.00 的支付动作。那么,完成这个任务的简单到极点的 SQL 命令象下面这样
UPDATE accounts SET balance = balance - 100.00 WHERE name = 'Alice'; UPDATE branches SET balance = balance - 100.00 WHERE name = (SELECT branch_name FROM accounts WHERE name = 'Alice'); UPDATE accounts SET balance = balance + 100.00 WHERE name = 'Bob'; UPDATE branches SET balance = balance + 100.00 WHERE name = (SELECT branch_name FROM accounts WHERE name = 'Bob');
这些命令的细节在这儿并不重要;重要的是这里牵涉到了好几个独立的更新来完成这个相当简单的操作。 我们的银行官员会希望要么所有这些更新都生效,要么全部不起作用。 我们当然不希望一次系统崩溃就导致 Bob 收到 100 块不是 Alice 支付的钱, 也不希望 Alice 老是不花钱从 Bob 那里拿到物品。我们需要保证:如果在操作的过程中出了差错, 那么所有这些步骤都不会发生效果。把这些更新组合成一个事务就给予我们这样的保证。 事务被认为是原子的:从其它事务的角度来看,它要么是全部发生,要么完全不发生。
我们还需要保证:一旦一个事务完成并且得到数据库系统的认可, 那么它必须被真正永久地存储,并且不会在随后的崩溃中消失。 比如,如果我们记录到了一个 Bob 撤单的动作, 那么我们不希望仅仅在他走出银行大门之后的一次崩溃就会导致对他的帐户的扣减动作消失。 一个事务型数据库保证一个事务所做的所有更新在事务发出完成响应之前都记录到永久的存储中(也就是磁盘)。
事务型数据库的另外一个重要的性质和原子更新的概念关系密切: 当多个事务并发地运行的时候,那么每个事务都不应看到其它事务所做的未完成的变化。 比如,如果一个事务正忙着计算所有分行的余额总和, 那么它不应该包括来自 Alice 的分行的扣帐和来自 Bob 分行的入帐,反之亦然。 所以事务必须是黑白分明的,不仅仅体现在它们在数据库上产生的永久影响出发,而且体现在它们运转时的自身的可视性上。 一个打开的事务做的更新在它完成之前是其它事务无法看到的,而到提交的时候所有更新同时可见。
在 PostgreSQL 里,一个事务是通过把 SQL 命令用 BEGIN 和 COMMIT 命令包围实现的。 因此我们的银行事务实际上看起来象下面这样
BEGIN; UPDATE accounts SET balance = balance - 100.00 WHERE name = 'Alice'; -- 等等 COMMIT;
如果在该事务的过程中,我们决定不做提交(可能是我们刚发现 Alice 的余额是负数), 那么我们可以发出 ROLLBACK 命令而不是 COMMIT 命令,那么到目前为止我们的所有更新都会被取消。
PostgreSQL 实际上把每个 SQL 语句当做在一个被执行的事务来看待。 如果你没有发出 BEGIN 命令,那么每个独立的语句都有一个隐含的 BEGIN 和(如果成功的话) COMMIT 语句包围在周围。 一组包围在 BEGIN 和 COMMIT 语句中间的语句有时候被称做事务块。
注意: 一些客户库自动发出 BEGIN 和 COMMIT, 因此你可能不需要特意请求就可以获取事务块的效果。查看你使用的接口的文档。
我们可以使用保存点的方法,在一个事务里更加精细地控制其中的语句。 保存点允许你有选择性地抛弃事务中的某些部分,而提交其它剩下的。 在用 SAVEPOINT 定义了一个保存点后,如果需要,你可以使用 ROLLBACK TO 回滚到该保存点。 则该事务在定义保存点到回滚到它之间的所有数据库更改都被抛弃,但是在保存点之前的修改将被保留。
在回滚到一个保存点之后,这个保存点仍然保存着其定义,所以你可以回滚到这个位置好几次。 当然,如果你确信你不需要再次回滚到一个保存点,那么你可以释放它,这样系统可以释放一些资源。 要记住:释放或者回滚到一个保存点都会自动释放在其后定义的所有保存点。
所有这些都发生在一个事务块内部,所以所有这些都不可能被其它事务会话看到。 当且仅当你提交了这个事务块,这些提交了的动作才能以一个单元的方式被其它会话看到, 而回滚的动作完全不会再被看到。
还记得我们的银行数据库吗?假设我们从 Alice 的帐户上消费 $100.00, 然后给 Bob 的帐户进行贷记加款,稍后我们发现我们应该给 Wally 的账号贷记加款。 那么我们可以像下面这样的保存点来做:
BEGIN; UPDATE accounts SET balance = balance - 100.00 WHERE name = 'Alice'; SAVEPOINT my_savepoint; UPDATE accounts SET balance = balance + 100.00 WHERE name = 'Bob'; -- 呀!加错钱了,应该用 Wally 的账号 ROLLBACK TO my_savepoint; UPDATE accounts SET balance = balance + 100.00 WHERE name = 'Wally'; COMMIT;
这个例子当然是实在太简单了,但是通过使用保存点,我们可以对事务块有大量的控制。 并且,ROLLBACK TO 是除了事务全部回滚,重新来过之外的唯一可用的, 用于重新控制一个因错误而被系统置于退出状态下的事务的方法。
3.5.Window 函数
一个Window 函数执行多个与表主题相关的当前行的行集计算。是可与聚合函数相媲美的计算类型。但又不同于普通的聚合函数,一个Window 函数的使用不会导致行分组为单一的输出行-行会保留各自的特性。在幕后,Window 函数的功能是不仅仅是获得查询结果的当前行了。
下面是来说明如何比较用他或她的部门的平均工资来比较每个员工的工资:
SELECT depname, empno, salary, avg(salary) OVER (PARTITION BY depname) FROM empsalary; depname | empno | salary | avg -----------+-------+--------+----------------------- develop | 11 | 5200 | 5020.0000000000000000 develop | 7 | 4200 | 5020.0000000000000000 develop | 9 | 4500 | 5020.0000000000000000 develop | 8 | 6000 | 5020.0000000000000000 develop | 10 | 5200 | 5020.0000000000000000 personnel | 5 | 3500 | 3700.0000000000000000 personnel | 2 | 3900 | 3700.0000000000000000 sales | 3 | 4800 | 4866.6666666666666667 sales | 1 | 5000 | 4866.6666666666666667 sales | 4 | 4800 | 4866.6666666666666667 (10 rows)
前三个输出列直接来自表empsalary,并且对于表中的每一行都有一个输出行。第四列表示在所有的表行中已为当前具有相同depname的行值采取了平均水平。(这实际上是与正常平均聚合函数相同的功能,但它的OVER子句被视为一个window 函数,计算一组适当的行值。)
一个Window 函数调用总是在函数名以及参数之后还包含一个over字句,这就在语法上区别了常规的函数或聚合函数。over字句决定Window 函数如何对查询行进行拆分处理。OVER中的PARTITION BY 的值指定如何将行分成组或者是区,组或区中的数据在PARTITION BY 的表达式中具有相同的值。对于每一行,Window 函数将那些分在同一区内的行作为当前行来计算。
你还可以控制是那些被Window 函数用OVER里面的ORDER BY命令进行处理过的行的顺序,(Window 函数的ORDER BY甚至无需与输出行的顺序一致。)这里有个例子:
SELECT depname, empno, salary, rank() OVER (PARTITION BY depname ORDER BY salary DESC) FROM empsalary; depname | empno | salary | rank -----------+-------+--------+------ develop | 8 | 6000 | 1 develop | 10 | 5200 | 2 develop | 11 | 5200 | 2 develop | 9 | 4500 | 4 develop | 7 | 4200 | 5 personnel | 2 | 3900 | 1 personnel | 5 | 3500 | 2 sales | 1 | 5000 | 1 sales | 4 | 4800 | 2 sales | 3 | 4800 | 2 (10 rows)
如上所示,RANK函数在当前行的分区内为每个不同的ORDER BY的值都生成了一个数值rank,由ORDER BY子句定义顺序。rank函数没有明确的参数,因为它的行为是完全由OVER子句决定。
那些由Window 函数生成的行都是根据查询语句的FROM子句里面的WHERE,GROUP BY,HAVING或是别的子句来过滤的。例如,一个行被删除了是因为它没有符合WHERE条件,所以没有在Window 函数中显示出来。一个查询可以包含多个Window 函数,这样就可以通过不同的OVER子句将数据进行分割,但这些数据都是在虚拟表中定义的相同的行集合中。
我们已经看到ORDER BY可以省略,如果行的顺序不是很重要的话,它也可以忽略PARTITION BY,在这种情况下只有一个分区包含所有的行。
这里还有一个重要概念与Window 函数有关联:对于每一行,在它的分区内有一个称之为Window frame的行集,许多(不是全部)Window 函数都只是对window frame的行起作用,而不是对分区内的所有行都起作用,默认情况下,如果有ORDER BY那么window frame包括所有的行,从分区开始到当前行,还有那些等同于当前行的行,根据OREDR BY字句就可进行区分。如果ORDER BY都省略了那么默认frame就包括了分区内的所有行[1]这里有一个使用sum的例子。
SELECT salary, sum(salary) OVER () FROM empsalary; salary | sum --------+------- 5200 | 47100 5000 | 47100 3500 | 47100 4800 | 47100 3900 | 47100 4200 | 47100 4500 | 47100 4800 | 47100 6000 | 47100 5200 | 47100 (10 rows)
看上面,因为在OVER 子句中没有ORDER BY,window frame就与分区一样,缺少PARTITION BY的是整个表,换句话说,每个SUM都计算了整个表,所以我们每个输出行都得到了相同的接结果。但是如果我们加个ORDER BY子句,将会得到不同的结果:
SELECT salary, sum(salary) OVER (ORDER BY salary) FROM empsalary; salary | sum --------+------- 3500 | 3500 3900 | 7400 4200 | 11600 4500 | 16100 4800 | 25700 4800 | 25700 5000 | 30700 5200 | 41100 5200 | 41100 6000 | 47100 (10 rows)
这里的sum是计算从第一个(最低的)工资到当前工资,包括任何重复数据(注意重复工资的结果)
Window 函数只在SELECT列表和ORDER BY子句的查询中被允许,在别的地方都是被禁止的,例如在GROUP BY, HAVING 和 WHERE 子句中。这是因为他们在逻辑上都是在这些子句处理后执行,而且,Window 函数在常规聚合函数之后执行。这就意味着在一个Window 函数的参数中包含一个聚合函数是有效的,而不是刚好相反。
如果需要在Window计算执行后过滤或者对行进行分组,可以使用子选择,例如;
SELECT depname, empno, salary, enroll_date FROM (SELECT depname, empno, salary, enroll_date, rank() OVER (PARTITION BY depname ORDER BY salary DESC, empno) AS pos FROM empsalary ) AS ss WHERE pos < 3;
上面的查询只显示内查询中rank值小于3的行。
当一个查询包括多个Window 函数时,它可以为每个Window 函数写单独的OVER子句,但这是重复的,并且如果相同的窗口行为希望有不同的功能就会容易出错,相反,每个Window 的行为都可以在一个WINDOWS子句中命名,然后再OVER中引用,例如:
SELECT sum(salary) OVER w, avg(salary) OVER w FROM empsalary WINDOW w AS (PARTITION BY depname ORDER BY salary DESC);
更多的关于Window 函数的细节可以在章节4.2.8,章节9.19,章节7.2.4.还有SELECT相关的页面部分找到
Notes
[1] 有其他的选择定义window frame的方式,但本教程中不包括它们,有关详细信息,请看章节4.2.8
3.6.继承
继承是面向对象数据库中的一个概念。它开启了数据库设计新的有趣的可能性大门。
让我们创建两个表:一个表 cities 和一个表 capitals。自然,首府(capital)也是城市(cities), 因此在列出所有城市时你想要某种方法隐含地显示首府。 如果你已经很高明了,那么你可能会创造类似下面这样的模式:
CREATE TABLE capitals ( name text, population real, altitude int, -- (单位是英尺) state char(2) );
CREATE TABLE non_capitals ( name text, population real, altitude int -- (单位是英尺) );
CREATE VIEW cities AS SELECT name, population, altitude FROM capitals UNION SELECT name, population, altitude FROM non_capitals;
一个更好的方法是;
CREATE TABLE cities ( name text, population real, altitude int -- (in ft) );
CREATE TABLE capitals ( state char(2) ) INHERITS (cities);
在这个例子里,capitals 的一行继承所有来自它的父表cities 的所有字段(name, population,和 altitude)。 字段 name 的类型是 text, 是 PostgreSQL 用于变长字符串的固有类型。 州首府有一个额外的字段,州,显示所处的州。在 PostgreSQL 里,一个表可以从零个或者更多其它表中继承过来。
比如,下面的查询找出所有海拔超过 500 英尺的城市的名字, 包括州首府:
SELECT name, altitude FROM cities WHERE altitude > 500;
它返回:
name | altitude -----------+---------- Las Vegas | 2174 Mariposa | 1953 Madison | 845 (3 rows)
另外一方面,下面的查询找出所有不是州首府并且位于海拔大于或等于 500 英尺的城市:
SELECT name, altitude FROM ONLY cities WHERE altitude > 500; name | altitude -----------+---------- Las Vegas | 2174 Mariposa | 1953 (2 rows)
这里的 cities 前面的 ONLY 指示系统只对 cities 表运行查询,而不包括继承级别中低于 cities 的表。 许多我们已经讨论过的命令 — SELECT, UPDATE 和 DELETE — 支持这个 ONLY 表示法。
注意: 尽管继承经常是有用的,但是它还没有集成唯一约束或者外键,因此制约了其实用性。 参阅 Section 5.8 获取更多细节。
3.7.结论
PostgreSQL 有许多这份教程里没有谈到的特性, 因为这份教程主要是面向新 SQL 用户的。这些特性在本书剩余部分将有更详细的介绍。
如果你觉得自己需要更多介绍性材料,请访问 PostgreSQL网站 获取更多资源的联接。