1.1 PostgreSQL的核心定位:对象-关系型数据库(ORDBMS)
如果你接触过数据库,一定听过"关系型数据库"(比如MySQL、Oracle)------它们用表、行、列 存储数据,用SQL语言操作。而PostgreSQL不一样,它是对象-关系型数据库(ORDBMS):既保留了关系型数据库的"规矩"(比如表结构、SQL标准),又融入了对象型数据库的"灵活"(比如自定义数据类型、方法)。
举个通俗的例子:如果要存储"几何图形"数据,传统关系型数据库可能会用多个字段(比如x1,y1,x2,y2
存矩形),但PostgreSQL允许你自定义一个rectangle
类型 ,直接存储矩形的坐标,还能给这个类型加个area()
方法(计算面积)。这就是"对象"特性的体现------把"数据"和"操作数据的方法"打包在一起;而"关系"特性则是它依然遵守SQL规则,能用表、JOIN、外键这些传统玩法。
1.2 PostgreSQL的起源与发展:从伯克利到全球社区
PostgreSQL不是"凭空造出来的",它的祖先叫POSTGRES ,是1986年美国加州大学伯克利分校(UC Berkeley)的科研项目。当时的关系型数据库有个大痛点:无法处理复杂数据类型(比如几何图形、文档)。POSTGRES的目标就是解决这个问题------它首次提出"对象-关系"模型,允许用户自定义数据类型和函数。
1996年,POSTGRES的开源版本发布,改名为"PostgreSQL"(意为"PostgreSQL是POSTGRES的后继者")。从那以后,全球开发者一起维护它,现在已经更新到17版本(2024年),成为最流行的开源数据库之一。
1.3 PostgreSQL的核心特性:为什么它能打?
PostgreSQL能火,不是因为"开源免费",而是因为它的"硬实力"------支持很多现代数据库的核心特性,而且做得很极致。我们逐一拆解:
1.3.1 复杂查询:搞定"绕弯子"的需求
PostgreSQL的查询能力堪称"全能",支持:
- 多表JOIN :比如查"每个订单的用户姓名和商品名称",可以把
orders
(订单表)、users
(用户表)、products
(商品表)JOIN起来; - 子查询:比如查"工资高于平均工资的员工",先用子查询算出平均工资,再筛选;
- CTE(Common Table Expressions) :用
WITH
语句把复杂查询拆成"临时表",比如查"每个部门的TOP3工资员工",用CTE先算每个部门的工资排名,再取前3。
举个电商系统的实际例子------热销商品统计:
sql
-- 第一步:用CTE算出每个商品的总销量
WITH product_sales AS (
SELECT product_id, SUM(quantity) AS total_sold
FROM orders
GROUP BY product_id
)
-- 第二步:关联商品表,得到热销TOP10
SELECT p.name, ps.total_sold
FROM products p
JOIN product_sales ps ON p.id = ps.product_id
ORDER BY ps.total_sold DESC LIMIT 10;
这个查询用CTE拆解了复杂逻辑,PostgreSQL处理起来毫无压力。
1.3.2 事务完整性:保证数据"不出错"
你肯定遇到过"转账失败"的坑:从A账户扣了钱,B账户却没收到------这就是"事务不完整"。PostgreSQL支持ACID特性,彻底解决这个问题:
- 原子性(Atomicity):事务里的操作要么全做,要么全不做。比如转账的两个步骤(扣钱、加钱),只要有一个失败,整个事务回滚;
- 一致性(Consistency):事务前后数据要符合规则。比如你的账户余额不能是负数,PostgreSQL会自动检查;
- 隔离性(Isolation):多个事务同时运行,互不干扰。比如你在查余额时,别人在转账,你看到的是转账前的余额;
- 持久性(Durability):事务提交后,数据永远不会丢(即使服务器宕机)。
1.3.3 MVCC:让"读写"互不干扰
传统数据库里,"读"和"写"会互相阻塞------比如你在看数据时,别人不能改;别人在改数据时,你不能看。这在高并发场景下(比如秒杀、电商大促)会卡死。
PostgreSQL用**MVCC(多版本并发控制)**解决了这个问题:每个事务看到的数据是一个"快照"。比如:
- 用户A在"读"订单表(看自己的订单);
- 用户B在"写"订单表(修改订单状态);
- 用户A看到的还是修改前的订单状态,直到用户B提交事务。
这样一来,"读"和"写"可以同时进行,不会阻塞,大大提高了并发性能。
1.3.4 外键、触发器:自动维护数据"规矩"
PostgreSQL还能帮你"自动做事",不用手动检查数据:
- 外键(Foreign Key) :比如
orders
表的user_id
必须指向users
表的id
。如果有人想插入一个不存在的user_id
,PostgreSQL会直接拒绝,防止"无效订单"; - 触发器(Trigger):比如当订单状态改为"完成"时,自动减少商品库存。你不用写代码调库存接口,PostgreSQL会帮你做:
sql
-- 1. 创建触发器函数(要做的事情)
CREATE OR REPLACE FUNCTION update_inventory()
RETURNS TRIGGER AS $$
BEGIN
-- 减少商品库存(NEW代表新插入的订单记录)
UPDATE products
SET stock = stock - NEW.quantity
WHERE id = NEW.product_id;
RETURN NEW; -- 返回新记录,继续执行插入操作
END;
$$ LANGUAGE plpgsql; -- 用PL/pgSQL写函数(PostgreSQL内置语言)
-- 2. 绑定触发器到orders表的INSERT操作
CREATE TRIGGER order_insert_trigger
AFTER INSERT ON orders -- 当插入订单后执行
FOR EACH ROW -- 每插入一行都执行
EXECUTE FUNCTION update_inventory(); -- 执行上面的函数
当你插入一个订单时,这个触发器会自动更新商品库存------是不是很省心?
1.4 PostgreSQL的"超能力":可扩展性
如果你觉得PostgreSQL的"默认功能"不够用,没关系------它允许你自定义几乎所有东西,堪称"数据库里的乐高":
1.4.1 自定义数据类型
比如你要存储"电话号码",可以定义一个phone_number
类型:
sql
-- 1. 创建自定义类型(包含国家码、区号、号码)
CREATE TYPE phone_number AS (
country_code VARCHAR(3), -- 国家码(比如+86)
area_code VARCHAR(3), -- 区号(比如010)
number VARCHAR(8) -- 号码(比如12345678)
);
-- 2. 用这个类型建表
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
phone phone_number -- 使用自定义类型
);
-- 3. 插入数据(用ROW()包裹自定义类型的值)
INSERT INTO users (name, phone)
VALUES ('张三', ROW('+86', '010', '12345678'));
-- 4. 查询数据(用.(字段名)取自定义类型的值)
SELECT name, (phone).country_code || '-' || (phone).area_code || '-' || (phone).number AS full_phone
FROM users;
1.4.2 自定义函数
PostgreSQL支持用多种语言写函数(比如PL/pgSQL、Python、Java)。比如你要计算两个点之间的距离,可以写一个函数:
sql
-- 1. 定义点类型(PostgreSQL内置了geometry类型,但这里用自定义类型举例)
CREATE TYPE point AS (x INT, y INT);
-- 2. 定义计算距离的函数
CREATE OR REPLACE FUNCTION distance(p1 point, p2 point)
RETURNS FLOAT AS $$ -- 返回浮点型(距离)
BEGIN
-- 勾股定理计算距离:√[(x1-x2)² + (y1-y2)²]
RETURN SQRT(POW(p1.x - p2.x, 2) + POW(p1.y - p2.y, 2));
END;
$$ LANGUAGE plpgsql;
-- 3. 使用函数(计算(0,0)到(3,4)的距离)
SELECT distance(ROW(0,0)::point, ROW(3,4)::point); -- 返回5.0
1.4.3 自定义索引
如果默认的B-tree索引不够用(比如要查"距离某点1公里内的商店"),PostgreSQL允许你用GiST 或GIN索引。比如用GiST索引加速几何查询:
sql
-- 1. 创建存储商店位置的表(用PostgreSQL内置的geometry类型)
CREATE TABLE shops (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
location geometry(Point, 4326) -- Point代表点,4326是坐标系(WGS84)
);
-- 2. 添加GiST索引(加速几何查询)
CREATE INDEX shops_location_idx ON shops USING GIST (location);
-- 3. 查询"天安门附近1公里内的商店"(116.403874, 39.914885是天安门的坐标)
SELECT name
FROM shops
WHERE ST_DWithin(
location,
ST_SetSRID(ST_MakePoint(116.403874, 39.914885), 4326), -- 天安门的点
1000 -- 距离(米)
);
1.5 课后Quiz:巩固你的理解
Q1:PostgreSQL作为ORDBMS,"对象"特性主要体现在哪里?
A1 :支持用户自定义数据类型、函数、运算符等"对象"特性。比如你可以定义phone_number
类型(包含国家码、区号、号码),或定义distance
函数(像"对象的方法"一样计算距离)。
Q2:MVCC(多版本并发控制)解决了什么问题?
A2:解决了"读"和"写"互相阻塞的问题。每个事务看到的是数据的"快照",比如你在查订单时,别人可以修改订单,你看到的还是修改前的状态,直到别人提交事务。高并发场景下(比如秒杀),读写可以同时进行,不会卡死。
Q3:PostgreSQL的触发器能做什么?举个例子。
A3:触发器可以在表发生插入、更新、删除时自动执行操作。比如当订单状态改为"完成"时,自动减少商品库存;或当用户注册时,自动发送欢迎邮件(需要结合外部服务)。
1.6 常见报错及解决
报错1:ERROR: relation "table_name" does not exist
原因:
- 表名拼写错误(比如把
orders
写成order
); - 表不在当前schema下(比如表在
sales
schema里,但你用了public
schema); - 用户没有访问表的权限。
解决办法:
- 检查表名拼写:用
\dt
命令(psql客户端)查看当前schema下的表; - 指定schema:比如
SELECT * FROM sales.orders;
(如果表在sales
schema里); - 授予权限:用
GRANT SELECT ON orders TO your_user;
给用户读权限。
预防:
- 用全小写、下划线分隔的命名规范(比如
user_orders
而不是UserOrders
); - 总是指定schema(比如
public.orders
); - 给用户最小必要的权限(比如只读用户只给
SELECT
权限)。
报错2:ERROR: insert or update on table "orders" violates foreign key constraint "orders_user_id_fkey"
原因 :你插入的user_id
在users
表中不存在(比如users
表没有id=100
的用户,但你插入了user_id=100
的订单)。
解决办法:
- 检查
user_id
是否存在:SELECT * FROM users WHERE id = 100;
; - 如果不存在,先插入用户,再插入订单。
预防:
- 用外键约束(PostgreSQL会自动检查);
- 插入数据前先验证关联ID的存在。
参考链接
- What Is PostgreSQL?:www.postgresql.org/docs/17/int...
- Data Types:www.postgresql.org/docs/17/dat...
- Triggers:www.postgresql.org/docs/17/tri...
- MVCC:www.postgresql.org/docs/17/mvc...