强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

PostgreSQL 完全指南 / 04 - SQL 基础

第 04 章 · SQL 基础

本章涵盖 PostgreSQL 中 SQL 语言的核心语法:数据定义(DDL)、数据操作(DML)、数据查询(DQL)以及常用数据类型。


4.1 SQL 语句分类

类别 全称 关键字 作用
DDL Data Definition Language CREATE, ALTER, DROP, TRUNCATE 定义/修改数据库对象结构
DML Data Manipulation Language INSERT, UPDATE, DELETE, MERGE 增删改数据
DQL Data Query Language SELECT 查询数据
DCL Data Control Language GRANT, REVOKE 权限控制
TCL Transaction Control BEGIN, COMMIT, ROLLBACK, SAVEPOINT 事务控制

4.2 DDL — 数据定义

数据库操作

-- 创建数据库
CREATE DATABASE mydb
    WITH
    OWNER = myuser
    ENCODING = 'UTF8'
    LC_COLLATE = 'en_US.UTF-8'
    LC_CTYPE = 'en_US.UTF-8'
    TEMPLATE = template0
    CONNECTION LIMIT = 100;

-- 查看所有数据库
\l

-- 修改数据库
ALTER DATABASE mydb SET timezone = 'Asia/Shanghai';

-- 删除数据库(危险操作!)
DROP DATABASE IF EXISTS mydb;

-- 重命名数据库
ALTER DATABASE mydb RENAME TO newdb;

⚠️ 注意事项DROP DATABASE 不可逆,会删除所有数据。生产环境务必谨慎!

表操作

-- 创建表
CREATE TABLE employees (
    id          BIGSERIAL       PRIMARY KEY,
    name        VARCHAR(100)    NOT NULL,
    email       VARCHAR(255)    UNIQUE NOT NULL,
    age         SMALLINT        CHECK (age >= 0 AND age <= 150),
    salary      NUMERIC(12, 2)  DEFAULT 0.00,
    department  VARCHAR(50),
    hire_date   DATE            NOT NULL DEFAULT CURRENT_DATE,
    bio         TEXT,
    is_active   BOOLEAN         DEFAULT TRUE,
    created_at  TIMESTAMPTZ     DEFAULT NOW(),
    updated_at  TIMESTAMPTZ     DEFAULT NOW()
);

-- 添加列
ALTER TABLE employees ADD COLUMN phone VARCHAR(20);

-- 修改列类型
ALTER TABLE employees ALTER COLUMN phone TYPE VARCHAR(30);

-- 添加 NOT NULL 约束
ALTER TABLE employees ALTER COLUMN name SET NOT NULL;

-- 删除列
ALTER TABLE employees DROP COLUMN IF EXISTS phone;

-- 重命名列
ALTER TABLE employees RENAME COLUMN name TO full_name;

-- 添加约束
ALTER TABLE employees ADD CONSTRAINT chk_salary
    CHECK (salary >= 0);

-- 删除约束
ALTER TABLE employees DROP CONSTRAINT IF EXISTS chk_salary;

-- 创建索引
CREATE INDEX idx_emp_email ON employees (email);
CREATE INDEX idx_emp_dept ON employees (department) WHERE is_active = TRUE;

-- 删除表
DROP TABLE IF EXISTS employees CASCADE;

-- 截断表(清空数据,保留结构)
TRUNCATE TABLE employees RESTART IDENTITY;

PostgreSQL 特有的 DDL 语法

-- IF EXISTS / IF NOT EXISTS(防止报错)
CREATE TABLE IF NOT EXISTS t1 (id INT);
DROP TABLE IF EXISTS t1;

-- CREATE TABLE AS(从查询结果建表)
CREATE TABLE emp_backup AS SELECT * FROM employees WHERE hire_date < '2024-01-01';

-- CREATE TABLE ... LIKE(复制表结构)
CREATE TABLE emp_like (LIKE employees INCLUDING ALL);

-- 自增主键的三种方式
-- 方式 1:BIGSERIAL(传统方式)
CREATE TABLE t1 (id BIGSERIAL PRIMARY KEY, name TEXT);

