目录
[1.1 表分区](#1.1 表分区)
[1.1.1 表分区基本概念](#1.1.1 表分区基本概念)
[1.2 范围分区详解](#1.2 范围分区详解)
[1.2.1 范围分区语法结构](#1.2.1 范围分区语法结构)
[1.2.2 范围分区实战案例](#1.2.2 范围分区实战案例)
[1.2.3 范围分区使用模式](#1.2.3 范围分区使用模式)
[1.3 列表分区详解](#1.3 列表分区详解)
[1.3.1 列表分区语法结构](#1.3.1 列表分区语法结构)
[1.3.2 列表分区实战案例](#1.3.2 列表分区实战案例)
[1.3.3 列表分区使用模式](#1.3.3 列表分区使用模式)
[1.4 哈希分区详解](#1.4 哈希分区详解)
[1.4.1 哈希分区语法结构](#1.4.1 哈希分区语法结构)
[1.4.2 哈希分区实战案例](#1.4.2 哈希分区实战案例)
[1.4.3 哈希分区使用模式](#1.4.3 哈希分区使用模式)
[1.5 多级分区详解](#1.5 多级分区详解)
[1.5.1 多级分区语法结构](#1.5.1 多级分区语法结构)
[1.5.2 多级分区实战案例](#1.5.2 多级分区实战案例)
[1.5.3 多级分区使用模式](#1.5.3 多级分区使用模式)
[1.6 分区管理操作](#1.6 分区管理操作)
[1.6.1 分区维护语法结构](#1.6.1 分区维护语法结构)
[1.6.2 分区维护实战案例](#1.6.2 分区维护实战案例)
[1.6.3 分区信息查询](#1.6.3 分区信息查询)
[1.7 索引基础概念](#1.7 索引基础概念)
[1.7.1 什么是索引?](#1.7.1 什么是索引?)
[1.7.2 索引的类型概述](#1.7.2 索引的类型概述)
[1.8 B-tree 索引详解](#1.8 B-tree 索引详解)
[1.8.1 B-tree 索引概念](#1.8.1 B-tree 索引概念)
[1.8.2 创建 B-tree 索引](#1.8.2 创建 B-tree 索引)
[1.8.3 B-tree 索引使用场景](#1.8.3 B-tree 索引使用场景)
[1.9 高级索引类型](#1.9 高级索引类型)
[1.9.1 Hash 索引](#1.9.1 Hash 索引)
[1.10 GiST 索引](#1.10 GiST 索引)
[1.11 GIN 索引](#1.11 GIN 索引)
[1.12 BRIN 索引](#1.12 BRIN 索引)
[1.13 索引优化策略](#1.13 索引优化策略)
[1.13.1 索引选择策略](#1.13.1 索引选择策略)
[1.13.1 复合索引策略](#1.13.1 复合索引策略)
[1.13.3 部分索引和表达式索引](#1.13.3 部分索引和表达式索引)
1.1 表分区
1.1.1 表分区基本概念
**概念解释:**表分区是一种数据库设计技术,它将一个大表按照某种规则分割成多个较小的、更易管理的物理部分,这些部分被称为分区。尽管在物理上被分割,但在逻辑上仍然表现为一个完整的表。
分区的核心价值:
-
性能提升:查询只需要扫描相关分区,减少 I/O 操作
-
管理便捷:可以对单个分区进行维护操作,而不影响整个表
-
数据生命周期管理:轻松实现旧数据的归档和删除
-
存储优化:不同分区可以存储在不同的表空间中。
PostgreSQL 表分区按分区策略这一维度可分为 3 种原生类型,再加上由这 3 种类型组合而成的复合分区,共 4 类:
**(1)范围分区(Range Partitioning):**按连续区间(日期、数值、自增 ID 等)切分,各分区区间互不重叠,适合时间序列表、日志表等顺序写入场景。
**(2)列表分区(List Partitioning):**按离散值列表(如地区、状态、业务线)切分,每个分区显式绑定一组枚举值,适合类别明确且值集相对固定的表。
**(3)哈希分区(Hash Partitioning):**对分区键做哈希取模,将数据均匀分散到固定数量的分区,适合没有天然范围或列表特征、仅希望负载均衡的大表。
**(4)复合/多级分区(Composite / Multi-Level Partitioning):**在上述三种策略基础上再嵌套一层或多层分区,例如"先按时间做范围分区,再对每个子分区按地区做列表分区",形成分区树,用于超大规模数据或需要同时兼顾多种访问维度的场景。
**注:**PostgreSQL 10 起提供"声明式分区"(DDL 直接 PARTITION BY ...),10 之前需借助表继承+触发器实现;上述分类适用于声明式分区,也可通过继承方式模拟。
1.2 范围分区详解
1.2.1 范围分区语法结构
完整语法:
sql
-- 创建分区表主表语法
CREATE TABLE table_name (
column1 data_type,
column2 data_type,
...
) PARTITION BY RANGE (partition_key);
-- 创建分区语法
CREATE TABLE partition_name PARTITION OF table_name
FOR VALUES FROM (start_value) TO (end_value);
-- 创建默认分区语法
CREATE TABLE partition_name PARTITION OF table_name DEFAULT;
语法说明:
-
PARTITION BY RANGE (partition_key):指定按范围分区,partition_key 是分区依据的列 -
FOR VALUES FROM (start_value) TO (end_value):定义分区的取值范围,包含 start_value,不包含 end_value -
DEFAULT:创建默认分区,用于存储不符合任何分区范围的数据
1.2.2 范围分区实战案例
SQL 示例演示:
sql
-- 创建按月的销售数据分区表
drop table if exists hr.sales;
CREATE TABLE hr.sales (
sale_id SERIAL,
sale_date DATE NOT NULL,
customer_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER,
unit_price DECIMAL(10,2),
total_amount DECIMAL(10,2),
region VARCHAR(20)
) PARTITION BY RANGE (sale_date);
-- 创建季度分区
CREATE TABLE sales_2022_q1 PARTITION OF hr.sales FOR VALUES FROM ('2022-01-01') TO ('2022-04-01');
CREATE TABLE sales_2022_q2 PARTITION OF hr.sales FOR VALUES FROM ('2022-04-01') TO ('2022-07-01');
CREATE TABLE sales_2022_q3 PARTITION OF hr.sales FOR VALUES FROM ('2022-07-01') TO ('2022-10-01');
CREATE TABLE sales_2022_q4 PARTITION OF hr.sales FOR VALUES FROM ('2022-10-01') TO ('2023-01-01');
CREATE TABLE sales_default PARTITION OF hr.sales DEFAULT; -- -- 创建默认分区
-- 插入测试数据
INSERT INTO hr.sales (sale_date, customer_id, product_id, quantity, unit_price, total_amount, region)
VALUES
('2022-01-15', 1001, 2001, 2, 50.00, 100.00, 'North'),
('2022-05-20', 1002, 2002, 1, 75.00, 75.00, 'South'),
('2022-08-10', 1003, 2003, 3, 30.00, 90.00, 'East'),
('2022-11-25', 1004, 2004, 1, 120.00, 120.00, 'West');
-- 验证数据分布
SELECT tableoid::regclass AS partition_name, count(*)
FROM hr.sales
GROUP BY partition_name
ORDER BY partition_name;
--
INSERT INTO hr.sales (sale_date, customer_id, product_id, quantity, unit_price, total_amount, region)
SELECT '2025-01-15' as sale_date
,1005 + i as customer_id
,2005 + i as product_id
,1 + i as quantity
,20.36 + i as unit_price
,63 + 5%3 as total_amount
,'North' as region
FROM generate_series(1,100) i;
1.2.3 范围分区使用模式
查询模式:
sql
-- 精确日期查询 - 只扫描一个分区
SELECT *
FROM sales
WHERE sale_date = '2022-05-20';
-- 日期范围查询 - 扫描多个分区
SELECT *
FROM sales
WHERE sale_date BETWEEN '2022-04-01' AND '2022-09-30';
-- 季度统计查询
SELECT
EXTRACT(QUARTER FROM sale_date) as quarter,
COUNT(*) as transaction_count,
SUM(total_amount) as total_revenue
FROM sales
WHERE sale_date BETWEEN '2022-01-01' AND '2022-12-31'
GROUP BY quarter
ORDER BY quarter;
1.3 列表分区详解
1.3.1 列表分区语法结构
完整语法:
sql
-- 创建分区表主表语法
CREATE TABLE table_name (
column1 data_type,
column2 data_type,
...
) PARTITION BY LIST (partition_key);
-- 创建分区语法
CREATE TABLE partition_name PARTITION OF table_name
FOR VALUES IN (value1, value2, value3, ...);
-- 创建默认分区语法
CREATE TABLE partition_name PARTITION OF table_name DEFAULT;
语法说明:
-
PARTITION BY LIST (partition_key):指定按列表分区,partition_key 是分区依据的列 -
FOR VALUES IN (value1, value2, ...):定义分区包含的离散值列表 -
DEFAULT:创建默认分区,用于存储不符合任何分区列表的数据
1.3.2 列表分区实战案例
SQL 示例演示:
sql
-- 创建按地区的客户数据分区表
CREATE TABLE customers (
customer_id BIGSERIAL,
customer_name VARCHAR(100) NOT NULL,
email VARCHAR(150),
phone VARCHAR(20),
region VARCHAR(20) NOT NULL,
signup_date DATE,
status VARCHAR(10) DEFAULT 'ACTIVE'
) PARTITION BY LIST (region);
-- 创建地区分区
CREATE TABLE customers_north PARTITION OF customers
FOR VALUES IN ('Beijing', 'Tianjin', 'Hebei', 'Shanxi', 'Inner Mongolia');
CREATE TABLE customers_east PARTITION OF customers
FOR VALUES IN ('Shanghai', 'Jiangsu', 'Zhejiang', 'Anhui', 'Fujian', 'Jiangxi', 'Shandong');
CREATE TABLE customers_south PARTITION OF customers
FOR VALUES IN ('Guangdong', 'Guangxi', 'Hainan', 'Henan', 'Hubei', 'Hunan');
CREATE TABLE customers_west PARTITION OF customers
FOR VALUES IN ('Chongqing', 'Sichuan', 'Guizhou', 'Yunnan', 'Tibet', 'Shaanxi', 'Gansu', 'Qinghai', 'Ningxia', 'Xinjiang');
CREATE TABLE customers_northeast PARTITION OF customers
FOR VALUES IN ('Liaoning', 'Jilin', 'Heilongjiang');
-- 创建默认分区
CREATE TABLE customers_default PARTITION OF customers DEFAULT;
-- 插入测试数据
INSERT INTO customers (customer_name, email, region, signup_date)
VALUES
('张三', 'zhangsan@email.com', 'Beijing', '2023-01-15'),
('李四', 'lisi@email.com', 'Shanghai', '2023-02-20'),
('王五', 'wangwu@email.com', 'Guangdong', '2023-03-10'),
('赵六', 'zhaoliu@email.com', 'Sichuan', '2023-04-05'),
('钱七', 'qianqi@email.com', 'Liaoning', '2023-05-12');
-- 验证数据分布
SELECT tableoid::regclass AS partition_name, count(*)
FROM customers
GROUP BY partition_name
ORDER BY partition_name;
1.3.3 列表分区使用模式
查询模式:
sql
-- 单地区查询 - 只扫描一个分区
SELECT * FROM customers WHERE region = 'Beijing';
-- 多地区查询 - 扫描多个分区
SELECT * FROM customers WHERE region IN ('Beijing', 'Shanghai', 'Guangdong');
-- 地区统计查询
SELECT
region,
COUNT(*) as customer_count,
MIN(signup_date) as first_signup,
MAX(signup_date) as latest_signup
FROM customers
GROUP BY region
ORDER BY customer_count DESC;
1.4 哈希分区详解
概念解释:
哈希分区使用哈希函数将分区键的值映射到一个固定的数值范围内,然后根据这个数值决定数据应该存储在哪个分区中。哈希分区也称为散列分区。
哈希函数 特性:
-
确定性:相同的输入总是产生相同的输出。
-
均匀性:不同的输入均匀分布在输出范围内。
-
高效性:计算速度快,适合大数据量处理。
-
不可逆性:从输出值很难反推输入值。
PostgreSQL 哈希函数 :
PostgreSQL 使用内置的哈希函数来处理分区键,支持各种数据类型的哈希计算。
哈希分区算法公式
sql
分区号 = abs(hash(分区键值)) % 分区总数
计算步骤:
-
对分区键值应用哈希函数,得到一个哈希值。
-
取哈希值的绝对值(确保为正数)。
-
对分区总数取模运算,得到分区编号(0 到 分区总数 -1)。
SQL 示例演示:
sql
-- 查看 PostgreSQL 的哈希函数结果
SELECT
user_id,
hashtext(user_id::text) as hash_value,
abs(hashtext(user_id::text)) as abs_hash,
abs(hashtext(user_id::text)) % 4 as partition_number
FROM (VALUES (1001), (1002), (1003), (1004)) AS users(user_id);
1.4.1 哈希分区语法结构
完整语法:
sql
-- 创建分区表主表语法
CREATE TABLE table_name (
column1 data_type,
column2 data_type,
...
) PARTITION BY HASH (partition_key);
-- 创建分区语法
CREATE TABLE partition_name PARTITION OF table_name
FOR VALUES WITH (MODULUS modulus_value, REMAINDER remainder_value);
语法说明:
-
PARTITION BY HASH (partition_key):指定按哈希分区,partition_key 是分区依据的列 -
MODULUS modulus_value:指定哈希分区的总数 -
REMAINDER remainder_value:指定当前分区对应的余数(从 0 到 modulus_value-1) -
哈希分区没有默认分区概念
1.4.2 哈希分区实战案例
SQL 示例演示:
sql
-- 创建哈希分区的用户会话表
drop table if exists hr.user_sessions;
CREATE TABLE hr.user_sessions (
session_id UUID DEFAULT gen_random_uuid(),
user_id INTEGER NOT NULL,
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
logout_time TIMESTAMP,
ip_address TEXT,
user_agent TEXT,
session_data JSONB
) PARTITION BY HASH (user_id);
-- 创建8个哈希分区
CREATE TABLE user_sessions_p0 PARTITION OF hr.user_sessions FOR VALUES WITH (MODULUS 8, REMAINDER 0);
CREATE TABLE user_sessions_p1 PARTITION OF hr.user_sessions FOR VALUES WITH (MODULUS 8, REMAINDER 1);
CREATE TABLE user_sessions_p2 PARTITION OF hr.user_sessions FOR VALUES WITH (MODULUS 8, REMAINDER 2);
CREATE TABLE user_sessions_p3 PARTITION OF hr.user_sessions FOR VALUES WITH (MODULUS 8, REMAINDER 3);
CREATE TABLE user_sessions_p4 PARTITION OF hr.user_sessions FOR VALUES WITH (MODULUS 8, REMAINDER 4);
CREATE TABLE user_sessions_p5 PARTITION OF hr.user_sessions FOR VALUES WITH (MODULUS 8, REMAINDER 5);
CREATE TABLE user_sessions_p6 PARTITION OF hr.user_sessions FOR VALUES WITH (MODULUS 8, REMAINDER 6);
CREATE TABLE user_sessions_p7 PARTITION OF hr.user_sessions FOR VALUES WITH (MODULUS 8, REMAINDER 7);
-- 插入测试数据
INSERT INTO hr.user_sessions (user_id, ip_address, user_agent)
SELECT
(random() * 10000)::integer as user_id,
'192.168.1.' || (random() * 255)::integer as ip_address,
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' as user_agent
FROM generate_series(1, 1000);
-- 验证数据分布均匀性
SELECT
tableoid::regclass AS partition_name,
count(*) as record_count,
count(distinct user_id) as unique_users
FROM hr.user_sessions
GROUP BY partition_name
ORDER BY partition_name;
1.4.3 哈希分区使用模式
查询模式:
sql
-- 精确用户查询 - 通过哈希定位到单个分区
SELECT * FROM hr.user_sessions WHERE user_id = 1234;
-- 用户范围查询 - 需要扫描所有分区
SELECT * FROM hr.user_sessions WHERE user_id BETWEEN 1000 AND 2000;
-- 活跃会话统计
SELECT
COUNT(*) as active_sessions,
COUNT(DISTINCT user_id) as unique_users
FROM hr.user_sessions
WHERE logout_time IS NULL;
1.5 多级分区详解
1.5.1 多级分区语法结构
完整语法:
sql
-- 创建一级分区表主表语法
CREATE TABLE table_name (
column1 data_type,
column2 data_type,
...
) PARTITION BY RANGE|LIST|HASH (partition_key1);
-- 创建一级分区语法
CREATE TABLE first_level_partition PARTITION OF table_name
FOR VALUES ... -- 根据一级分区类型使用相应的VALUES子句
PARTITION BY RANGE|LIST|HASH (partition_key2);
-- 创建二级分区语法
CREATE TABLE second_level_partition PARTITION OF first_level_partition
FOR VALUES ...; -- 根据二级分区类型使用相应的VALUES子句
语法说明:
-
多级分区是分层的,第一级分区下可以继续分区
-
每一级可以使用不同的分区策略(范围、列表、哈希)
-
支持任意深度的分区层次,但通常建议不超过 3 级
1.5.2 多级分区实战案例
SQL 示例演示:
sql
-- 创建二级分区表:先按时间范围分区,再按地区列表分区
CREATE TABLE sales_detailed (
sale_id BIGSERIAL,
sale_date DATE NOT NULL,
customer_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER,
unit_price DECIMAL(10,2),
total_amount DECIMAL(10,2),
region VARCHAR(20) NOT NULL,
city VARCHAR(50)
) PARTITION BY RANGE (sale_date);
-- 第一级:按年分区
CREATE TABLE sales_2023 PARTITION OF sales_detailed
FOR VALUES FROM ('2023-01-01') TO ('2024-01-01')
PARTITION BY LIST (region);
CREATE TABLE sales_2024 PARTITION OF sales_detailed
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01')
PARTITION BY LIST (region);
-- 第二级:按地区分区
-- 2023年分区
CREATE TABLE sales_2023_north PARTITION OF sales_2023
FOR VALUES IN ('Beijing', 'Tianjin', 'Hebei');
CREATE TABLE sales_2023_east PARTITION OF sales_2023
FOR VALUES IN ('Shanghai', 'Jiangsu', 'Zhejiang');
CREATE TABLE sales_2023_south PARTITION OF sales_2023
FOR VALUES IN ('Guangdong', 'Fujian', 'Hainan');
-- 2024年分区
CREATE TABLE sales_2024_north PARTITION OF sales_2024
FOR VALUES IN ('Beijing', 'Tianjin', 'Hebei');
CREATE TABLE sales_2024_east PARTITION OF sales_2024
FOR VALUES IN ('Shanghai', 'Jiangsu', 'Zhejiang');
CREATE TABLE sales_2024_south PARTITION OF sales_2024
FOR VALUES IN ('Guangdong', 'Fujian', 'Hainan');
-- 插入测试数据
INSERT INTO sales_detailed (sale_date, customer_id, product_id, quantity, unit_price, total_amount, region, city)
VALUES
('2023-01-15', 1001, 2001, 2, 50.00, 100.00, 'Beijing', 'Beijing'),
('2023-06-20', 1002, 2002, 1, 75.00, 75.00, 'Shanghai', 'Shanghai'),
('2024-03-10', 1003, 2003, 3, 30.00, 90.00, 'Guangdong', 'Guangzhou'),
('2024-08-25', 1004, 2004, 1, 120.00, 120.00, 'Beijing', 'Beijing');
-- 验证多级分区数据分布
SELECT
tableoid::regclass AS partition_name,
count(*) as record_count
FROM sales_detailed
GROUP BY partition_name
ORDER BY partition_name;
1.5.3 多级分区使用模式
查询模式:
sql
-- 精确时间+地区查询 - 只扫描一个叶子分区
SELECT * FROM sales_detailed
WHERE sale_date = '2023-06-20' AND region = 'Shanghai';
-- 时间范围+单地区查询 - 扫描多个年份的同一地区分区
SELECT * FROM sales_detailed
WHERE sale_date BETWEEN '2023-01-01' AND '2024-12-31'
AND region = 'Beijing';
-- 单年份+多地区查询 - 扫描同一年份的多个地区分区
SELECT * FROM sales_detailed
WHERE sale_date BETWEEN '2023-01-01' AND '2023-12-31'
AND region IN ('Beijing', 'Shanghai');
1.6 分区管理操作
1.6.1 分区维护语法结构
完整语法:
sql
-- 添加新分区语法
CREATE TABLE new_partition PARTITION OF table_name
FOR VALUES ...; -- 根据分区类型使用相应的VALUES子句
-- 分离分区语法
ALTER TABLE table_name DETACH PARTITION partition_name;
-- 附加分区语法
ALTER TABLE table_name ATTACH PARTITION partition_name
FOR VALUES ...; -- 根据分区类型使用相应的VALUES子句
-- 删除分区语法
DROP TABLE partition_name;
1.6.2 分区维护实战案例
SQL 示例演示:
sql
-- 为范围分区表添加新分区
CREATE TABLE sales_2023_q1 PARTITION OF sales
FOR VALUES FROM ('2023-01-01') TO ('2023-04-01');
-- 分离分区进行归档
ALTER TABLE sales DETACH PARTITION sales_2022_q1;
-- 分离的分区变成独立表,可以进行备份
-- 备份完成后删除分区
DROP TABLE sales_2022_q1;
-- 为列表分区表添加新地区
CREATE TABLE customers_taiwan PARTITION OF customers
FOR VALUES IN ('Taiwan');
-- 哈希分区扩展示例(需要创建新表并迁移数据)
CREATE TABLE user_sessions_new (
LIKE user_sessions INCLUDING ALL
) PARTITION BY HASH (user_id);
-- 创建更多分区
CREATE TABLE user_sessions_new_p0 PARTITION OF user_sessions_new FOR VALUES WITH (MODULUS 16, REMAINDER 0);
CREATE TABLE user_sessions_new_p1 PARTITION OF user_sessions_new FOR VALUES WITH (MODULUS 16, REMAINDER 1);
-- ... 创建其余分区
-- 迁移数据到新表
INSERT INTO user_sessions_new SELECT * FROM user_sessions;
-- 切换表名
ALTER TABLE user_sessions RENAME TO user_sessions_old;
ALTER TABLE user_sessions_new RENAME TO user_sessions;
DROP TABLE user_sessions_old;
1.6.3 分区信息查询
SQL 示例演示:
sql
-- 查看所有分区表
SELECT
schemaname,
tablename,
tableowner
FROM pg_tables
WHERE tablename IN (
SELECT DISTINCT inhparent::regclass::text
FROM pg_inherits
)
ORDER BY schemaname, tablename;
-- 查看分区表的分区信息
SELECT
child.relname AS partition_name,
parent.relname AS parent_table,
pg_get_expr(child.relpartbound, child.oid) AS partition_bound
FROM pg_inherits i
JOIN pg_class parent ON i.inhparent = parent.oid
JOIN pg_class child ON i.inhrelid = child.oid
WHERE parent.relname = 'sales'
ORDER BY partition_name;
-- 查看分区数据分布
SELECT
tableoid::regclass AS partition_name,
COUNT(*) AS row_count,
PG_SIZE_PRETTY(PG_TOTAL_RELATION_SIZE(tableoid)) AS partition_size
FROM sales
GROUP BY partition_name
ORDER BY partition_name;
本节为每种分区类型提供了完整的语法结构和实际案例,帮助学员全面掌握分区表的创建、管理和使用。每种分区类型都包含了详细的使用模式说明,展示了在不同查询场景下的应用方式。
1.7 索引基础概念
1.7.1 什么是索引?
概念解释:
索引是数据库中一种特殊的数据结构,它能够显著提高数据检索速度。索引类似于书籍的目录,通过维护特定列或表达式的有序引用,让数据库能够快速定位到所需数据,而不需要扫描整个表。
索引的核心价值:
-
性能提升:大幅加快数据查询速度
-
资源优化:减少磁盘 I/O 操作和 CPU 消耗
-
约束保证:支持唯一性约束和主键约束
-
排序优化:加速 ORDER BY 和 GROUP BY 操作
索引的工作原理:
当执行查询时,PostgreSQL 的查询规划器会检查可用的索引,如果存在合适的索引,就会使用索引来快速定位数据,而不是执行全表扫描。这种机制对于大型表尤其重要。
1.7.2 索引的类型概述
概念解释:
PostgreSQL 支持多种类型的索引,每种索引都有其特定的适用场景和优势。了解不同类型的索引有助于选择最适合业务需求的索引策略。
主要索引类型:
-
B-tree 索引:最常用的索引类型,适用于等值查询和范围查询。
-
Hash 索引:适用于简单的等值查询。
-
GiST 索引:通用搜索树,支持地理数据和全文搜索。
-
GIN 索引:通用倒排索引,适用于数组、JSON 和全文搜索。
-
SP-GiST 索引:空间分区搜索树,支持非平衡数据结构。
-
BRIN 索引:块范围索引,适用于有序的大表。
PostgreSQL 6 种原生索引的典型应用场景速查表
| 索引类型 | 一句话定位 | 典型查询/数据类型 | 典型表规模 | 写放大 | 最不适用的场景 |
|---|---|---|---|---|---|
| B-tree | OLTP 万能索引 | =、>、<、BETWEEN、ORDER BY、'abc%' 前缀 LIKE;整数、日期、短文本 | 任意 | 低 | '%abc' 后模糊、全文检索、超长键值高并发写 |
| Hash | 内存敏感的小表等值 | 仅 "=";短文本、整数 | 小表(<百万) | 低 | 范围、排序、组合条件、LIKE |
| GiST | 空间/范围/相似度 | ST_Contains、ST_DWithin、<@、 cube、trigram 相似度 | 中到大 | 中 | 精确等值、高并发追加写 |
| GIN | 全文搜索/数组/JSON | to_tsvector @@、数组 @>、jsonb @>、? | 、?& | 中到超大 | 高(可 fastupdate) |
| SP-GiST | 非平衡空间/电话/IP | IP range、quad-tree、kd-tree、邮编区间 | 中 | 中 | 高并发顺序插入、纯等值 |
| BRIN | 顺序大表"时间线" | 时间/序列范围扫描;块级 min/max | 超大(亿级以上) | 极低 | 随机更新、小表、非顺序数据 |
使用顺序建议 :
(1)默认先上 B-tree,覆盖 90% 场景。
(2)出现"全文、数组、JSONB" → 直接 GIN。
(3)出现"地理/几何/范围" → GiST(PostGIS 默认)。
(4)表>10 亿且时间顺序 → 加 BRIN 做"粗粒度"索引,与 B-tree 互补。
(5)其余两类(Hash、SP-GiST)只在特定业务(IP 段、内存极限小表)才考虑。
1.8 B-tree 索引详解
1.8.1 B-tree 索引概念
**概念解释:**B-tree(平衡树)索引是 PostgreSQL 中最常用和默认的索引类型。它是一种自平衡的树状数据结构,能够保持数据有序,并支持高效的等值查询、范围查询和排序操作。
B-tree 索引特点:
-
保持数据有序存储
-
支持等值查询(=)和范围查询(>, <, BETWEEN)
-
支持多列索引(复合索引)
-
支持唯一性约束
-
适用于大多数业务场景
1.8.2 创建 B-tree 索引
完整语法:
sql
CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] index_name ]
ON table_name [ USING btree ]
( column_name [ ASC | DESC ] [ NULLS { FIRST | LAST } ] [, ...] )
[ INCLUDE ( column_name [, ...] ) ]
[ WITH ( storage_parameter = value [, ... ] ) ]
[ TABLESPACE tablespace_name ]
[ WHERE predicate ]
SQL 示例演示:
sql
-- 创建测试表
CREATE TABLE hr.employeesssss (
employee_id SERIAL PRIMARY KEY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
email VARCHAR(100) UNIQUE,
department_id INTEGER,
salary DECIMAL(10,2),
hire_date DATE,
status VARCHAR(20) DEFAULT 'ACTIVE'
);
-- 插入测试数据
INSERT INTO hr.employeesssss (first_name, last_name, email, department_id, salary, hire_date)
SELECT
'First' || n,
'Last' || n,
'user' || n || '@company.com',
(n % 5) + 1,
50000 + (n % 500) * 100,
CURRENT_DATE - (n % 1000)
FROM generate_series(1, 10000) n;
CREATE INDEX idx_employees_last_name ON hr.employeesssss (last_name); -- 1. 创建基本的 B-tree 索引
CREATE UNIQUE INDEX idx_employees_email ON hr.employeesssss (email); -- 2. 创建唯一索引
CREATE INDEX idx_employees_dept_salary ON hr.employeesssss (department_id, salary); -- 3. 创建多列索引(复合索引)
CREATE INDEX idx_employees_hire_date_desc ON hr.employeesssss (hire_date DESC); -- 4. 创建包含排序方向的索引
CREATE INDEX idx_employees_lower_email ON hr.employeesssss (LOWER(email)); -- 5. 创建函数索引
CREATE INDEX idx_employees_active ON hr.employeesssss (department_id) WHERE status = 'ACTIVE'; -- 6. 创建部分索引(只索引活跃员工)
-- 查看索引信息
SELECT
indexname,
indexdef
FROM pg_indexes
WHERE tablename = 'employeesssss'
ORDER BY indexname;
1.8.3 B-tree 索引使用场景
SQL 示例演示:
sql
-- 1. 等值查询 - 使用索引
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM employees WHERE last_name = 'Last100';
-- 2. 范围查询 - 使用索引
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM employees
WHERE hire_date BETWEEN '2023-01-01' AND '2023-12-31';
-- 3. 排序操作 - 使用索引
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM employees
ORDER BY hire_date DESC
LIMIT 10;
-- 4. 多列索引使用
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM employees
WHERE department_id = 3 AND salary > 60000;
-- 5. 函数索引使用
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM employees
WHERE LOWER(email) = LOWER('USER500@COMPANY.COM');
-- 6. 部分索引使用
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM employees
WHERE department_id = 2 AND status = 'ACTIVE';
1.9 高级索引类型
1.9.1 Hash 索引
**概念解释:**Hash 索引基于哈希表实现,专门用于等值查询(=)。它通过哈希函数将索引键值映射到特定的存储位置,从而实现快速的数据查找。
Hash 索引特点:
-
只支持等值查询,不支持范围查询。
-
通常比 B-tree 索引更小。
-
在 PostgreSQL 10 之后支持 WAL 日志,可用于复制。
-
适用于经常进行等值查询的列。
SQL 示例演示:
sql
-- 创建测试表
CREATE TABLE user_sessions (
session_id UUID DEFAULT gen_random_uuid(),
user_id INTEGER NOT NULL,
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ip_address INET
);
-- 插入测试数据
INSERT INTO user_sessions (user_id, ip_address)
SELECT
(random() * 1000)::integer,
'192.168.1.' || (random() * 255)::integer
FROM generate_series(1, 5000);
-- 创建 Hash 索引
CREATE INDEX idx_user_sessions_user_id_hash ON user_sessions USING hash (user_id);
-- Hash 索引查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM user_sessions WHERE user_id = 123;
-- 比较 B-tree 和 Hash 索引
CREATE INDEX idx_user_sessions_user_id_btree ON user_sessions (user_id);
-- 查看索引大小
SELECT
indexname,
pg_size_pretty(pg_relation_size(indexname::regclass)) as size
FROM pg_indexes
WHERE tablename = 'user_sessions'
ORDER BY indexname;
1.10 GiST 索引
**概念解释:**GiST(Generalized Search Tree)通用搜索树索引是一种可扩展的索引框架,支持多种数据类型的搜索操作,特别适合地理数据、范围数据和全文搜索。
GiST 索引应用场景:
-
几何数据类型(point, line, circle, polygon)
-
范围类型(int4range, daterange, tsrange)
-
全文搜索
-
网络地址类型
SQL 示例演示:
sql
-- 创建地理数据表
CREATE TABLE locations (
location_id SERIAL PRIMARY KEY,
place_name VARCHAR(100),
coordinates POINT,
address_range INT4RANGE
);
-- 插入测试数据
INSERT INTO locations (place_name, coordinates, address_range)
SELECT
'Location ' || n,
point((random() * 180 - 90), (random() * 360 - 180)),
int4range((n % 100) * 10, (n % 100) * 10 + 50)
FROM generate_series(1, 1000) n;
-- 创建 GiST 索引
CREATE INDEX idx_locations_coordinates_gist ON locations USING gist (coordinates);
CREATE INDEX idx_locations_address_range_gist ON locations USING gist (address_range);
-- GiST 索引查询 - 距离查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM locations
WHERE coordinates <-> point(0, 0) < 10;
-- GiST 索引查询 - 范围包含
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM locations
WHERE address_range @> 150;
-- 创建全文搜索表
CREATE TABLE documents (
doc_id SERIAL PRIMARY KEY,
title VARCHAR(200),
content TEXT,
search_vector TSVECTOR
);
-- 生成搜索向量
UPDATE documents SET search_vector = to_tsvector('english', title || ' ' || content);
-- 创建 GiST 全文搜索索引
CREATE INDEX idx_documents_search_gist ON documents USING gist (search_vector);
-- 全文搜索查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT title FROM documents
WHERE search_vector @@ to_tsquery('english', 'database & performance');
1.11 GIN 索引
**概念解释:**GIN(Generalized Inverted Index)通用倒排索引适用于包含多个值的数据类型,如数组、JSONB 和全文搜索向量。它特别适合处理"包含"查询。
GIN 索引应用场景:
-
JSONB 数据类型
-
数组数据类型
-
全文搜索(TSVECTOR)
-
范围查询
SQL 示例演示:
sql
-- 创建 JSONB 数据表
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
product_name VARCHAR(100),
attributes JSONB,
tags TEXT[]
);
-- 插入测试数据
INSERT INTO products (product_name, attributes, tags)
SELECT
'Product ' || n,
jsonb_build_object(
'category', CASE n % 5 WHEN 0 THEN 'electronics' WHEN 1 THEN 'clothing' WHEN 2 THEN 'books' WHEN 3 THEN 'home' ELSE 'sports' END,
'price', (50 + (n % 450))::text,
'specs', jsonb_build_object('color', 'red', 'size', 'medium')
),
ARRAY['tag' || (n % 10), 'category' || (n % 5)]
FROM generate_series(1, 1000) n;
-- 创建 GIN 索引
CREATE INDEX idx_products_attributes_gin ON products USING gin (attributes);
CREATE INDEX idx_products_tags_gin ON products USING gin (tags);
-- JSONB 查询 - 键存在检查
EXPLAIN (ANALYZE, BUFFERS)
SELECT product_name FROM products
WHERE attributes ? 'category';
-- JSONB 查询 - 键值匹配
EXPLAIN (ANALYZE, BUFFERS)
SELECT product_name FROM products
WHERE attributes @> '{"category": "electronics"}';
-- 数组查询 - 包含检查
EXPLAIN (ANALYZE, BUFFERS)
SELECT product_name FROM products
WHERE tags @> ARRAY['tag5'];
-- 数组查询 - 重叠检查
EXPLAIN (ANALYZE, BUFFERS)
SELECT product_name FROM products
WHERE tags && ARRAY['tag3', 'tag7'];
-- 创建多列 GIN 索引
CREATE INDEX idx_products_attrs_tags_gin ON products USING gin (attributes, tags);
1.12 BRIN 索引
概念解释: BRIN(Block Range Index)块范围索引是一种为大型表设计的轻量级索引。它不索引单个行,而是索引连续的数据块范围,非常适合物理存储顺序与逻辑顺序一致的大型表。
BRIN 索引特点:
-
索引尺寸非常小
-
维护成本低
-
适用于按时间顺序存储的数据
-
对于范围查询性能优秀
SQL 示例演示:
sql
-- 创建时间序列数据表
CREATE TABLE sensor_readings (
reading_id BIGSERIAL PRIMARY KEY,
sensor_id INTEGER,
reading_value DECIMAL(8,2),
reading_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入测试数据(按时间顺序)
INSERT INTO sensor_readings (sensor_id, reading_value, reading_time)
SELECT
(n % 100) + 1,
random() * 100,
CURRENT_TIMESTAMP - (n * interval '1 minute')
FROM generate_series(1, 100000) n;
-- 创建 BRIN 索引
CREATE INDEX idx_sensor_readings_time_brin ON sensor_readings USING brin (reading_time);
-- 查看索引大小比较
CREATE INDEX idx_sensor_readings_time_btree ON sensor_readings (reading_time);
SELECT
indexname,
pg_size_pretty(pg_relation_size(indexname::regclass)) as size
FROM pg_indexes
WHERE tablename = 'sensor_readings';
-- BRIN 索引查询 - 时间范围
EXPLAIN (ANALYZE, BUFFERS)
SELECT COUNT(*) FROM sensor_readings
WHERE reading_time BETWEEN CURRENT_TIMESTAMP - interval '1 day' AND CURRENT_TIMESTAMP;
-- 带参数的 BRIN 索引
CREATE INDEX idx_sensor_readings_time_brin_params ON sensor_readings USING brin (reading_time)
WITH (pages_per_range = 16);
-- 比较不同 pages_per_range 参数的效果
CREATE INDEX idx_sensor_readings_time_brin_8 ON sensor_readings USING brin (reading_time)
WITH (pages_per_range = 8);
CREATE INDEX idx_sensor_readings_time_brin_32 ON sensor_readings USING brin (reading_time)
WITH (pages_per_range = 32);
1.13 索引优化策略
1.13.1 索引选择策略
**概念解释:**选择合适的索引类型和列对于查询性能至关重要。不同的查询模式和数据分布需要不同的索引策略。
索引选择原则:
-
高基数列更适合索引
-
经常出现在 WHERE 子句中的列应该建立索引
-
多列查询考虑复合索引
-
避免在频繁更新的列上建立过多索引
SQL 示例演示:
sql
-- 分析表的数据分布
SELECT
column_name,
data_type,
COUNT(*) as total_rows,
COUNT(DISTINCT column_name) as distinct_values,
ROUND(COUNT(DISTINCT column_name) * 100.0 / COUNT(*), 2) as selectivity
FROM employees
CROSS JOIN LATERAL (VALUES
('employee_id', employee_id::text),
('department_id', department_id::text),
('status', status)
) AS cols(column_name, column_value)
GROUP BY column_name, data_type
ORDER BY selectivity DESC;
-- 分析查询模式
SELECT
query,
calls,
total_time,
mean_time,
rows
FROM pg_stat_statements
WHERE query LIKE '%employees%'
ORDER BY total_time DESC
LIMIT 10;
-- 创建基于查询模式的索引
-- 常见的查询模式:按部门和状态筛选
CREATE INDEX idx_employees_dept_status ON employees (department_id, status);
-- 常见的查询模式:按薪资范围查询
CREATE INDEX idx_employees_salary_range ON employees (salary)
WHERE salary > 50000;
-- 查看索引使用统计
SELECT
indexrelname as index_name,
idx_scan as index_scans,
idx_tup_read as tuples_read,
idx_tup_fetch as tuples_fetched
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan DESC;
1.13.1 复合索引策略
**概念解释:**复合索引(多列索引)是在多个列上创建的索引。复合索引的列顺序非常重要,它决定了索引的可用性。
复合索引 设计原则:
-
将等值查询的列放在前面
-
将范围查询的列放在后面
-
考虑列的基数(唯一性)
-
避免创建过多的复合索引。
SQL 示例演示:
sql
-- 创建测试订单表
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_id INTEGER,
order_date DATE,
status VARCHAR(20),
total_amount DECIMAL(10,2),
region VARCHAR(10)
);
-- 插入测试数据
INSERT INTO orders (customer_id, order_date, status, total_amount, region)
SELECT
(random() * 1000)::integer,
CURRENT_DATE - (random() * 365)::integer,
CASE (random() * 4)::integer
WHEN 0 THEN 'PENDING'
WHEN 1 THEN 'PROCESSING'
WHEN 2 THEN 'SHIPPED'
ELSE 'DELIVERED'
END,
(random() * 1000)::decimal,
CASE (random() * 5)::integer
WHEN 0 THEN 'NORTH'
WHEN 1 THEN 'SOUTH'
WHEN 2 THEN 'EAST'
WHEN 3 THEN 'WEST'
ELSE 'CENTRAL'
END
FROM generate_series(1, 50000) n;
-- 1. 好的复合索引设计
CREATE INDEX idx_orders_customer_status_date ON orders (customer_id, status, order_date);
-- 等值查询使用所有列 - 最优
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE customer_id = 123 AND status = 'DELIVERED' AND order_date >= '2023-01-01';
-- 等值查询使用前两列 - 良好
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE customer_id = 123 AND status = 'DELIVERED';
-- 等值查询使用第一列 - 可用
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE customer_id = 123;
-- 2. 不好的复合索引设计
CREATE INDEX idx_orders_date_status_customer ON orders (order_date, status, customer_id);
-- 只使用状态查询 - 无法使用索引
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE status = 'DELIVERED';
-- 3. 包含索引(Covering Index)
CREATE INDEX idx_orders_covering ON orders (customer_id, order_date)
INCLUDE (total_amount, status);
-- 查询可以使用包含索引避免表访问
EXPLAIN (ANALYZE, BUFFERS)
SELECT customer_id, order_date, total_amount
FROM orders
WHERE customer_id = 123 AND order_date >= '2023-01-01';
1.13.3 部分索引和表达式索引
**概念解释:**部分索引和表达式索引是 PostgreSQL 的高级特性,可以创建更精确和高效的索引。
**部分索引:**只对表中满足特定条件的行创建索引,减少索引大小和维护成本。
**表达式索引:**对表达式的结果创建索引,而不是直接对列值创建索引。
SQL 示例演示:
sql
-- 部分索引示例
-- 只索引活跃订单
CREATE INDEX idx_orders_active ON orders (customer_id, order_date)
WHERE status IN ('PENDING', 'PROCESSING');
-- 查询活跃订单 - 使用部分索引
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE customer_id = 123
AND status IN ('PENDING', 'PROCESSING')
AND order_date >= '2023-06-01';
-- 查询所有订单 - 不使用部分索引
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE customer_id = 123
AND order_date >= '2023-06-01';
-- 表达式索引示例
-- 创建不区分大小写的电子邮件索引
CREATE INDEX idx_employees_email_lower ON employees (LOWER(email));
-- 使用表达式索引查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM employees
WHERE LOWER(email) = LOWER('USER500@COMPANY.COM');
-- 日期部分索引
CREATE INDEX idx_orders_year_month ON orders (EXTRACT(YEAR FROM order_date), EXTRACT(MONTH FROM order_date));
-- 使用日期部分索引查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT COUNT(*) FROM orders
WHERE EXTRACT(YEAR FROM order_date) = 2023
AND EXTRACT(MONTH FROM order_date) = 12;
-- 复杂表达式索引
CREATE INDEX idx_orders_amount_category ON orders (
CASE
WHEN total_amount < 100 THEN 'SMALL'
WHEN total_amount < 500 THEN 'MEDIUM'
ELSE 'LARGE'
END
);
-- 使用复杂表达式索引查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE (CASE
WHEN total_amount < 100 THEN 'SMALL'
WHEN total_amount < 500 THEN 'MEDIUM'
ELSE 'LARGE'
END) = 'MEDIUM';