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

PostgreSQL 完全指南 / 04 - SQL 基础

第 04 章 · SQL 基础

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


4.1 SQL 语句分类

类别全称关键字作用
DDLData Definition LanguageCREATE, ALTER, DROP, TRUNCATE定义/修改数据库对象结构
DMLData Manipulation LanguageINSERT, UPDATE, DELETE, MERGE增删改数据
DQLData Query LanguageSELECT查询数据
DCLData Control LanguageGRANT, REVOKE权限控制
TCLTransaction ControlBEGIN, 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;
操作DELETETRUNCATE
速度慢(逐行删除)快(直接释放数据页)
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 / INT4 字节整数42
BIGINT8 字节整数9999999999
SMALLINT2 字节整数100
SERIAL自增 4 字节整数自动生成
BIGSERIAL自增 8 字节整数自动生成
NUMERIC(p,s)精确数值12345.67
REAL4 字节浮点数3.14
DOUBLE PRECISION8 字节浮点数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'
UUIDUUID'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'
JSONJSON 文本'{"key": "value"}'
JSONBJSON 二进制(推荐)'{"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

扩展阅读