3. 函数索引概念:什么是函数索引?与普通索引的区别,创建语法与使用限制
3.1 函数索引的定义
函数索引(Functional Index),又称表达式索引(Expression Index),是 PostgreSQL 中一种特殊的索引类型。它不直接索引表的原始列值,而是索引对一列或多列应用某个函数或表达式后的计算结果。
当查询的 WHERE 条件、ORDER BY 或 JOIN 条件中包含对列的函数运算时,普通索引无法被使用,而函数索引可以精确匹配该表达式,实现索引扫描。
核心思想:将查询中频繁出现的函数表达式“物化”为索引条目,使优化器能够利用索引加速该表达式的等值、范围或排序操作。
3.2 与普通索引的核心区别
| 对比维度 | 普通索引(B-tree 等) | 函数索引 |
|---|---|---|
| 索引键 | 直接存储列原始值 | 存储函数/表达式计算结果 |
| 查询匹配 | 仅匹配 WHERE col = value 等原始列条件 |
匹配 WHERE func(col) = value 等表达式条件 |
| 写入开销 | 写入时直接插入列值 | 写入时需先计算表达式,再插入结果 |
| 适用场景 | 精确查找、范围查询、排序 | 大小写不敏感搜索、日期截断查询、JSON 字段提取、计算列过滤 |
| 索引大小 | 通常较小(存储原始值) | 可能更大(表达式结果可能更长) |
3.3 创建语法
函数索引的创建语法与普通索引几乎相同,只需将列名替换为括号包裹的表达式:
-- 基本语法
CREATE INDEX idx_name ON table_name (expression);
-- 示例 1:对用户名字段创建大小写不敏感的索引
CREATE INDEX idx_users_lower_name ON users (LOWER(name));
-- 示例 2:对订单创建时间按日期截断的索引
CREATE INDEX idx_orders_created_date ON orders (DATE(created_at));
-- 示例 3:对 JSONB 字段中的某个键创建索引
CREATE INDEX idx_users_email ON users ((data->>'email'));
-- 示例 4:多列混合表达式索引
CREATE INDEX idx_full_name ON employees (LOWER(last_name) || ' ' || LOWER(first_name));
注意:表达式必须使用圆括号包裹。如果表达式本身包含括号(如函数调用),外层仍需再加一层括号,如
((data->>'email'))。
3.4 使用限制与注意事项
- 表达式必须稳定(IMMUTABLE):索引使用的函数必须是
IMMUTABLE类型,即对于相同的输入始终返回相同的结果。例如LOWER()、DATE()、ABS()是稳定的;而RANDOM()、CURRENT_TIMESTAMP不允许用于函数索引。 - 查询必须精确匹配表达式:优化器只会在查询的
WHERE条件与索引定义中的表达式完全一致时使用该索引。例如索引定义为LOWER(name),查询写WHERE LOWER(name) = 'alice'会命中索引,但WHERE LOWER(TRIM(name)) = 'alice'不会。 - 索引维护开销:每次
INSERT、UPDATE、DELETE时,数据库都需要重新计算表达式并更新索引,写入性能会受影响。 - 索引大小与类型:函数索引支持 B-tree、Hash、GiST、GIN 等多种索引类型,但默认使用 B-tree。表达式结果的数据类型决定了索引的适用操作(如 B-tree 支持排序和范围查询,Hash 仅支持等值查询)。
- NULL 处理:如果表达式结果为
NULL,该行不会被包含在索引中(B-tree 默认行为),除非使用NULLS NOT DISTINCT选项(PostgreSQL 15+)。
常见陷阱:不要对
WHERE 条件中的函数表达式直接创建普通索引——普通索引无法加速函数运算后的过滤。例如 WHERE DATE(created_at) = '2024-01-01' 必须使用 CREATE INDEX ... ON table (DATE(created_at)) 才能生效。
3.5 函数索引工作原理示意图
3.6 实战验证:函数索引是否生效
使用 EXPLAIN 命令可以验证函数索引是否被查询使用:
-- 创建测试表
CREATE TABLE test_users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
-- 插入测试数据
INSERT INTO test_users (name)
SELECT 'User_' || generate_series(1, 100000);
-- 创建函数索引
CREATE INDEX idx_lower_name ON test_users (LOWER(name));
-- 验证查询计划
EXPLAIN ANALYZE
SELECT * FROM test_users WHERE LOWER(name) = 'user_50000';
-- 预期输出(索引扫描):
-- Index Scan using idx_lower_name on test_users (cost=0.29..8.31 rows=1 width=36)
-- Index Cond: (lower(name) = 'user_50000'::text)
验证要点:如果
EXPLAIN 输出中出现 Index Scan 或 Bitmap Index Scan 且索引名称为你创建的函数索引名,则说明索引生效。如果出现 Seq Scan(全表扫描),请检查查询中的表达式是否与索引定义完全一致(包括空格、大小写、函数嵌套等)。