-- 方式 2:GENERATED ALWAYS AS IDENTITY(SQL 标准,推荐)
CREATE TABLE t2 (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT);

-- 方式 3:GENERATED BY DEFAULT AS IDENTITY(允许手动指定值)
CREATE TABLE t3 (id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name TEXT);

4.3 DML — 数据操作

INSERT

-- 单行插入
INSERT INTO employees (name, email, age, salary, department, hire_date)
VALUES ('张三', 'zhangsan@example.com', 30, 15000.00, '工程部', '2024-01-15');

-- 多行插入
INSERT INTO employees (name, email, age, salary, department, hire_date) VALUES
    ('李四', 'lisi@example.com', 28, 12000.00, '产品部', '2024-02-01'),
    ('王五', 'wangwu@example.com', 35, 20000.00, '工程部', '2023-06-15'),
    ('赵六', 'zhaoliu@example.com', 26, 10000.00, '设计部', '2024-03-10');

-- 从查询结果插入
INSERT INTO emp_backup (name, email, salary)
SELECT name, email, salary FROM employees WHERE department = '工程部';

-- UPSERT(INSERT ... ON CONFLICT)— PG 独有亮点
INSERT INTO employees (name, email, age, salary, department, hire_date)
VALUES ('张三', 'zhangsan@example.com', 31, 16000.00, '工程部', '2024-01-15')
ON CONFLICT (email)
DO UPDATE SET
    age = EXCLUDED.age,
    salary = EXCLUDED.salary,
    updated_at = NOW();

-- ON CONFLICT DO NOTHING(忽略冲突)
INSERT INTO employees (name, email, age, salary, department, hire_date)
VALUES ('张三', 'zhangsan@example.com', 31, 16000.00, '工程部', '2024-01-15')
ON CONFLICT DO NOTHING;

-- RETURNING 子句(返回受影响的行)
INSERT INTO employees (name, email, age, salary, department, hire_date)
VALUES ('钱七', 'qianqi@example.com', 29, 13000.00, '市场部', '2024-04-01')
RETURNING id, name;

UPDATE

-- 基本更新
UPDATE employees
SET salary = salary * 1.10
WHERE department = '工程部';

-- 多列更新
UPDATE employees
SET salary = 25000.00, department = '技术管理部', updated_at = NOW()
WHERE name = '王五';

-- 基于其他表的更新(使用 FROM)
UPDATE employees e
SET salary = e.salary * (1 + r.raise_rate)
FROM salary_raises r
WHERE e.id = r.emp_id AND r.effective_date = CURRENT_DATE;

-- RETURNING
UPDATE employees
SET is_active = FALSE
WHERE hire_date < '2020-01-01'
RETURNING id, name, is_active;

DELETE

-- 基本删除
DELETE FROM employees WHERE id = 42;

-- 使用子查询删除
DELETE FROM employees
WHERE id IN (
    SELECT e.id FROM employees e
    LEFT JOIN departments d ON e.department = d.name
    WHERE d.name IS NULL
);

-- 使用 USING(多表关联删除)
DELETE FROM employees e
USING departments d
WHERE e.department = d.name AND d.is_archived = TRUE;

-- TRUNCATE(清空整表,比 DELETE 快得多)
TRUNCATE TABLE temp_data RESTART IDENTITY CASCADE;
操作 DELETE TRUNCATE
速度 慢(逐行删除) 快(直接释放数据页)
WHERE 条件 ✅ 支持 ❌ 不支持
触发器 ✅ 触发 ❌ 不触发
回滚 ✅ 可回滚 ✅ 可回滚(事务内)
自增列 不重置 RESTART IDENTITY 重置
空间释放 VACUUM 后释放 立即释放

MERGE(PG 16+)

-- MERGE:根据条件执行 INSERT/UPDATE/DELETE
MERGE INTO target_table t
USING source_table s ON t.id = s.id
WHEN MATCHED AND s.is_deleted THEN
    DELETE
WHEN MATCHED THEN
    UPDATE SET t.name = s.name, t.updated_at = NOW()
