PostgreSQL字段类型与创建索引和查询之间的关系,通过这篇文章,我们希望数据库管理员以及开发人员能认识到,在PostgreSQL选择正确的数据类型对于数据处理和查询的优势,更进一步的论证,各个POSTGRESQL数据类型在数据处理中的损耗。
这里我们通过 int,bigint,float,numeric,text等字段来进行相关的测试。
通过下面我们可以看到各个字段在建立索引时的时间。
创建表
插入数据
db1=#
db1=# \d+ t_demo
Table "public.t_demo"
Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description
--------+------------------+-----------+----------+---------+----------+-------------+--------------+-------------
v1 | integer | | | | plain | | |
v2 | bigint | | | | plain | | |
v3 | double precision | | | | plain | | |
v4 | numeric | | | | main | | |
v5 | text | | | | extended | | |
Access method: heap
db1=# \timing
Timing is on.
db1=# CREATE INDEX ON t_demo (v1); -- int
CREATE INDEX
Time: 24008.908 ms (00:24.009)
db1=#
db1=# CREATE INDEX ON t_demo (v2);
CREATE INDEX
Time: 25224.961 ms (00:25.225)
db1=#
db1=#
db1=# CREATE INDEX ON t_demo (v3);
CREATE INDEX
Time: 40735.718 ms (00:40.736)
db1=#
db1=# CREATE INDEX ON t_demo (v4);
CREATE INDEX
Time: 56503.447 ms (00:56.503)
db1=#
db1=#
db1=#
db1=# CREATE INDEX ON t_demo (v5);
CREATE INDEX
Time: 156576.956 ms (02:36.577)
db1=#
表的列的释义
列名 | 数据类型 | 存储方式 | 描述 |
v1 | integer | plain | 整数类型 |
v2 | bigint | plain | 大整数类型 |
v3 | double precision | plain | 浮点数 |
v4 | numeric | main | 高精度数值 |
v5 | text | extended | 文本 |
列名 | 数据类型 | 索引创建耗时(ms) | 索引创建耗时(格式化) |
v1 | integer | 24008.908 | 00:24.009 |
v2 | bigint | 25224.961 | 00:25.225 |
v3 | double precision | 40735.718 | 00:40.736 |
v4 | numeric | 56503.447 | 00:56.503 |
v5 | text | 156576.956 | 02:36.577 |
通过上图我们创建索引的时间可以看出,随着数据的精度提高,创建索引的时间逐步提高。原因是什么,这就要说到CPU的指令集。CPU 在处理数据中使用的是指令集,这就牵扯到CPU硬件本身支持的原始数据类型,如整形,浮点等都是CPU直接支持的原始类型。这与CPU内部的寄存器的宽度对应。
而如POSTGRESQL这样的数据库产品,使用C语言进行编写,C语言针对原始的数据类型设计为直接映射到CPU的原生数据类型。原始的数据类型可以产生编译器高度优化的机器代码。从而利用CPU固有的方式来处理这些数据类型。效率来自于直接性,任何无法直接进行处理的数据类型,都需要进行转换,这就是上面bigint 比 int大一倍,处理的速度并没有慢一倍的原因。
类型类别 | 典型名称 | 常见大小(位/字节) | 有符号范围 | 无符号范围 | 相关 CPU/架构/标准 |
整数 | Byte, octet, char (C99) | 8 位 / 1 字节 | -128 到 +127 | 0 到 255 | x86, C99 |
整数 | x86 word, short, int (C) | 16 位 / 2 字节 | -32,768 到 +32,767 | 0 到 65,535 | x86, C |
整数 | x86 double word, int, long (C) | 32 位 / 4 字节 | -2,147,483,648 到 +2,147,483,647 | 0 到 4,294,967,295 | x86, C |
整数 | x86 quadruple word, long long, long (C) | 64 位 / 8 字节 | -9,223,372,036,854,775,808 到 +9,223,372,036,854,775,807 | 0 到 18,446,744,073,709,551,615 | x86, C |
浮点数 | float | 32 位 / 4 字节 | IEEE 754 单精度 | N/A | IEEE 754 |
浮点数 | double | 64 位 / 8 字节 | IEEE 754 双精度 | N/A | IEEE 754 |
布尔值 | Boolean, bool | 1 位(通常为 1 字节) | N/A | True / False | 多数语言 / 硬件 |
除此以外,在内存中的内存对齐对于建立索引的速度也有影响,如B树的构建是需要在内存中进行建立的,然后在持久化到磁盘中,对数据在内存访问的方式是影响索引建立速度的第二个点,那么什么是内存对齐。
我们画一个图来举一个例子:
内存结构如何存储数据
上图中很明显,在内存中我们的 char和int结构在内存中基于a是一个字节,则会匹配3个pad来进行地址的对齐。上面的案例就是不对齐的情况,而编译器在a 和 b之间进行了对齐。
说完这些,很多人看到这里很可能不知道这篇文章要说什么,按照我一贯的风格,马上就要突变。因为要说到开发,表设计,PostgreSQL的表设计。
在PostgreSQL中我们都明白,基于PG的原理
1 索引越少越少越好
2 索引越小越好
(如果你还不明白为什么,说明你对PG的原理不明晰,不清楚)
那么在明白了原理上面的比较后,我们要做的就是下一步,如何让索引变少,如何让索引变小。
方案 1:
替代法:在很多商业数据库中都这样设计,原因有几个其中最初的目的并不是为了要节省索引的大小,而是杜绝商业的剽窃,提高商业剽窃的难度。所以如果你们注意一些大型的国外的软件厂商特别愿意中这样的方式设计数据库表。
其原理是,设计两张表,一张为真实的metadata表也就是第二张表的信息解释,这里做一个举例。
CREATE TABLE order_status_dict (
status_id SMALLINT PRIMARY KEY,
status_text TEXT NOT NULL,
description TEXT,
is_active BOOLEAN DEFAULT TRUE
);
-- 示例插入数据:
INSERT INTO order_status_dict (status_id, status_text, description) VALUES
(1, '待支付', '用户已下单,尚未付款'),
(2, '已支付', '用户完成支付'),
(3, '已取消', '订单取消'),
(4, '退款中', '退款流程已启动'),
(5, '已退款', '退款完成'),
(6, '已完成', '订单流程已完结');
CREATE TABLE orders (
order_id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
status_id SMALLINT NOT NULL REFERENCES order_status_dict(status_id),
order_created_at TIMESTAMP NOT NULL
);
CREATE INDEX idx_status_id ON orders(status_id);
在一个成熟商业软件公司,这样的设计是一个常规的设计,但我知道大部分的甲方公司的开发并没有这样的意识和开发方式。
2 字段类型缩减法
在一些大量使用float,numeric的表中,可以考虑通过附属列来进行表达比如
12.35,可以变成 列1 1235 列2 2,这个意思就是第一列的数据需要小数位2位。当然我非常理解开发同学对这样的优化的不理解,但大部分情况这样开发系统还是为了防止数据丢失,如果数据被窃取,大部分不明白其中表设计的人是无法看懂数据的真实含义。
这也是我年轻在某个公司见过的开发人员使用的一些方案来避免数据被拿走后就明白其中的含义的方案。
至于其他,建议经常去扫描POSTGRESQL中的无用索引,合并一些索引,能复合的索引就别单独建立了。