在 MARS3 中,排序键是决定引擎能否发挥扫描效率、能否长期稳定运行的核心设计点。有序数据 + 可靠的块级元数据可以大幅提升扫描效率。
排序键选得好,数据在 Run 内以及更高层级中会呈现更强的局部性,查询的过滤条件更容易命中连续范围,跳读更有效; 排序键选得不合理,数据分布会更“散”,过滤条件无法收敛扫描范围,系统会表现为“看似有索引/有元数据,但读起来还是像全扫”。
排序键带来的核心收益可以拆成五类
用最常见、过滤效果最强的查询维度来组织数据。实践中,排序键的选择通常来自两个维度的组合:
在排序键中,过滤条件中频繁使用的列应置于更靠前的位置。
WHERE 过滤条件中出现频率最高的列作为排序键的最左前缀。BRIN 索引,排序键中列的出现位置越靠前,其重要性越高 —— 因为该列对索引跳过无关数据的能力影响越大。简而言之,让最常见的查询条件能够尽可能把扫描范围收敛到连续的、有聚集性的区间。
如下是一个真实的客户案例 —— 时序场景,查询样例如下:
SELECT time_bucket_gapfill ('5 min', time) AS bucket_time,
locf (LAST (value, time)) AS last_value,
locf (LAST (quality, time)) AS last_quality,
locf (LAST (flags, time)) AS last_flags
FROM
xxx
WHERE
id = '116812373032966284'
AND type = 'ANA'
AND time >= '2025-11-16 00:00:00.000'
AND time <= '2025-11-16 23:59:59.000'
GROUP BY
bucket_time;
结果表明不同的排序键会对索引扫描性能产生显著影响,其作用原理类似于复合索引的工作机制。

| Order Key | Index | MxVScan | MxVIndexScan |
|---|---|---|---|
| id,type,time | id,type,time | 9s | 100ms |
| id,time | id,time | 9s | 100ms |
| time,id,station,type | id,type,station,time | 14s | 6.8s |
| id,station,type,time | id,station,type,time | 9s | 100ms |
当时间字段被置于首位时,相同 ID 的元组会随时间推移分散存储在存储空间中。因此,索引扫描在获取对应数据块时必须执行大量随机读取操作。 然而,当 ID 置于首位时,所有属于同一 ID 的元组都会存储在相邻位置。这极大地减少了随机 I/O 操作,因为索引扫描只需读取少量连续的块。
写入的数据在 Rowstore 向 Columnstore 转换的时候进行排序,Rowstore 本身是不排序的,如果 Rowstore 上有 BTREE 索引的话,那么 BTREE 索引也是有序的。只要指定排序键,那么:
不指定排序键通常可以少掉这部分,因此写入常更轻;但是排序键也会改变落盘数据的天然规整程度:
CREATE TABLE t_w0_nosort (
id bigint NOT NULL,
k1 bigint NOT NULL, -- 高基数
k2 smallint NOT NULL, -- 低基数
k3 bigint NOT NULL, -- 单调列(用 bigint 模拟递增)
v1 double precision NOT NULL,
v2 double precision NOT NULL,
payload text NOT NULL -- 控制写入体量:建议 256B/1024B
) USING mars3;
CREATE TABLE t_w1_1key (LIKE t_w0_nosort INCLUDING ALL)
USING mars3
ORDER BY (k3);
CREATE TABLE t_w3a_3key (LIKE t_w0_nosort INCLUDING ALL)
USING mars3
ORDER BY (k1, k3, k2);
CREATE TABLE t_w3b_3key (LIKE t_w0_nosort INCLUDING ALL)
USING mars3
ORDER BY (k3, k1, k2);
构建中间数据集
DROP TABLE IF EXISTS t_src_s;
CREATE TABLE t_src_s USING MARS3 AS
SELECT
g AS id,
(hashint8(g)::bigint) AS k1,
(g % 64)::smallint AS k2,
g AS k3,
(g % 1000) * 0.01 AS v1,
(g % 10000) * 0.001 AS v2,
repeat('x', 256) AS payload
FROM generate_series(1, 200000000) g;
TRUNCATE t_w0_nosort;
\timing on
INSERT INTO t_w0_nosort SELECT * FROM t_src_s;
\timing off
TRUNCATE t_w1_1key;
\timing on
INSERT INTO t_w1_1key SELECT * FROM t_src_s;
\timing off
TRUNCATE t_w3a_3key;
\timing on
INSERT INTO t_w3a_3key SELECT * FROM t_src_s;
\timing off
TRUNCATE t_w3b_3key;
\timing on
INSERT INTO t_w3b_3key SELECT * FROM t_src_s;
\timing off
每做完一次测试,重启一下数据库,对比写入时间
adw=# TRUNCATE t_w0_nosort;
TRUNCATE TABLE
adw=# \timing on
Timing is on.
adw=# INSERT INTO t_w0_nosort SELECT * FROM t_src_s;
INSERT 0 200000000
Time: 76859.371 ms (01:16.859)
adw=# \timing off
Timing is off.
adw=# TRUNCATE t_w1_1key;
TRUNCATE TABLE
adw=# \timing on
Timing is on.
adw=# INSERT INTO t_w1_1key SELECT * FROM t_src_s;
INSERT 0 200000000
Time: 82864.008 ms (01:22.864)
adw=# \timing off
Timing is off.
adw=# TRUNCATE t_w3a_3key;
TRUNCATE TABLE
adw=# \timing on
Timing is on.
adw=# INSERT INTO t_w3a_3key SELECT * FROM t_src_s;
INSERT 0 200000000
Time: 106929.500 ms (01:46.930)
adw=# \timing off
Timing is off.
adw=# TRUNCATE t_w3b_3key;
TRUNCATE TABLE
adw=# \timing on
Timing is on.
adw=# INSERT INTO t_w3b_3key SELECT * FROM t_src_s;
INSERT 0 200000000
Time: 83456.346 ms (01:23.456)
adw=# \timing off
写入时间
吞吐:
相对不排序的开销:
把单调列 k3 放在排序键第一列几乎能把多列排序的写入代价压到接近单列排序。 把高基数随机列 k1 放第一列会让写入代价显著上升。
少做排序/比较/维护有序性这件事 → 前台写入代价最低,所以它给出基线吞吐 (2.60M rows/s)。
输入流本来就按 k3 递增,排序键与输入顺序一致 → 大部分时候顺着写,只多一点元数据维护开销,所以只慢 ~8%。
因为排序比较先看 k1,而 k1 是高基数近似随机列 ——这 会把整个写入流在 key 空间里打散:
因为第一排序列 k3 和输入流一致,系统能最大化利用天然有序:
所以它几乎等于 ORDER BY (k3) 的性能 (2.40M vs 2.41M rows/s)。
adw=# \dt+
List of relations
Schema | Name | Type | Owner | Storage | Size | Description
--------+-------------+-------+---------+---------+---------+-------------
public | t_default | table | mxadmin | mars3 | 160 kB |
public | t_sort_bad | table | mxadmin | mars3 | 103 MB |
public | t_sort_good | table | mxadmin | mars3 | 35 MB |
public | t_src_s | table | mxadmin | mars3 | 3040 MB |
public | t_w0_nosort | table | mxadmin | mars3 | 2769 MB |
public | t_w1_1key | table | mxadmin | mars3 | 2764 MB |
public | t_w3a_3key | table | mxadmin | mars3 | 3453 MB |
public | t_w3b_3key | table | mxadmin | mars3 | 2764 MB |
public | testmars3 | table | mxadmin | mars3 | 160 kB |
(9 rows)
返回上一章节:存储引擎原理