【AI大数据工程师特训笔记】第12讲:表分区与索引

目录

[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(分区键值)) % 分区总数

计算步骤:

  1. 对分区键值应用哈希函数,得到一个哈希值。

  2. 取哈希值的绝对值(确保为正数)。

  3. 对分区总数取模运算,得到分区编号(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';
相关推荐
侃谈科技圈1 小时前
破除数据中台落地困境:2026数据治理平台差异化能力与选型决策指南
大数据·人工智能
Litluecat2 小时前
信创迁移:Oracle切换海量数据库,慢sql扫描
数据库·sql·oracle·信创·海量
2601_961194022 小时前
27考研刘晓艳单词pdf
linux·sql·ubuntu·华为·pdf·.net
Elastic 中国社区官方博客2 小时前
Elasticsearch DiskBBQ:使用原生 SIMD Blocks 实现快 40% 的向量评分计算
大数据·人工智能·elasticsearch·搜索引擎·ai·全文检索·diskbbq
暴躁小师兄数据学院3 小时前
【AI大数据工程师特训笔记】第16讲:大数据环境安装
大数据·hadoop·笔记·flink·spark·database
豆豆3 小时前
垂直行业门户网站搭建解决方案与落地实操指南
大数据·cms·pageadmin·自定义模型·垂直门户·行业建站·站群建设
Elastic 中国社区官方博客3 小时前
Kibana:使用 AI Chat 及 MCP 轻松创建 AI 原生仪表板
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·信息可视化
Thomas_YXQ4 小时前
Unity无GC读取图片与网格完整方案
大数据·人工智能·unity·微信·产品运营