java-topology/defects/clickhouse/patch/clickhouse-0001-storage-system-columns-find-in-vector.md

3.6 KiB
Raw Blame History

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);
                 }