一般来说,如果单一索引可以解决问题,就是最好的选择。你不可能把人们所有可能过滤的列都建索引。你只能使用组合索引,尽可能地提高性能。
比如有这样一张表:postal_code、first_name和last_name。电话薄会使用这样的组合索引。你会看到数据根据位置排序。位置相同,再根据名和姓排序。
下表显示,使用三个列的组合索引,可能的操作:
查询 | 可能 | 注释 |
---|---|---|
postal_code = 2700 AND last_name = ‘san’ Yes AND first_name = ‘Li’ | 是 | 索引的理想用例 |
postal_code = 2700 AND last_name = ‘san’ | 是 | 无限制 |
last_name = ‘san’ AND postal_code = 2700 | 是 | 简单地交换条件 |
postal_code = 2700 | 是 | 和在postal_code列上的索引一样,但是组合索引更占空间 |
first_name = ‘Li’ | 是,这是不同的用例 | 不能使用该索引的排序属性。不过,如果是非常宽的表,有很多列,PostgreSQL会扫描整个索引,因为比扫描表更节约成本 |
PostgreSQL也支持函数索引-不是索引值,而是索引函数的输出。
比如下面的例子,可以给id列的余弦建索引:
postgres=# CREATE INDEX idx_cos ON t_random (cos(id));
CREATE INDEX
Time: 3359.743 ms (00:03.360)
postgres=# ANALYZE;
ANALYZE
Time: 1192.569 ms (00:01.193)
当然,只有输出不可变的函数,才可以建索引。比如
postgres=# SELECT age('2010-01-01 10:00:00'::timestamptz);
age
----------------------------------
8 years 11 mons 19 days 14:00:00
(1 行记录)
像age这样的函数,就无法建索引,因为它的输出总在变化。PostgreSQL明确禁止输入相同输出缺不同的函数。
要测试上面的索引,我们写个简单的查询:
postgres=# EXPLAIN SELECT * FROM t_random WHERE cos(id) = 10;
QUERY PLAN
------------------------------------------------------------------------
Index Scan using idx_cos on t_random (cost=0.43..8.45 rows=1 width=9)
Index Cond: (cos((id)::double precision) = '10'::double precision)
(2 行记录)
可以看到,该函数索引被使用了。
索引可以提升速度,但是带来了空间消耗。如果表包含1000万个整数值,对应的索引在逻辑上也包含这1000万个整数和额外的开销。
b-tree包含到每行的指针,我们看看索引消耗了多少空间:
postgres=# \di+
关联列表
架构模式 | 名称 | 类型 | 拥有者 | 数据表 | 大小 | 描述
----------+------------+------+----------+----------+-------+------
public | idx_cos | 索引 | postgres | t_random | 86 MB |
public | idx_id | 索引 | postgres | t_test | 86 MB |
public | idx_name | 索引 | postgres | t_test | 86 MB |
public | idx_random | 索引 | postgres | t_random | 86 MB |
(4 行记录)
我的数据库,为了存储这些索引,用了344 MB。我们和表占用的空间比较一下:
postgres=# \d+
关联列表
架构模式 | 名称 | 类型 | 拥有者 | 大小 | 描述
----------+---------------+--------+----------+------------+------
public | t_random | 数据表 | postgres | 169 MB |
public | t_test | 数据表 | postgres | 169 MB |
public | t_test_id_seq | 序列数 | postgres | 8192 bytes |
(3 行记录)
两张表才用了338 MB。换句话说,我们的索引比表还占用空间。
如果表里只有几个不同的值,可以使用部分索引:
postgres=# DROP INDEX idx_name;
DROP INDEX
postgres=# CREATE INDEX idx_name ON t_test (name)
postgres-# WHERE name NOT IN ('hans', 'paul');
CREATE INDEX
这样,大部分值被排除在外,可以享受一个小而有效的索引:
postgres=# \di+ idx_name
关联列表
架构模式 | 名称 | 类型 | 拥有者 | 数据表 | 大小 | 描述
----------+----------+------+----------+--------+------------+------
public | idx_name | 索引 | postgres | t_test | 8192 bytes |
(1 行记录)
注意,只有排除掉构成表的大部分很频繁的数据才有意义(最少25%)。
建索引很容易。不过,建索引的时候,不能修改表。CREATE INDEX命令会使用SHARE锁,确保不发生变化。对于小表,这没有问题。如果是生产系统中的大表,就是一个很严重的问题了。
一个解决办法是使用CREATE INDEX CONCURRENTLY命令,这样建索引的时候,就可以正常使用表了。
postgres=# CREATE INDEX CONCURRENTLY idx_name2 ON t_test (name);
CREATE INDEX
但是,如果你使用CREATE INDEX CONCURRENTLY命令,PostgreSQL不能保证成功。如果系统中的一些操作和这个建索引的操作相冲突,索引会最终标记为invalid。