WHEN NOT MATCHED THEN
    INSERT (id, name, created_at)
    VALUES (s.id, s.name, NOW());

4.4 DQL — 数据查询

SELECT 基础

-- 基本查询
SELECT id, name, salary FROM employees;

-- 去重
SELECT DISTINCT department FROM employees;

-- 别名
SELECT
    name AS 姓名,
    salary AS 月薪,
    salary * 12 AS 年薪
FROM employees;

-- 条件过滤
SELECT * FROM employees
WHERE department = '工程部'
  AND salary > 10000
  AND is_active = TRUE;

-- IN / NOT IN
SELECT * FROM employees WHERE department IN ('工程部', '产品部');

-- BETWEEN
SELECT * FROM employees WHERE salary BETWEEN 10000 AND 20000;

-- LIKE / ILIKE(PG 的 ILIKE 大小写不敏感)
SELECT * FROM employees WHERE name LIKE '张%';
SELECT * FROM employees WHERE email ILIKE '%@example.com';

-- NULL 判断
SELECT * FROM employees WHERE bio IS NULL;
SELECT * FROM employees WHERE bio IS NOT NULL;

-- COALESCE(空值替换)
SELECT name, COALESCE(bio, '暂无简介') AS bio FROM employees;

-- CASE WHEN(条件表达式)
SELECT name, salary,
    CASE
        WHEN salary >= 20000 THEN '高薪'
        WHEN salary >= 10000 THEN '中等'
        ELSE '初级'
    END AS salary_level
FROM employees;

ORDER BY / LIMIT / OFFSET

-- 排序
SELECT * FROM employees ORDER BY salary DESC, name ASC;

-- 分页
SELECT * FROM employees ORDER BY id LIMIT 20 OFFSET 40;
-- 第 3 页(每页 20 条)

-- LIMIT ... WITH TIES(PG 语法,包含并列行)
SELECT name, salary FROM employees
ORDER BY salary DESC
LIMIT 5 WITH TIES;

聚合函数

-- 常用聚合函数
SELECT
    COUNT(*) AS total_count,
    COUNT(DISTINCT department) AS dept_count,
    AVG(salary)::NUMERIC(12,2) AS avg_salary,
    MIN(salary) AS min_salary,
    MAX(salary) AS max_salary,
    SUM(salary) AS total_salary,
    STRING_AGG(name, ', ') AS all_names
FROM employees;

-- GROUP BY
SELECT
    department,
    COUNT(*) AS emp_count,
    AVG(salary)::NUMERIC(12,2) AS avg_salary
FROM employees
GROUP BY department;

-- HAVING(过滤分组结果)
SELECT
    department,
    AVG(salary)::NUMERIC(12,2) AS avg_salary
FROM employees
GROUP BY department
HAVING AVG(salary) > 15000;

JOIN

-- 创建示例表
CREATE TABLE departments (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50) UNIQUE NOT NULL,
    budget NUMERIC(15,2)
);

INSERT INTO departments (name, budget) VALUES
    ('工程部', 500000),
    ('产品部', 300000),
    ('设计部', 200000),
    ('市场部', 250000);

-- INNER JOIN(内连接)
SELECT e.name, e.salary, d.budget
FROM employees e
INNER JOIN departments d ON e.department = d.name;

-- LEFT JOIN(左连接,保留左表全部行)
SELECT e.name, e.salary, d.budget
FROM employees e
LEFT JOIN departments d ON e.department = d.name;

-- RIGHT JOIN(右连接)
SELECT e.name, d.name AS dept, d.budget
FROM employees e
RIGHT JOIN departments d ON e.department = d.name;

-- FULL OUTER JOIN(全外连接)
SELECT e.name, d.name AS dept
FROM employees e
FULL OUTER JOIN departments d ON e.department = d.name;

-- CROSS JOIN(笛卡尔积)
SELECT e.name, d.name FROM employees e CROSS JOIN departments d;

