开源数据库,第二部分:PostgreSQL
如果您正在开发一个依赖于存储和检索大量数据的应用程序,您无疑已经发现了关系数据库的用处。数据库已经存在一段时间了,但在很多年里,开源数据库产品不如它们的商业 counterparts。今天,我们有几个开源选项可供选择。上个月,我概述了 MySQL,它可能是最流行的开源客户端-服务器关系数据库。
本月,我们将关注 MySQL 最著名的竞争对手 PostgreSQL。(下个月,我计划比较这两个程序。)PostgreSQL 的社区比 MySQL 小,这有时导致人们忽略它,认为它不重要,甚至认为它不如 MySQL 强大。但是,PostgreSQL 包含了 MySQL 的几乎所有功能,并且它提供了 MySQL 当前不提供的许多功能。正如本文所示,在任何时候您需要数据库时,都值得考虑 PostgreSQL。
PostgreSQL 最初是 1985 年在加州大学伯克利分校的一个研究项目。计算机科学教授 Michael Stonebreaker 已经开发并发布了一个名为 Ingres 的数据库产品。在将 Ingres 商业化之后,他回到伯克利,设计了一个新的数据库(Postgres),部分目的是解决他之前在 Ingres 中看到的问题。尽管用户社区不断增长,Stonebreaker 还是在 1993 年关闭了 Postgres 项目。
然而,Postgres 是在 BSD 开源许可证下发布的,这意味着用户可以自由地修改和重新分发它。两位伯克利的学生,以及来自北美其他地方的一些人,决定看看他们可以多么容易地修改代码并分发一个可用的数据库。
这些开发者所做的一个改变是 Postgres 与外部世界通信的方式。最初,Postgres 使用了 Stonebreaker 为 Ingres 设计的 QUEL 查询语言。开发者移除了 QUEL,并添加了一个 SQL 解释器,以使其与其他产品更兼容。为了表明这个版本的 Postgres 使用了 SQL,数据库被重命名为 PostgreSQL。
今天,许多最初的开发者继续参与该项目,修复错误并贡献补丁。这项工作由一个志愿者指导委员会协调。个别 PostgreSQL 开发者可能会被各公司聘用(作为员工或承包商),但没有任何一家公司或组织控制 PostgreSQL 的整体开发或方向。
PostgreSQL 的最新版本是 8.2.3,于 2007 年 2 月初发布。这是 8.2 版本的错误修复版本,8.2 版本于 2006 年底发布。主要版本通常每年发布一次,另外还有针对安全和其他严重错误的次要版本。
安装 PostgreSQL 最简单的方法是使用软件包管理系统,例如 debs 或 RPMs。例如,在我的 Ubuntu 系统上,我能够使用 apt-get 轻松快速地安装 PostgreSQL。请注意,大多数软件包管理系统区分 PostgreSQL 客户端、服务器和开发者库,因此请务必检索最适合您需求的软件包。通过 apt-get 或 RPMs 安装 PostgreSQL 应该可以实现您入门所需的一切——从创建 postgres 用户到初始化数据目录。
如果您必须从源代码安装 PostgreSQL,我建议阅读随附的文档。编译 PostgreSQL 并不难,但这确实需要比我在本文中能提供的更多的描述。不过,对于任何从头开始编译它的人来说,一个建议是解压归档文件,并以 postgres 用户身份编译和测试它。尝试以 root 用户身份编译和测试 PostgreSQL 注定会失败,其他用户也可能没有足够的权限使其正常工作。
现在,我们准备启动服务器。预构建的软件包通常会包含一个 shell 脚本(放置在 /etc/init.d/ 或等效位置),该脚本为您启动服务器。即使您已经下载并安装了 PostgreSQL 的源代码,您也会在 contrib 目录中找到一个合适的启动脚本。我建议使用(或至少修改)这个脚本,而不是从头开始编写一个。在我的系统上,我可以使用以下命令启动 PostgreSQL:
/etc/init.d/postgresql start
如果一切顺利,我们的服务器现在应该正在运行。(我们可以通过输入ps aux | grep postgres在命令行中。)访问服务器最简单的方法是使用 psql 交互式客户端,它随 PostgreSQL 一起提供。要获取当前集群中数据库的列表,请使用以下语法:
psql -U <username> -l
其中 <username> 是在服务器上具有足够访问权限的 PostgreSQL 用户。-U 选项允许我们指示用户名,-l 选项要求服务器列出可用的数据库,例如:
List of databases Name | Owner | Encoding -----------+-----------+----------- testserver | reuven | SQL_ASCII postgres | postgres | SQL_ASCII template0 | postgres | SQL_ASCII template1 | postgres | SQL_ASCII (4 rows)
与许多数据库系统一样,PostgreSQL 维护着自己独立于底层操作系统的用户和组列表。要创建一个新用户,我们使用随 PostgreSQL 提供的 createuser 程序:
createuser -U postgres reuven
这会以 postgres(它有权创建其他用户)身份调用 createuser,然后创建一个名为 reuven 的新用户。如果我们使这个新用户成为超级用户,reuven 也将被允许创建新的数据库和角色。
现在,我们可以在此集群中创建一个新数据库:
createdb -U reuven linux
通过重用 psql -l 来再次检查数据库是否存在:
psql -U reuven -l
您可能已经注意到,在我们调用需要用户的命令时,我们没有给出任何密码。PostgreSQL 的默认设置使服务器通过本地套接字可用(因此在网络上不可用)。因为只有本地用户将被允许访问,所以我们允许来自任何已定义用户名的连接,即使没有密码。
我们可以更改此行为,以及其他安全和连接相关的行为,在位于集群目录顶部的 pg_hba.conf 文件中。该文件包含广泛的文档,并解释了如何设置连接参数。
让我们连接到我们的数据库,看看会发生什么:
$ psql -U reuven linux
果然,我们得到了 psql 提示符,邀请我们输入查询。可以通过键入以下命令获得 psql 命令列表:\?在提示符下。我们还可以使用 \h 获取 SQL 语法和命令的帮助,例如:
\h CREATE TABLE
果然,让我们从创建一个表开始:
CREATE TABLE People ( id SERIAL NOT NULL, first_name TEXT NOT NULL, last_name TEXT NOT NULL, email_address TEXT NOT NULL, added_at TIMESTAMP NOT NULL DEFAULT NOW(), PRIMARY KEY(id), UNIQUE(email_address) );
此表有五列和三种不同的数据类型。最常见的类型是 TEXT,这是存储文本数据的典型方式。PostgreSQL 完全支持 Unicode,TEXT 列可以包含非常长的字符串。(在 8.0 版本之前,PostgreSQL 可以在每行中存储有限的数据量,因此这对于某些人来说可能是新闻。)没有内置的索引 TEXT 列的支持,但是一个名为 tsearch2 的附加模块相对容易安装,并为任何需要它的人提供这种功能。
added_at 列的类型为 TIMESTAMP。由于 TIMESTAMP 和 INTERVAL 这两种基本数据类型之间的划分,PostgreSQL 提供了一套非常复杂的时间和日期处理例程。时间戳指示某事发生的时间,而间隔告诉您某事花费了多长时间。因此,我们可以减去两个时间戳(以获得一个间隔)或将一个间隔添加到时间戳(以获得一个新的时间戳)。此外,PostgreSQL 允许我们使用类似英语的语法定义间隔,例如:
SELECT id FROM People WHERE added_at > NOW () ↪- interval '30 days';
上面的查询显示了最近 30 天内添加到我们联系人数据库中的所有人。
added_at 列也定义了一个 DEFAULT 值。这意味着如果我们未能为 added_at 插入显式值,PostgreSQL 将使用当前时间(在插入时)。
id 列,我们将其定义为主键,使用 SERIAL 数据类型。问题是,SERIAL 根本不是一种数据类型。相反,它是语法糖,它做了几件事:
它创建一个新的序列对象,其值是从 1 开始的整数,并且每次我们请求一个值时都会递增。
它将列类型定义为 INTEGER。
它将我们列的 DEFAULT 子句设置为从序列请求新值的结果。
这听起来可能像是说“SERIAL 给我们一个自动递增列”的复杂方式。这在某种意义上是正确的,但是您可以在一个表中拥有任意多个 SERIAL 列,并且每个序列都可以具有与之关联的各种属性,包括其起点和增量。
最后,通过将 id 定义为主键,并将 email_address 定义为唯一键,我们隐式地要求 PostgreSQL 在这两列上创建索引。当我们执行上述查询时,PostgreSQL 会通知我们它在后台执行的操作:
NOTICE: CREATE TABLE will create implicit sequence "people_id_seq" for serial column "people.id" NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "people_pkey" for table "people" NOTICE: CREATE TABLE / UNIQUE will create implicit index "people_email_address_key" for table "people"
如果您删除 People 表,这些隐式定义的对象将自动删除。
要列出我们数据库中的所有表、序列和视图,我们可以使用 \d 命令:
linux=# \d List of relations Schema | Name | Type | Owner --------+---------------+----------+-------- public | people | table | reuven public | people_id_seq | sequence | reuven (2 rows)
我们可以向 \d 添加一个字母,以仅获取表 (t)、索引 (i)、序列 (s)、函数 (f) 或视图 (v) 的列表。例如,这是我们已创建的索引列表:
linux=# \di List of relations Schema | Name | Type | Owner | Table -------+--------------------------+-------+--------+------- public | people_email_address_key | index | reuven | people public | people_pkey | index | reuven | people (2 rows)
我们还可以使用 \d 更仔细地检查特定对象。例如,我们可以使用以下命令查看我们的 People 表:\d People:
linux=# \d People Table "public.people" Column | Type | Modifiers --------------+-----------------------------+---------- id | integer | not null default nextval('people_id_seq'::regclass) first_name | text | not null last_name | text | not null email_address | text | not null added_at | timestamp without time zone | not null default now() Indexes: "people_pkey" PRIMARY KEY, btree (id) "people_email_address_key" UNIQUE, btree (email_address)
在上面的输出中,有几件事需要注意:
首先,PostgreSQL 将表视为 public.people,而不仅仅是 people。这是因为每个对象都必须存在于模式或命名空间中,默认模式称为 public。我们可以使用模式来划分特定数据库中的命名空间或处理分区。这意味着我们不需要跨两个或更多数据库拆分数据,仅仅为了处理冲突的权限和名称。
表名以及所有列名都以小写字母显示。这是因为 PostgreSQL 试图尽可能地遵守 SQL 标准,并且该标准规定标识符应该是大小写不敏感的。如果您真的想要大小写敏感的名称(您可能不需要),请在标识符周围使用双引号。
我们的 id 列已按预期转换为整数列,其默认值取自序列。
我们的表定义存在一些问题。虽然我们已经有效地阻止了人们在我们的 TEXT 列中存储 NULL 值,但我们还没有做任何事情来阻止他们输入空字符串。此外,我们可能希望确保 email_address 列看起来至少像一个电子邮件地址。
我们可以通过向我们的列添加约束来做到这一点——检查正在插入或更新的值的小函数。如果新值不符合定义,PostgreSQL 将拒绝允许其插入。这是我们的表的新定义,其中定义了一些约束:
CREATE TABLE People ( id SERIAL NOT NULL, first_name TEXT NOT NULL CHECK (first_name <> ''), last_name TEXT NOT NULL CHECK (last_name <> ''), email_address TEXT NOT NULL CHECK (email_address ~* '.@.+\\\.'), added_at TIMESTAMP NOT NULL DEFAULT NOW(), PRIMARY KEY(id), UNIQUE(email_address) );
如果我们检查我们的表定义,它已经发生了一些变化,以包括约束:
linux=# \d people Table "public.people" Column | Type | Modifiers --------------+-----------------------------+---------------------- id | integer | not null default nextval('people_id_seq'::regclass) first_name | text | not null last_name | text | not null email_address | text | not null added_at | timestamp without time zone | not null default now() Indexes: "people_pkey" PRIMARY KEY, btree (id) "people_email_address_key" UNIQUE, btree (email_address) Check constraints: "people_email_address_check" CHECK (email_address ~* '.@.+\\.'::text) "people_first_name_check" CHECK (first_name <> ''::text) "people_last_name_check" CHECK (last_name <> ''::text)
让我们看看如果我们违反这些约束会发生什么:
linux=# insert into people (first_name , last_name, email_address) values ('', 'Lerner', 'reuven@lerner.co.il'); ERROR: new row for relation "people" violates check constraint "people_first_name_check" linux=# insert into people (first_name , last_name, email_address) values ('Reuven2', 'Lerner2', 'reuven'); ERROR: new row for relation "people" violates check constraint "people_email_address_check"
果然,我们的约束有助于确保我们的数据库井然有序。
最常见的约束类型是外键,其中一个表指向另一个表。例如:
CREATE TABLE Appointments ( person_id INTEGER NOT NULL REFERENCES People, starting_time TIMESTAMP NOT NULL, duration INTERVAL NOT NULL, notes TEXT NULL );
如果我们尝试创建一个引用不存在的人的预约,我们将被拒绝:
INSERT INTO Appointments (person_id, starting_time, duration, notes) VALUES (5000, '2007-Feb-12 13:00', interval '1 hour', 'Lunch'); ERROR: insert or update on table "appointments" violates foreign key constraint "appointments_person_id_fkey" DETAIL: Key (person_id)=(5000) is not present in table "people".
外键约束在另一个方向也很有帮助。如果您尝试删除外键指向的行,PostgreSQL 将拒绝该请求,指示您必须首先删除外键。您可以通过将 ON UPDATE 或 ON DELETE 修饰符设置为外键定义来调整这些约束的规则。
此功能列表只是冰山一角。这就是 PostgreSQL 的魅力所在——开箱即用,它简单易用,但您几乎总是可以使用自己的代码和数据重新定义和扩展现有功能。内置运算符,以及它们可以组合的灵活方式,以及使用您自己的函数和定义进一步增强它们的方式,构成了一个强大的组合。我不经常使用并集或交集,但我经常使用视图。
例如,我最喜欢的功能之一是几乎可以在任何您想要使用值的地方使用子查询。如果您有某人的电子邮件地址,您可以使用它通过单个命令将一行插入到 Appointments 中:
INSERT INTO Appointments (person_id, starting_time, duration, notes) VALUES ((SELECT id FROM People WHERE email_address = 'reuven@lerner.co.il'), '2007-Feb-12 13:00', interval '1 hour', 'Lunch');
如果现有的数据类型不够用,我们可以构建自己的数据类型。PostgreSQL 已经附带了许多现有的数据类型,包括几何形状、IP 地址,甚至 ISBN。
如果我们想要创建多个具有相似特征的表,我们可以利用 PostgreSQL 的面向对象特性。因此,我们可以有一个 People 表和一个 Managers 表,其中 Manager 的定义继承了 People 的特征并添加了自己的扩展。
您还可以使用各种不同的语言创建自己的服务器端函数——从 PostgreSQL 自己的 Pl/pgsql 到 Perl、Python、Tcl、Java、Ruby 和 R 统计语言的专门版本。这些函数可以返回单个值或整个表,并且可以在触发器中使用。您还可以使用这些函数来重写从表甚至视图中插入、更新和删除数据的规则。
但是,也许所有功能中最重要的是对事务的内置支持。事务是数据库编程的重要组成部分,因为它们允许我们将多个查询组合成一个要么全部执行,要么全部不执行的操作。事务的经典示例是将资金从一个银行帐户转移到另一个银行帐户;如果断电,您需要确保资金已转移,或者没有转移。当灯光重新亮起时,资金完全消失或出现在两个帐户中都是不可接受的。
PostgreSQL 的最新版本增强了其事务处理能力。您不仅可以提交或回滚事务,还可以在事务内定义保存点。如果出现问题,您可以回滚整个事务,或者仅仅转到上一个保存点。此外,PostgreSQL 现在支持两阶段提交,使得同步需要通信和协调的分布式进程成为可能。
如果出现任何问题,PostgreSQL 还通过预写式日志 (WAL) 提供 PITR(时间点恢复),确保即使在最关键的时刻断电,事务也将被提交或回滚,并且尽可能多的事务将被提交。
您可能已经注意到我根本没有提到锁定。这是因为,在大多数情况下,PostgreSQL 用户不必担心锁定。锁的缺乏是通过一个称为 MVCC(多版本并发控制)的系统来处理的,该系统只有一个缺点,即创建了许多未使用的和废弃的数据库行。在 PostgreSQL 中处理此问题的传统方法是定期 VACUUM 数据库,删除旧行并清理空间。最近的版本现在包含一个自动清理代理,减少甚至消除了定期 VACUUM 的需要。
最后,PostgreSQL 的最新版本包括对表空间的支持。这意味着您可以将表分布在不同的目录和文件系统中,而不是将所有内容都放在安装定义的目录下。这可以显著提高性能或可靠性,尤其是在大型数据库上。
不要将 PostgreSQL 视为一个强大的开源数据库。相反,将其视为一个强大的数据库,恰好以开源许可证发布。它具有丰富的功能,使其可扩展以适应大型系统和需求,但对于想要开始关系数据库世界之旅的初学者来说,它也很容易上手。
资源
PostgreSQL 的主要网站是 www.postgresql.org。该站点包含指向软件、文档、常见问题解答和大量邮件列表的链接。
我最喜欢的关于 PostgreSQL 的书简称为 PostgreSQL,第二版,由 Korry Douglas 编写,由 Sams 出版(ISBN 0672327562)。
PostgreSQL 社区邮件列表也是帮助和信息的宝贵来源。一位核心开发人员回答某人提出的问题或承认存在需要修复的错误并不罕见。
Reuven M. Lerner,一位长期的 Web/数据库顾问,是伊利诺伊州埃文斯顿西北大学学习科学专业的博士候选人。他目前与妻子和三个孩子住在伊利诺伊州斯考基。您可以在 altneuland.lerner.co.il 阅读他的博客。