3.6 KiB
UNDF: UNDF-2026-000000031
clickhouse-0001: StorageSystemColumns linear scan per column for key membership
Severity
HIGH
Location
src/Storages/System/StorageSystemColumns.cpp:241-253 — anonymous find_in_vector lambda called inside column loop
Pattern
SLOW: std::find(names.cbegin(), names.cend(), key) called 4× per column inside a loop over all columns — O(C×K) total
FAST: std::unordered_set<std::string> built once before the loop, .count(key) — O(C) total
Context
StorageSystemColumns::read() populates the system.columns virtual table. For every column in a storage (line 180 loop), it calls find_in_vector up to 4 times — once each for partition key columns, sorting key columns, primary key columns, and sampling columns. Each call is std::find over a Names (i.e. std::vector<String>).
With C columns and K key-list entries, total ops = C × K × 4. For a wide table (C=500, K=10) this is 20,000 string comparisons per system.columns query instead of 500.
system.columns is queried by every ClickHouse client, monitoring tool, schema inspector, and IDE. It is also queried internally during query planning and DESCRIBE TABLE. This is a hot path.
Speedup
20× at C=500, K=10 (typical wide table with compound sort key)
Patch
--- a/src/Storages/System/StorageSystemColumns.cpp
+++ b/src/Storages/System/StorageSystemColumns.cpp
@@ -155,6 +155,16 @@ class ColumnsSource : public ISource
cols_required_for_sampling = metadata_snapshot->getColumnsRequiredForSampling();
}
+ // Build O(1) lookup sets before iterating columns
+ std::unordered_set<std::string> partition_key_set(
+ cols_required_for_partition_key.begin(), cols_required_for_partition_key.end());
+ std::unordered_set<std::string> sorting_key_set(
+ cols_required_for_sorting_key.begin(), cols_required_for_sorting_key.end());
+ std::unordered_set<std::string> primary_key_set(
+ cols_required_for_primary_key.begin(), cols_required_for_primary_key.end());
+ std::unordered_set<std::string> sampling_set(
+ cols_required_for_sampling.begin(), cols_required_for_sampling.end());
+
for (const auto & column : columns)
{
@@ -239,12 +249,10 @@ class ColumnsSource : public ISource
{
- auto find_in_vector = [&key = column.name](const Names& names)
- {
- return std::find(names.cbegin(), names.cend(), key) != names.end();
- };
-
if (columns_mask[src_index++])
- res_columns[res_index++]->insert(find_in_vector(cols_required_for_partition_key));
+ res_columns[res_index++]->insert(partition_key_set.count(column.name) > 0);
if (columns_mask[src_index++])
- res_columns[res_index++]->insert(find_in_vector(cols_required_for_sorting_key));
+ res_columns[res_index++]->insert(sorting_key_set.count(column.name) > 0);
if (columns_mask[src_index++])
- res_columns[res_index++]->insert(find_in_vector(cols_required_for_primary_key));
+ res_columns[res_index++]->insert(primary_key_set.count(column.name) > 0);
if (columns_mask[src_index++])
- res_columns[res_index++]->insert(find_in_vector(cols_required_for_sampling));
+ res_columns[res_index++]->insert(sampling_set.count(column.name) > 0);
}