-- SELF JOIN(自连接)
SELECT e1.name AS employee, e2.name AS colleague
FROM employees e1
INNER JOIN employees e2 ON e1.department = e2.department AND e1.id <> e2.id;

子查询

-- 标量子查询
SELECT name, salary, (SELECT AVG(salary) FROM employees)::NUMERIC(12,2) AS avg_salary
FROM employees;

-- IN 子查询
SELECT * FROM employees
WHERE department IN (SELECT name FROM departments WHERE budget > 300000);

-- EXISTS 子查询
SELECT * FROM departments d
WHERE EXISTS (SELECT 1 FROM employees e WHERE e.department = d.name);

-- FROM 子句中的子查询(派生表)
SELECT dept, avg_sal FROM (
    SELECT department AS dept, AVG(salary) AS avg_sal
    FROM employees
    GROUP BY department
) sub
WHERE avg_sal > 10000;

4.5 常用数据类型速查

类型 说明 示例
INTEGER / INT 4 字节整数 42
BIGINT 8 字节整数 9999999999
SMALLINT 2 字节整数 100
SERIAL 自增 4 字节整数 自动生成
BIGSERIAL 自增 8 字节整数 自动生成
NUMERIC(p,s) 精确数值 12345.67
REAL 4 字节浮点数 3.14
DOUBLE PRECISION 8 字节浮点数 3.14159265358979
VARCHAR(n) 变长字符串(有长度限制) 'hello'
CHAR(n) 定长字符串(补空格) 'ab'
TEXT 变长字符串(无长度限制) '任意文本'
BOOLEAN 布尔值 TRUE, FALSE, NULL
DATE 日期 '2024-01-15'
TIME 时间 '14:30:00'
TIMESTAMP 日期时间(无时区) '2024-01-15 14:30:00'
TIMESTAMPTZ 日期时间(有时区) '2024-01-15 14:30:00+08'
INTERVAL 时间间隔 '2 years 3 months'
UUID UUID 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'
JSON JSON 文本 '{"key": "value"}'
JSONB JSON 二进制(推荐) '{"key": "value"}'
ARRAY 数组 '{1,2,3}'ARRAY[1,2,3]
BYTEA 二进制数据 '\x48656c6c6f'

💡 技巧:优先使用 TEXT 而非 VARCHAR(n)。PostgreSQL 中 TEXTVARCHAR 性能完全相同,TEXT 更灵活。只有在需要业务约束(如邮箱最长 255)时才用 VARCHAR(n)

💡 技巧:时间类型优先使用 TIMESTAMPTZ(带时区),避免时区转换问题。


4.6 PostgreSQL 特有的 SQL 特性

-- RETURNING 子句(INSERT/UPDATE/DELETE 后返回结果)
DELETE FROM employees WHERE id = 42 RETURNING *;

-- ILIKE(大小写不敏感 LIKE)
SELECT * FROM employees WHERE name ILIKE '%张%';

-- 类型转换(双冒号语法)
SELECT '123'::INTEGER, '2024-01-15'::DATE, '{"a":1}'::JSONB;

-- ARRAY 操作
SELECT ARRAY[1,2,3] || ARRAY[4,5];  -- 数组拼接:{1,2,3,4,5}
SELECT ARRAY[1,2,3,4] @> ARRAY[2,3]; -- 包含判断:true

-- 任意值比较
SELECT * FROM employees WHERE salary > ALL (SELECT AVG(salary) FROM employees GROUP BY department);

-- 值构造器(VALUES)
VALUES (1, 'a'), (2, 'b'), (3, 'c');

-- GROUPING SETS / ROLLUP / CUBE
SELECT department, is_active, COUNT(*)
FROM employees
GROUP BY ROLLUP(department, is_active);

业务场景

场景 SQL 技巧
用户注册 INSERT ... ON CONFLICT 实现去重注册
数据报表 GROUP BY + 聚合函数 + ORDER BY
关联查询 JOIN + 派生表 + 子查询
分页列表 ORDER BY ... LIMIT ... OFFSET
批量操作 多行 INSERT + RETURNING
数据迁移 CREATE TABLE AS SELECT

扩展阅读