在MySQL基础上速通PostgreSQL

前言

因为一些缘故,接触了一下对空间地理数据的分析工作。忙完以后,想了想决定写下这篇文章,方便日后如有需要,可以快速查阅,也供后人参考。

背景条件

在我的分析过程中,需要使用PostgreSQL,但我之前用MySQL/MariaDB比较多,稍微花了一点点时间适应。怕下次忘了,干脆写出来。

本文还假设你已经具备了最基本的SQL,尤其是MySQL知识。有了这些基础知识,我们的适应就会非常迅速。

PostgreSQL速通

我们会着重介绍几个概念。

数据库实例 - Cluster

和我们平时常说的计算集群(Computer Cluster)不同,也和SQL中的聚合索引(Clustered Index)不同,PostgreSQL中的Cluster,其实可以简单理解成一个数据库实例。

具体来说,同一套PostgreSQL的二进制文件,可以运行多个实例(Cluster),每个实例具有不同的数据目录,配置文件,监听不同的端口。

而如果想要创建一个Cluster,使用initdb命令是最简单的方法。其手册可以参考https://www.postgresql.org/docs/current/app-initdb.html

也可以参考这里https://www.postgresql.org/docs/current/creating-cluster.html

数据库初始化

大部分情况下,包括Windows安装包安装,以及Debian/Ubuntu APT源中的安装包安装,都会自动为我们创建好一个可以直接使用的Cluster,我们一般不需要手动创建一个Cluster。

不过,如果在Windows上使用Zip压缩包进行安装时,需要运行initdb.exe,指定一个数据目录,创建一个新的Cluster。毕竟没有程序自动地帮你做这件事。

用户与权限

在较新版本(>=8.1)的PostgreSQL中,用户和组都统称为角色(ROLE)。在我看来,区别用户和组的最大的区别,或许在于,是否给一个角色赋予登录权限。

在Debian/Ubuntu系统下,当从APT源安装PostgreSQL时,会在系统中自动创建一个postgres用户,这个用户可以通过Unix Socket访问到PostgreSQL默认Cluster中创建的同名超级用户。

1
2
3
4
5
root@MyPC:~# sudo -u postgres -i psql
psql (14.2 (Ubuntu 14.2-1ubuntu1))
Type "help" for help.

postgres=#

如果你需要创建其他角色,则需要具有CREATEROLE权限。

要创建一个可以登录的角色(下面我就简称用户啦),可以使用以下SQL语句:

1
CREATE ROLE alice LOGIN PASSWORD '12345';

这会创建一个名为alice的用户,赋予(通过网络)登录权限,密码为12345。我们可以运行psql命令登录到这一用户。

