又一个月过去了,这意味着又到了发布新版本的时候了!
ClickHouse 24.6 版本包含 **23 个新功能** 🎁 **24 项性能优化** 🛷 **59 个错误修复** 🐛
新贡献者
一如既往,我们对 24.6 版本的所有新贡献者表示热烈欢迎!ClickHouse 的受欢迎程度在很大程度上归功于社区的贡献。看到社区的不断发展,我们深感谦卑。
以下是新贡献者的姓名列表
Artem Mustafin,Danila Puzov,Francesco Ciocchetti,Grigorii Sokolik,HappenLee,Kris Buytaert,Lee sungju,Mikhail Gorshkov,Philipp Schreiber,Pratima Patel,Sariel,TTPO100AJIEX,Tim MacDonald,Xu Jia,ZhiHong Zhang,allegrinisante,anonymous,chloro,haohang,iceFireser,morning-color,pn,sarielwxm,wudidapaopao,xogoodnow
提示:如果您好奇我们是如何生成此列表的……点击此处。
您还可以查看演示文稿的幻灯片。
最佳表格排序
由 Igor Markelov 贡献
MergeTree 表中行的物理磁盘顺序由其 ORDER BY 键定义。
提醒一下,ORDER BY 键和相关的物理行顺序有三个用途
- 为范围请求构建稀疏索引(也可以指定为 PRIMARY KEY)。
- 为合并模式定义一个键,例如,对于 Aggregating- 或 ReplacingMergeTree 表。
- 提供一种通过在列文件中共同定位数据来改进压缩的方法。
对于上面提到的第三个用途,此版本引入了一个名为 optimize_row_order
的新设置。此设置仅适用于普通 MergeTree 引擎表。在根据 ORDER BY 列排序后,它会根据剩余列的基数在数据导入过程中自动对表行进行排序,从而确保最佳压缩。
如果数据显示出模式,则诸如 LZ4
和 ZSTD
之类的通用压缩编解码器可以实现最大压缩率。例如,相同值的较长运行通常压缩效果非常好。可以通过按列值对物理存储的行进行排序来实现此类相同值的较长运行,从基数最低的列开始。下图对此进行了说明
因为上图中的行按列值(从基数最低的列开始)在磁盘上排序,所以每个列都有相同值的较长运行。如上所述,这有利于表部分的列文件的压缩率。
作为反例,下图说明了首先按高基数列对磁盘上的行进行排序的效果
因为行首先按高基数列值排序,所以通常不再可能根据其他列的值对行进行排序以创建相同值的较长运行。因此,列文件的压缩率不理想。
使用新的optimize_row_order 设置,ClickHouse 会自动实现最佳数据压缩。ClickHouse 首先按磁盘上的 ORDER BY 列(照常)对行进行排序。此外,对于具有相同 ORDER BY 列值的行的每个范围,都会根据剩余列的值(按范围本地列基数升序排列)对行进行排序
此优化仅适用于在插入时创建的数据部分,而不在部分合并期间应用。由于大多数普通 MergeTree 表的合并只是连接 ORDER BY 键的不重叠范围,因此通常会保留已优化的行顺序。
预计 INSERT 操作将花费更长时间,具体取决于数据特征,时间增加 30-50%。
预计 LZ4
或 ZSTD
的压缩率平均提高 20-40%。
此设置最适合没有 ORDER BY 键或具有低基数 ORDER BY 键的表,即只有几个不同的 ORDER BY 键值的表。具有高基数 ORDER BY 键的表(例如,类型为 DateTime64
的时间戳列)预计不会从此设置中受益。
为了演示此新的优化,我们将 10 亿行公共 PyPI 下载统计数据集加载到一个没有 optimize_row_order
设置的表和一个有该设置的表中。
我们创建没有新设置的表
CREATE OR REPLACE TABLE pypi
(
`timestamp` DateTime64(6),
`date` Date MATERIALIZED timestamp,
`country_code` LowCardinality(String),
`url` String,
`project` String,
`file` Tuple(filename String, project String, version String, type Enum8('bdist_wheel' = 0, 'sdist' = 1, 'bdist_egg' = 2, 'bdist_wininst' = 3, 'bdist_dumb' = 4, 'bdist_msi' = 5, 'bdist_rpm' = 6, 'bdist_dmg' = 7)),
`installer` Tuple(name LowCardinality(String), version LowCardinality(String)),
`python` LowCardinality(String),
`implementation` Tuple(name LowCardinality(String), version LowCardinality(String)),
`distro` Tuple(name LowCardinality(String), version LowCardinality(String), id LowCardinality(String), libc Tuple(lib Enum8('' = 0, 'glibc' = 1, 'libc' = 2), version LowCardinality(String))),
`system` Tuple(name LowCardinality(String), release String),
`cpu` LowCardinality(String),
`openssl_version` LowCardinality(String),
`setuptools_version` LowCardinality(String),
`rustc_version` LowCardinality(String),
`tls_protocol` Enum8('TLSv1.2' = 0, 'TLSv1.3' = 1),
`tls_cipher` Enum8('ECDHE-RSA-AES128-GCM-SHA256' = 0, 'ECDHE-RSA-CHACHA20-POLY1305' = 1, 'ECDHE-RSA-AES128-SHA256' = 2, 'TLS_AES_256_GCM_SHA384' = 3, 'AES128-GCM-SHA256' = 4, 'TLS_AES_128_GCM_SHA256' = 5, 'ECDHE-RSA-AES256-GCM-SHA384' = 6, 'AES128-SHA' = 7, 'ECDHE-RSA-AES128-SHA' = 8, 'AES128-GCM' = 9)
)
Engine = MergeTree
ORDER BY (project);
并将数据加载到该表中。请注意,我们增加了 min_insert_block_size_rows
设置的值以更好地演示优化
INSERT INTO pypi
SELECT
*
FROM s3(
'https://storage.googleapis.com/clickhouse_public_datasets/pypi/file_downloads/sample/2023/{0..61}-*.parquet')
SETTINGS
input_format_null_as_default = 1,
input_format_parquet_import_nested = 1,
min_insert_block_size_bytes = 0,
min_insert_block_size_rows = 60_000_000;
接下来,我们使用新设置创建相同的表
CREATE TABLE pypi_opt
(
`timestamp` DateTime64(6),
`date` Date MATERIALIZED timestamp,
`country_code` LowCardinality(String),
`url` String,
`project` String,
`file` Tuple(filename String, project String, version String, type Enum8('bdist_wheel' = 0, 'sdist' = 1, 'bdist_egg' = 2, 'bdist_wininst' = 3, 'bdist_dumb' = 4, 'bdist_msi' = 5, 'bdist_rpm' = 6, 'bdist_dmg' = 7)),
`installer` Tuple(name LowCardinality(String), version LowCardinality(String)),
`python` LowCardinality(String),
`implementation` Tuple(name LowCardinality(String), version LowCardinality(String)),
`distro` Tuple(name LowCardinality(String), version LowCardinality(String), id LowCardinality(String), libc Tuple(lib Enum8('' = 0, 'glibc' = 1, 'libc' = 2), version LowCardinality(String))),
`system` Tuple(name LowCardinality(String), release String),
`cpu` LowCardinality(String),
`openssl_version` LowCardinality(String),
`setuptools_version` LowCardinality(String),
`rustc_version` LowCardinality(String),
`tls_protocol` Enum8('TLSv1.2' = 0, 'TLSv1.3' = 1),
`tls_cipher` Enum8('ECDHE-RSA-AES128-GCM-SHA256' = 0, 'ECDHE-RSA-CHACHA20-POLY1305' = 1, 'ECDHE-RSA-AES128-SHA256' = 2, 'TLS_AES_256_GCM_SHA384' = 3, 'AES128-GCM-SHA256' = 4, 'TLS_AES_128_GCM_SHA256' = 5, 'ECDHE-RSA-AES256-GCM-SHA384' = 6, 'AES128-SHA' = 7, 'ECDHE-RSA-AES128-SHA' = 8, 'AES128-GCM' = 9)
)
Engine = MergeTree
ORDER BY (project)
SETTINGS optimize_row_order = 1;
并将数据加载到表中
INSERT INTO pypi_opt
SELECT
*
FROM s3(
'https://storage.googleapis.com/clickhouse_public_datasets/pypi/file_downloads/sample/2023/{0..61}-*.parquet')
SETTINGS
input_format_null_as_default = 1,
input_format_parquet_import_nested = 1,
min_insert_block_size_bytes = 0,
min_insert_block_size_rows = 60_000_000;
让我们比较两个表的存储大小和压缩率
SELECT
`table`,
formatReadableQuantity(sum(rows)) AS rows,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed,
formatReadableSize(sum(data_compressed_bytes)) AS compressed,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 0) AS ratio
FROM system.parts
WHERE active AND (database = 'default') AND startsWith(`table`, 'pypi')
GROUP BY `table`
ORDER BY `table` ASC;
┌─table────┬─rows─────────┬─uncompressed─┬─compressed─┬─ratio─┐
1. │ pypi │ 1.01 billion │ 227.97 GiB │ 25.36 GiB │ 9 │
2. │ pypi_opt │ 1.01 billion │ 227.97 GiB │ 17.52 GiB │ 13 │
└──────────┴──────────────┴──────────────┴────────────┴───────┘
如您所见,对于具有新设置的表,数据压缩提高了约 30%。
chDB 2.0
由 Auxten Wang 贡献
chDB 是 ClickHouse 的一个进程内版本,适用于多种语言,最突出的是 Python。它今年早些时候加入了 ClickHouse 家族,并在本周发布了 Python 库的 2.0 测试版。
要安装该版本,您必须在安装期间指定版本,如下所示
pip install chdb==2.0.0b1
在此版本中,ClickHouse 引擎已升级到 24.5 版本,现在可以查询 Pandas DataFrame、Arrow 表和 Python 对象。
让我们首先生成一个包含 1 亿行的 CSV 文件
import pandas as pd
import datetime as dt
import random
rows = 100_000_000
now = dt.date.today()
df = pd.DataFrame({
"score": [random.randint(0, 1_000_000) for _ in range(0, rows)],
"result": [random.choice(['win', 'lose', 'draw']) for _ in range(0, rows)],
"dateOfBirth": [now - dt.timedelta(days = random.randint(5_000, 30_000)) for _ in range(0, rows)]
})
df.to_csv("scores.csv", index=False)
然后,我们可以编写以下代码将数据重新加载到 Pandas DataFrame 中,并使用 chDB 的 Python
表引擎查询 DataFrame
import pandas as pd
import chdb
import time
df = pd.read_csv("scores.csv")
start = time.time()
print(chdb.query("""
SELECT sum(score), avg(score), median(score),
avgIf(score, dateOfBirth > '1980-01-01') as avgIf,
countIf(result = 'win') AS wins,
countIf(result = 'draw') AS draws,
countIf(result = 'lose') AS losses,
count()
FROM Python(df)
""", "Vertical"))
end = time.time()
print(f"{end-start} seconds")
输出如下
Row 1:
──────
sum(score): 49998155002154
avg(score): 499981.55002154
median(score): 508259
avgIf: 499938.84709508
wins: 33340305
draws: 33334238
losses: 33325457
count(): 100000000
0.4595322608947754 seconds
我们可以将 CSV 文件加载到 PyArrow 表中并查询该表
import pyarrow.csv
table = pyarrow.csv.read_csv("scores.csv")
start = time.time()
print(chdb.query("""
SELECT sum(score), avg(score), median(score),
avgIf(score, dateOfBirth > '1980-01-01') as avgIf,
countIf(result = 'win') AS wins,
countIf(result = 'draw') AS draws,
countIf(result = 'lose') AS losses,
count()
FROM Python(table)
""", "Vertical"))
end = time.time()
print(f"{end-start} seconds")
该代码块的输出如下所示
Row 1:
──────
sum(score): 49998155002154
avg(score): 499981.55002154
median(score): 493265
avgIf: 499955.15289763256
wins: 33340305
draws: 33334238
losses: 33325457
count(): 100000000
3.0047709941864014 seconds
我们还可以查询 Python 字典,只要值为列表即可
x = {"col1": [random.uniform(0, 1) for _ in range(0, 1_000_000)]}
print(chdb.query("""
SELECT avg(col1), max(col1)
FROM Python(x)
""", "DataFrame"))
avg(col1) max(col1)
0 0.499888 0.999996
试一试,并告诉我们您的使用体验!
希尔伯特曲线
由 Artem Mustafin 贡献
具有数学背景的用户可能熟悉空间填充曲线的概念。在 24.6 版本中,我们添加了对希尔伯特曲线的支持,以补充现有的莫顿编码函数。这些提供了加速某些查询的潜力,这在时间序列和地理数据中很常见。
空间填充曲线是一种连续曲线,它穿过给定多维空间中的每个点,通常位于单位正方形或立方体内部,从而完全填充该空间。这些曲线对许多人来说都很有吸引力,主要是因为它们挑战了直观的概念,即一维线无法覆盖二维面积或更高维度的体积。最著名的例子包括希尔伯特曲线和更简单(也略早)的佩亚诺曲线,这两者都是通过迭代构建来增加其在空间中的复杂性和密度的。空间填充曲线在数据库中具有实际应用,因为它们提供了一种有效的方法来将多维数据映射到一维,同时保留局部性。
考虑一个二维坐标系,其中一个函数将任何点映射到一维线,同时保留局部性,确保原始空间中靠近的点在线上也保持靠近。这条线使用单个数值对高维数据进行排序。可以通过想象一个二维空间被分成四个象限,每个象限进一步分成四个,最终得到 16 个部分来形象化这一点。这个递归过程虽然在概念上是无限的,但在有限深度(例如深度 3)更容易理解,从而产生一个 8x8 网格,包含 64 个象限。此函数生成的曲线穿过所有象限,保持最终线上原始点的邻近性。
重要的是,我们递归和分割空间的深度越深,线上越多的点在其等效的二维位置上稳定下来。在无限远处,空间被完全“填充”。
用户可能也熟悉术语来自 Delta Lake 等湖泊格式的 Z 顺序 - 也称为莫顿序。这是一种空间填充曲线,它也保留空间局部性,类似于我们上面提到的希尔伯特示例。莫顿序,通过 ClickHouse 中的mortenEncode 函数 支持,没有像希尔伯特曲线那样好地保留局部性,但在某些情况下由于其更简单的计算和实现而更受欢迎。
为了更好地了解空间填充曲线及其与无限数学的关系,我们推荐此视频。
那么,这在优化数据库中的查询方面有什么用处呢?
我们可以有效地将多列编码为单个值,并通过保持数据的空间局部性,使用结果值作为我们表中的排序顺序。这有助于提高范围查询和最近邻搜索的性能,并且当编码的列在范围和分布方面具有相似的属性,并且具有大量不同的值时,效果最佳。空间填充曲线的讨论属性确保高维空间中靠近的且具有相同范围查询匹配的值将具有相似的排序值,并且将是相同粒度的部分。因此,需要读取的粒度更少,查询速度更快!
列之间也不应存在相关性 - ClickHouse 排序键使用的传统字典序在这种情况下更有效,因为按第一列排序将隐式地对第二列进行排序。
虽然数值物联网数据(如时间戳和传感器值)通常满足这些属性,但更直观的示例是地理经度和纬度坐标。让我们看一个简单的示例,说明如何使用希尔伯特编码来加速这些类型的查询。
考虑NOAA 全球历史气候网络数据,其中包含过去 120 年的 10 亿次天气测量。每行是一次测量,对应于一个时间点和一个站点。我们在 2xcore r5.large 实例上执行以下操作。
我们稍微修改了默认模式,添加了 mercator_x
和 mercator_y
列。这些值来自墨卡托投影的经度和纬度值,作为 UDF 实现,允许数据在二维表面上可视化。此投影通常用于将点投影到指定高度和宽度的像素空间中,如以前的博文中所示。我们使用它将我们的坐标从 Float32 投影到无符号整数(使用 4294967295 的最大 Int32 值作为我们的高度和宽度),如我们的hilbertEncode 函数所需。
墨卡托投影是一种流行的投影,常用于地图。它有很多优点;主要的是它在局部范围内保留角度和形状,使其成为导航的绝佳选择。它还有一个优点,即恒定方位角(行进方向)是直线,使导航变得简单。
CREATE OR REPLACE FUNCTION mercator AS (coord) -> (
((coord.1) + 180) * (4294967295 / 360),
(4294967295 / 2) - ((4294967295 * ln(tan((pi() / 4) + ((((coord.2) * pi()) / 180) / 2)))) / (2 * pi()))
)
CREATE TABLE noaa
(
`station_id` LowCardinality(String),
`date` Date32,
`tempAvg` Int32 COMMENT 'Average temperature (tenths of a degrees C)',
`tempMax` Int32 COMMENT 'Maximum temperature (tenths of degrees C)',
`tempMin` Int32 COMMENT 'Minimum temperature (tenths of degrees C)',
`precipitation` UInt32 COMMENT 'Precipitation (tenths of mm)',
`snowfall` UInt32 COMMENT 'Snowfall (mm)',
`snowDepth` UInt32 COMMENT 'Snow depth (mm)',
`percentDailySun` UInt8 COMMENT 'Daily percent of possible sunshine (percent)',
`averageWindSpeed` UInt32 COMMENT 'Average daily wind speed (tenths of meters per second)',
`maxWindSpeed` UInt32 COMMENT 'Peak gust wind speed (tenths of meters per second)',
`weatherType` Enum8('Normal' = 0, 'Fog' = 1, 'Heavy Fog' = 2, 'Thunder' = 3, 'Small Hail' = 4, 'Hail' = 5, 'Glaze' = 6, 'Dust/Ash' = 7, 'Smoke/Haze' = 8, 'Blowing/Drifting Snow' = 9, 'Tornado' = 10, 'High Winds' = 11, 'Blowing Spray' = 12, 'Mist' = 13, 'Drizzle' = 14, 'Freezing Drizzle' = 15, 'Rain' = 16, 'Freezing Rain' = 17, 'Snow' = 18, 'Unknown Precipitation' = 19, 'Ground Fog' = 21, 'Freezing Fog' = 22),
`location` Point,
`elevation` Float32,
`name` LowCardinality(String),
`mercator_x` UInt32 MATERIALIZED mercator(location).1,
`mercator_y` UInt32 MATERIALIZED mercator(location).2
)
ENGINE = MergeTree
ORDER BY (station_id, date)
INSERT INTO noaa SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/noaa/noaa_enriched.parquet') WHERE location.1 < 180 AND location.1 > -180 AND location.2 > -90 AND location.2 < 90
假设我们希望计算由边界框表示的区域的统计数据。例如,让我们计算阿尔卑斯山脉每年每一周的总降雪量和平均积雪深度(也许可以让我们了解滑雪的最佳时间!)。
上面的排序 (station_id, date)
在这里没有提供任何实际好处,会导致对所有行的线性扫描。
WITH
mercator((5.152588, 44.184654)) AS bottom_left,
mercator((16.226807, 47.778548)) AS upper_right
SELECT
toWeek(date) AS week,
sum(snowfall) / 10 AS total_snowfall,
avg(snowDepth) AS avg_snowDepth
FROM noaa
WHERE (mercator_x >= (bottom_left.1)) AND (mercator_x < (upper_right.1)) AND (mercator_y >= (upper_right.2)) AND (mercator_y < (bottom_left.2))
GROUP BY week
ORDER BY week ASC
54 rows in set. Elapsed: 1.449 sec. Processed 1.05 billion rows, 4.61 GB (726.45 million rows/s., 3.18 GB/s.)
┌─week─┬─total_snowfall─┬──────avg_snowDepth─┐
│ 0 │ 0 │ 150.52388947519907 │
│ 1 │ 56.7 │ 164.85788967420854 │
│ 2 │ 44 │ 181.53357163761027 │
│ 3 │ 13.3 │ 190.36173190191738 │
│ 4 │ 25.2 │ 199.41843468092216 │
│ 5 │ 30.7 │ 207.35987294422503 │
│ 6 │ 18.8 │ 222.9651218746731 │
│ 7 │ 7.6 │ 233.50080515297907 │
│ 8 │ 2.8 │ 234.66253449285057 │
│ 9 │ 19 │ 231.94969343126792 │
...
│ 48 │ 5.1 │ 89.46301878043126 │
│ 49 │ 31.1 │ 103.70976325737577 │
│ 50 │ 11.2 │ 119.3421940216704 │
│ 51 │ 39 │ 133.65286953585073 │
│ 52 │ 20.6 │ 138.1020341499629 │
│ 53 │ 2 │ 125.68478260869566 │
└─week─┴─total_snowfall──┴──────avg_snowDepth─┘
对于此类查询,自然的选择可能是按 (mercator_x, mercator_y)
对表进行排序 - 首先按 mercator_x
对数据进行排序,然后按 mercator_y
排序。这实际上通过减少读取的行数(减少到 4200 万行)显著提高了查询性能。
CREATE TABLE noaa_lat_lon
(
`station_id` LowCardinality(String),
…
`mercator_x` UInt32 MATERIALIZED mercator(location).1,
`mercator_y` UInt32 MATERIALIZED mercator(location).2
)
ENGINE = MergeTree
ORDER BY (mercator_x, mercator_y)
--populate from existing
INSERT INTO noaa_lat_lon SELECT * FROM noaa
WITH
mercator((5.152588, 44.184654)) AS bottom_left,
mercator((16.226807, 47.778548)) AS upper_right
SELECT
toWeek(date) AS week,
sum(snowfall) / 10 AS total_snowfall,
avg(snowDepth) AS avg_snowDepth
FROM noaa_lat_lon
WHERE (mercator_x >= (bottom_left.1)) AND (mercator_x < (upper_right.1)) AND (mercator_y >= (upper_right.2)) AND (mercator_y < (bottom_left.2))
GROUP BY week
ORDER BY week ASC
--results omitted for brevity
54 rows in set. Elapsed: 0.197 sec. Processed 42.37 million rows, 213.44 MB (214.70 million rows/s., 1.08 GB/s.)
顺便说一句,经验丰富的 ClickHouse 用户可能会尝试在此处使用
pointInPolygon
函数。不幸的是,它目前没有利用索引,并且导致性能下降。
通过使用 EXPLAIN indexes=1
子句,我们可以看到此键在过滤粒度方面相对有效,将其减少到需要读取的 5172 个。
EXPLAIN indexes = 1
WITH
mercator((5.152588, 44.184654)) AS bottom_left,
mercator((16.226807, 47.778548)) AS upper_right
SELECT
toWeek(date) AS week,
sum(snowfall) / 10 AS total_snowfall,
avg(snowDepth) AS avg_snowDepth
FROM noaa_lat_lon
WHERE (mercator_x >= (bottom_left.1)) AND (mercator_x < (upper_right.1)) AND (mercator_y >= (upper_right.2)) AND (mercator_y < (bottom_left.2))
GROUP BY week
ORDER BY week ASC
但是,上述两级排序不能保证靠近的点位于相同的粒度中。希尔伯特编码应该提供此属性,使我们能够更有效地过滤粒度。为此,我们需要将表的 ORDER BY
修改为 hilbertEncode(mercator_x, mercator_y)
。
CREATE TABLE noaa_hilbert
(
`station_id` LowCardinality(String),
...
`mercator_x` UInt32 MATERIALIZED mercator(location).1,
`mercator_y` UInt32 MATERIALIZED mercator(location).2
)
ENGINE = MergeTree
ORDER BY hilbertEncode(mercator_x, mercator_y)
WITH
mercator((5.152588, 44.184654)) AS bottom_left,
mercator((16.226807, 47.778548)) AS upper_right
SELECT
toWeek(date) AS week,
sum(snowfall) / 10 AS total_snowfall,
avg(snowDepth) AS avg_snowDepth
FROM noaa_hilbert
WHERE (mercator_x >= (bottom_left.1)) AND (mercator_x < (upper_right.1)) AND (mercator_y >= (upper_right.2)) AND (mercator_y < (bottom_left.2))
GROUP BY week
ORDER BY week ASC
--results omitted for brevity
54 rows in set. Elapsed: 0.090 sec. Processed 3.15 million rows, 41.16 MB (35.16 million rows/s., 458.74 MB/s.)
我们的编码将查询性能提高了一半,达到 0.09 秒
,并且仅读取了 300 万行。我们可以通过 EXPLAIN indexes=1
确认更有效的粒度过滤。
为什么不只用希尔伯特或莫顿编码对所有排序键进行编码呢?
首先,mortonEncode 和 hilbertEncode 函数仅限于无符号整数(因此需要在上面使用墨卡托投影)。其次,如前所述,如果列之间存在相关性,则使用空间填充曲线不会带来任何好处,并且会增加插入(和排序)时间开销。此外,如果仅按键中的第一列进行过滤,则经典排序更有效。如果仅按第二列进行过滤,希尔伯特(或莫顿排序)编码平均而言会更快。