1
2
3
4
5
6
7
8
9
root@MyPC:~# psql -U alice -W postgres -h localhost
Password:
psql (14.2 (Ubuntu 14.2-1ubuntu1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=> \conninfo
You are connected to database "postgres" as user "alice" on host "localhost" (address "127.0.0.1") at port "5432".
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)

如果你希望赋予这个用户一些权限,例如CREATEDB, CREATEROLE, 甚至是SUPERUSER,都可以在上方指定。例如,我们可以在创建时指定Alice为Superuser

1
CREATE ROLE alice SUPERUSER LOGIN PASSWORD '12345';

查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@MyPC:~# psql -U alice -W postgres -h localhost
Password:
psql (14.2 (Ubuntu 14.2-1ubuntu1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=# \du+
List of roles
Role name | Attributes | Member of | Description
-----------+------------------------------------------------------------+-----------+-------------
alice | Superuser | {} |
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {} |

postgres=#

但使用SQL语句直接创建用户的记录,会将密码明文存储在数据库的历史查询中。我们觉得这样不好。

PostgreSQL提供了许多命令行工具,即便不用显式登录到psql,也可以管理数据库。createuser便是其中用于创建角色的工具。其文档可以参考https://www.postgresql.org/docs/current/sql-createrole.html

基于此,我们可以将上面的创建用户代码,替换成Shell指令: createuser -l -s -e -P alice

1
2
3
4
5
postgres@MyPC:~$ createuser -l -s -e -P alice
Enter password for new role:
Enter it again:
SELECT pg_catalog.set_config('search_path', '', false);
CREATE ROLE alice PASSWORD 'SCRAM-SHA-256$4096:91DfHk6BEMX4Ay7S8TsfQw==$2tGBIh4PC/ce4ThLajfvm9ZwwVzidWCorlsHuHBKhX8=:0HZpvT5kReucYmGGwKR6I1OLsaZo3aOjcSMlBI7vrfQ=' SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;

这一串命令中,

  • -l代表赋予LOGIN登录权限
  • -s代表赋予SUPERUSER超级用户身份
  • -e指示该命令将最终发送给PostgreSQL服务器的SQL语句打印出来(最后2行)
  • -P指示命令将提示输入该新用户的密码

可以看到,我们发送给数据库服务器的SQL命令,已经将密码哈希处理了。

当然如果你觉得这一串参数记忆复杂,也可以直接使用--interactive选项,回答程序提出的一系列问题,即可创建用户。

数据库 & 模式 & 数据表

这三个词分别对应着:

  • 数据库 - Database
  • 模式 - Schema
  • 数据表 - Table

数据表很好理解。但前两个常常带来争议。

MySQL的Schema

在MySQL中,我们可以在MySQL :: MySQL 8.0 Reference Manual :: MySQL Glossary中看到这一段话:

In MySQL, physically, a schema is synonymous with a database. You can substitute the keyword SCHEMA instead of DATABASE in MySQL SQL syntax, for example using CREATE SCHEMA instead of CREATE DATABASE.

Some other database products draw a distinction. For example, in the Oracle Database product, a schema represents only a part of a database: the tables and other objects owned by a single user.

简单来说,在MySQL中,Database 等价于 Schema.

PostgreSQL的Schema

但PostgreSQL不完全这么想。在PostgreSQL: Documentation: 14: 5.9. Schemas中,是这么说的:

A PostgreSQL database cluster contains one or more named databases. Roles and a few other object types are shared across the entire cluster. A client connection to the server can only access data in a single database, the one specified in the connection request.

A database contains one or more named schemas, which in turn contain tables. Schemas also contain other kinds of named objects, including data types, functions, and operators. The same object name can be used in different schemas without conflict; for example, both schema1 and myschema can contain tables named mytable. Unlike databases, schemas are not rigidly separated: a user can access objects in any of the schemas in the database they are connected to, if they have privileges to do so.

简单来说,在PostgreSQL中,一个实例(Cluster)可以包含多个数据库(Database),一个数据库(Database)可以包含多个模式(Schema),一个模式(Schema)下面有很多表(Table),数据就存储在表(Table)中。

PostgreSQL中的默认Schema

在PostgreSQL中创建一个数据库时,该数据库会默认包含一个public模式。对在该数据库上执行的所有SQL语句来说,如果没有显式指定模式名称,则都会被认为是对public的操作。

假设数据库中存在一个名为public的模式,其下有一个mytable表,则以下两个SQL语句是等效的。

1
SELECT * FROM mytable;

或者

1
SELECT * FROM public.mytable;

跨库查询

两个数据库在组织这些术语时的差异,就进一步导出了一个,非常明显,但仔细思考之下显得比较有道理的限制条件:MySQL允许跨数据库查询,而PostgreSQL不允许跨数据库查询。

更进一步,PostgreSQL虽然不允许跨数据库(Database)查询,但允许跨模式(Schema)查询。

或许是因为,PostgreSQL认为,不同数据库存储的数据,涉及到的业务内容相互间应该是没有紧密关联的(如果有紧密联系,就应该放在同一个数据库内!)

因此,PostgreSQL选择,从逻辑上将数据库彼此隔离。

或许,只要我们把跨数据库查询(Cross-Database Query),改名为跨模式查询(Cross-Schema Query),或许就变成这两个数据库都能支持的操作了呢?

构建数据库的思考

紧接而来的问题是,在PostgreSQL中,我们应该如何选择数据库的构建形式?

这样的问题在应用程序不涉及跨Schema查询时会更令人举棋不定,而跨Schema查询在构建微服务时从来都不是一个建议的选择。

现在考虑这两种构建形式:

第一种,大致如下图所示。每个数据库中只有一个Schema,即public。使用多个数据库存储不同业务模块数据,每个数据库彼此分离。

1
2
3
4
5
6
7
8
9
10
11
12
- PostgreSQL
|---- mydatabase1
| |---- public
| |---- mytable1
| |---- mytable1
| |---- mytable3
|
|---- mydatabase2
| |---- public
| |---- mytable4
| |---- mytable5
| |---- mytable6

第二种,大致如下图所示。每个数据库中使用多个Schema,尽可能减少PostgreSQL服务器下,Database的数量。

1
2
3
4
5
6
7
8
9
10
11
- PostgreSQL
|---- mydatabase1
|---- myschema1
| |---- mytable1
| |---- mytable1
| |---- mytable3
|
|---- myschema2
|---- mytable4
|---- mytable5
|---- mytable6

这两种方案从实现角度都是可行,现成的。第一种方案相较于第二种方案,失去了跨库查询的能力,但提高了隔离性。

这时候一定有人问:性能如何?

目前似乎在网上暂时搜不到这两种方案性能差异的对比。StackOverflow上2009年的帖子里,有人曾经引用Heroku PostgreSQL上的文档,认为 一个数据库内多个Schema(One Database - Many Schema) 的方案会严重拖累服务器性能,但似乎目前在该文档中已经找不到这一段文字,且实际上也没有给出两个方案具体的性能差异。

因此我更倾向于认为,两者在性能上不会有天差地别的影响。

当数据越来越多时候,数据库的效率会自然下降。或许考虑其他方法,如分表,索引等手段加快性能,收益会更为明显。

至于选择,我个人认为,第一种方案,即 One Database - One Schema 会更方便我们管理数据库的权限,我更倾向采用这种方案。

命名规则

虽然PostgreSQL的命名规则并不离经叛道,但我个人并不建议在数据库名、模式名、表名、字段名中使用大写字母!

因为,在PostgreSQL中,如果上述名字含有大写字母,需要将这一部分用双引号包起来!

逃课方法: 使用GUI数据库管理工具,如DBeaver、Beekeeper Studio等。 可以直接无视此部分问题。生成SQL语句复制粘贴即可。

例如,如果我们创建一个数据表如下所示。注意到Name字段包含了大写字母。

1
2
3
4
5
CREATE TABLE public.mytb1 (
id integer NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"Name" varchar NULL,
CONSTRAINT mytb1_pk PRIMARY KEY (id)
);

插入一些数据

1
2
INSERT INTO public.mytb1 ("Name") VALUES('Bob');
INSERT INTO public.mytb1 ("Name") VALUES('Cindy');

执行简单的查询

1
2
3
4
5
6
mytest1=# SELECT * FROM mytb1;
id | Name
----+-------
1 | Bob
2 | Cindy
(2 rows)

如果我们在语句中,包含对Name的操作但没有将其用双引号包围,会出现错误

1
2
3
4
5
mytest1=# SELECT * FROM mytb1 WHERE Name = 'Cindy';
ERROR: column "name" does not exist
LINE 1: SELECT * FROM mytb1 WHERE Name = 'Cindy';
^
HINT: Perhaps you meant to reference the column "mytb1.Name".

稍作修改,给Name加上双引号,就可以正常工作

1
2
3
4
5
mytest1=# SELECT * FROM mytb1 WHERE "Name" = 'Cindy';
id | Name
----+-------
2 | Cindy
(1 row)

如果你在查询时,总是找不到对应的关系,不妨检查一下有没有遗漏双引号。

数据类型

PostgreSQL的数据类型也没啥特别的。不过,根据SQL规范,整型的名称默认应该是integer,因此从MySQL迁移过来时需要稍微注意。

另外值得一提的还有自动增长类型的功能。在PostgreSQL中,自动增长常用Identity指代。注意,另有一种实现方法是通过serial实现,但这似乎是旧的方法,推荐采用Identity的方法。

Identity 有两种模式,即BY DEFAULTALWAYS。它们的区别在PostgreSQL: Documentation: 14: CREATE TABLE中有说明:

The clauses ALWAYS and BY DEFAULT determine how explicitly user-specified values are handled in INSERT and UPDATE commands.

In an INSERT command, if ALWAYS is selected, a user-specified value is only accepted if the INSERT statement specifies OVERRIDING SYSTEM VALUE. If BY DEFAULT is selected, then the user-specified value takes precedence. See INSERT for details. (In the COPY command, user-specified values are always used regardless of this setting.)

In an UPDATE command, if ALWAYS is selected, any update of the column to any value other than DEFAULT will be rejected. If BY DEFAULT is selected, the column can be updated normally. (There is no OVERRIDING clause for the UPDATE command.)

省流: ALWAYS在接受用户对数据表中,该自增序列作插入或修改时,条件比BY DEFAULT更严苛。

BY DEFAULT比较听用户的话,ALWAYS有自己的执着。

如果你之前有使用其他SQL的经验,上面的内容足够让你开心地在PostgreSQL上执行一系列查询了。