Clustering key

更新时间:
复制 MD 格式

This topic describes how to use a clustering key in Hologres to sort data and accelerate queries.

Introduction

Hologres sorts data within files based on a clustering key. This can accelerate range and filter queries on the indexed columns. You must specify the clustering key when you create a table with the following syntax.

-- Syntax supported by Hologres V2.1 and later
CREATE TABLE <table_name> (...) WITH (clustering_key = '[<columnName>[,...]]');
-- Syntax supported by all versions
BEGIN;
CREATE TABLE <table_name> (...);
CALL set_table_property('<table_name>', 'clustering_key', '[<columnName>{:asc} [,...]]');
COMMIT;

Parameters:

Parameter

Description

table_name

The name of the table for which you want to specify a clustering key.

columnName

The name of the column that you want to specify as the clustering key.

Usage recommendations

  • A clustering key is ideal for point and range queries. It significantly improves performance for filter operations, such as queries with WHERE a = 1 or WHERE a > 1 AND a < 5. For optimal point query performance, you can define both a clustering key and a bitmap index on the same columns.

  • A clustering key follows the leftmost matching principle. Therefore, we recommend that you include no more than two columns in a clustering key to ensure it applies to a wide range of queries. The order of columns matters: columns listed first have a higher sorting priority than columns listed later.

  • When you specify a Clustering Key field, you can add :asc after the field name to specify the sort order for the index. The default sort order is asc (ascending). Hologres versions earlier than V2.1 do not support setting the sort order to descending (desc). In these versions, if the sort order was set to descending, the Clustering Key could not be hit, which resulted in poor query performance. Starting from V2.1, you can set the Clustering Key to desc after you enable a specific GUC parameter. However, this feature is supported only for fields of data types such as Text, Char, Varchar, Bytea, and Int. Setting the Clustering Key to desc is not yet supported for fields of other data types.

    set hg_experimental_optimizer_enable_variable_length_desc_ck_filter = on;
  • For a row-oriented table, the clustering key defaults to the primary key. In versions earlier than Hologres V0.9, no clustering key was set by default. If you define a clustering key that is different from the primary key, Hologres creates two sorted copies of the data, one for the primary key and one for the clustering key. This causes data redundancy.

Limits

  • To modify a clustering key, you must create a new table and import the data.

  • The columns specified in a clustering key must be NOT NULL. Hologres versions from V1.3.20 to V1.3.27 supported nullable columns in a clustering key, but this is no longer supported starting from V1.3.28. A nullable clustering key can affect data correctness. If your business requires a nullable clustering key, you can enable it by running the following command before your SQL statement.

    set hg_experimental_enable_nullable_clustering_key = true;
  • Columns with the following data types cannot be used in a clustering key: Float, Float4, Float8, Double, Decimal(Numeric), Json, Jsonb, Bit, Varbit, Money, and Time With Time Zone, or other complex data types.

  • In Hologres versions earlier than V2.1, you cannot set the sort order for an index to descending (desc). If the sort order is set to descending, the clustering key cannot be hit, which results in poor query performance. Starting from V2.1, you can enable the following GUC to set the clustering key to desc. However, this feature is supported only for fields of data types such as Text, Char, Varchar, Bytea, and Int. You cannot set the clustering key to desc for fields of other data types.

    set hg_experimental_optimizer_enable_variable_length_desc_ck_filter = on;
  • For a column-oriented table, no clustering key is defined by default. You must explicitly specify one based on your use case.

  • In Hologres, you can set only one clustering key for each table. This means that when you create a table, you can run the call command only once, not multiple times. For example:

    • Syntax supported by Hologres V2.1 and later:

      -- Correct usage
      CREATE TABLE tbl (
          a int NOT NULL,
          b text NOT NULL
      )
      WITH (
          clustering_key = 'a,b'
      );
      -- Incorrect usage
      CREATE TABLE tbl (
          a int NOT NULL,
          b text NOT NULL
      )
      WITH (
          clustering_key = 'a',
          clustering_key = 'b'
      );
    • Syntax supported by all versions:

      -- Correct usage
      BEGIN;
      CREATE TABLE tbl (a int NOT NULL, b text NOT NULL);
      CALL set_table_property('tbl', 'clustering_key', 'a,b');
      COMMIT;
      -- Incorrect usage
      BEGIN;
      CREATE TABLE tbl (a int NOT NULL, b text NOT NULL);
      CALL set_table_property('tbl', 'clustering_key', 'a');
      CALL set_table_property('tbl', 'clustering_key', 'b');
      COMMIT;

How it works

A clustering key physically sorts data within files. By default, the data is sorted in ascending (asc) order. The following sections explain the concepts of logical and physical layouts for a clustering key.

  • Logical layout

    Queries that use a clustering key follow the leftmost matching principle. If a query's filter conditions do not match the prefix of the clustering key columns, the clustering key cannot accelerate the query. The following example illustrates the logical layout of a clustering key in Hologres.

    Assume you have a table with the columns Name, Date, and Class.

    • If you set the clustering key to (Date), the data in the table is sorted by the Date column.

    • If you set the clustering key to (Class, Date), the data is first sorted by the Class column, and then by the Date column within each class.

    The choice of clustering key columns determines the final sorted layout of the data.逻辑布局

  • Physical storage layout

    The physical storage layout of a clustering key groups related data together.物理布局

These layout principles have the following implications:

  • A clustering key is well-suited for range filters. For example, queries with conditions like WHERE date = '1/1' or WHERE a > '1/1' AND a < '1/5' are significantly accelerated.

  • Queries follow the leftmost matching principle. For example, if a clustering key is defined on columns (a,b,c), a query can be accelerated if it filters on (a,b,c) or (a,b). If the query filters on (a,c), only the condition on a can use the clustering key. If the query filters on (b,c), it cannot use the clustering key at all.

In the following example, the uid,class,date columns form the clustering key.

  • Syntax supported by Hologres V2.1 and later:

    CREATE TABLE clustering_test (
        uid int NOT NULL,
        name text NOT NULL,
        class text NOT NULL,
        date text NOT NULL,
        PRIMARY KEY (uid)
    )
    WITH (
        clustering_key = 'uid,class,date'
    );
    INSERT INTO clustering_test VALUES
    (1,'Alice','1','2022-10-19'),
    (2,'Bob','3','2022-10-19'),
    (3,'Sam','2','2022-10-20'),
    (4,'Joe','2','2022-10-20'),
    (5,'Sue','2','2022-10-18'),
    (6,'Ben','3','2022-10-17'),
    (7,'Tom','3','2022-10-20');
  • Syntax supported by all versions:

    BEGIN;
    CREATE TABLE clustering_test (
      uid int NOT NULL,
      name text NOT NULL,
      class text NOT NULL,
      date text NOT NULL,
      PRIMARY KEY (uid)
    );
    CALL set_table_property('clustering_test', 'clustering_key', 'uid,class,date');
    COMMIT;
    INSERT INTO clustering_test VALUES
    (1,'Alice','1','2022-10-19'),
    (2,'Bob','3','2022-10-19'),
    (3,'Sam','2','2022-10-20'),
    (4,'Joe','2','2022-10-20'),
    (5,'Sue','2','2022-10-18'),
    (6,'Ben','3','2022-10-17'),
    (7,'Tom','3','2022-10-20');
  • Querying only the uid column hits the clustering key.

    SELECT * FROM clustering_test WHERE uid > '3';

    Inspecting the execution plan with the EXPLAIN statement reveals a Cluster Filter operator. This indicates the optimizer used the clustering key to accelerate the query.

    explain SELECT * FROM clustering_test WHERE uid > '3';
    QUERY PLAN
    Gather  (cost=0.00..1.10 rows=4 width=24)
      ->  Exchange (Gather Exchange)  (cost=0.00..1.10 rows=4 width=24)
        ->  Decode  (cost=0.00..1.10 rows=4 width=24)
          ->  Index Scan using holo_index:[1] on clustering_test  (cost=0.00..1.00 rows=4 width=24)
                Cluster Filter: (uid > 3)
    Optimizer: HQO version 1.3.0
  • Querying the uid,class columns hits the clustering key.

    SELECT * FROM clustering_test WHERE uid = '3' AND class >'1' ;

    The execution plan shows a Cluster Filter operator, which indicates the optimizer used the clustering key to accelerate the query.

    explain SELECT * FROM clustering_test WHERE uid = '3' AND class >'1' ;
    QUERY PLAN
    Exchange (Gather Exchange)  (cost=0.00..1.10 rows=1 width=24)
    ->  Decode  (cost=0.00..1.10 rows=1 width=24)
        ->  Index Scan using holo_index:[1] on clustering_test  (cost=0.00..1.00 rows=1 width=24)
              Cluster Filter: ((uid = 3) AND (class > '1'::text))
              Shard Selector(Eagerly):
                ->: l0 [3]
    Optimizer: HQO version 1.3.0
  • Querying the uid,class,date columns hits the clustering key.

    SELECT * FROM clustering_test WHERE uid = '3' AND class ='2' AND date > '2022-10-17';

    By inspecting the execution plan, you can see a Cluster Filter operator, which indicates that the clustering key was hit and the query was accelerated. The execution plan result shows that an Index Scan using holo_index:[1] on clustering_test is used and the Cluster Filter condition is ((uid = 3) AND (class = '2'::text) AND (date > '2022-10-17'::text)). This confirms that the clustering key was correctly used.

  • Querying the uid,date columns does not follow the leftmost matching principle. Therefore, only uid can hit the Clustering Key, and date is processed by using a regular filter.

    SELECT * FROM clustering_test WHERE uid = '3'  AND date > '2022-10-17';

    By viewing the execution plan (EXPLAIN SQL), you can see that in the plan shown below, only the uid column has the Cluster Filter operator.

    explain SELECT * FROM clustering_test WHERE uid = '3'  AND date > '2022-10-17';
    QUERY PLAN
    Exchange (Gather Exchange)  (cost=0.00..1.10 rows=1 width=24)
      -> Decode  (cost=0.00..1.10 rows=1 width=24)
        -> Index Scan using holo_index:[1] on clustering_test  (cost=0.00..1.00 rows=1 width=24)
              Filter: (date > '2022-10-17'::text)
              Cluster Filter: (uid = 3)
            Shard Selector(Eagerly):
              ->: I0 [3]
    Optimizer: HQO version 1.3.0
  • Querying only the class,date columns does not follow the leftmost matching principle. The query cannot leverage the clustering key.

    SELECT * FROM clustering_test WHERE class ='2' AND date > '2022-10-17';

    The absence of the Cluster Filter operator in the execution plan (from EXPLAIN SQL) indicates that the Clustering Key was not used. After you run EXPLAIN, the query plan shows an Index Scan where the date column uses a Filter and the class column uses a Bitmap Filter, neither of which is accelerated by the Clustering Key.

Examples

Example 1: Scenarios where a query can hit a clustering key.

  • Syntax supported by Hologres V2.1 and later:

    CREATE TABLE table1 (
        col1 int NOT NULL,
        col2 text NOT NULL,
        col3 text NOT NULL,
        col4 text NOT NULL
    )
    WITH (
        clustering_key = 'col1,col2'
    );
    -- For the table created above, the following queries can be accelerated.
    -- This query can be accelerated.
    select * from table1 where col1='abc';
    -- This query can be accelerated.
    select * from table1 where col1>'xxx' and col1<'abc';
    -- This query can be accelerated.
    select * from table1 where col1 in ('abc','def');
    -- This query can be accelerated.
    select * from table1 where col1='abc' and col2='def'; 
    -- This query cannot be accelerated.
    select col1,col4 from table1 where col2='def';
  • Syntax supported by all versions:

    begin;
    create table table1 (
      col1 int not null,
      col2 text not null,
      col3 text not null,
      col4 text not null
    );
    call set_table_property('table1', 'clustering_key', 'col1,col2');
    commit;
    -- For the table created above, the following queries can be accelerated.
    -- This query can be accelerated.
    select * from table1 where col1='abc';
    -- This query can be accelerated.
    select * from table1 where col1>'xxx' and col1<'abc';
    -- This query can be accelerated.
    select * from table1 where col1 in ('abc','def');
    -- This query can be accelerated.
    select * from table1 where col1='abc' and col2='def';
    -- This query cannot be accelerated.
    select col1,col4 from table1 where col2='def';

Example 2: The Clustering Key is set to asc/desc.

  • Syntax supported by Hologres V2.1 and later:

    CREATE TABLE tbl (
        a int NOT NULL,
        b text NOT NULL
    )
    WITH (
        clustering_key = 'a:desc,b:asc'
    );
  • Syntax supported by all versions:

    BEGIN;
    CREATE TABLE tbl (
      a int NOT NULL, 
      b text NOT NULL
    );
    CALL set_table_property('tbl', 'clustering_key', 'a:desc,b:asc');
    COMMIT;

Advanced tuning

Unlike the clustering key in traditional databases such as MySQL or SQL Server, sorting in Hologres is performed only within files, not across the entire table. Therefore, performing an ORDER BY operation on the Clustering Key still has a performance cost.

Starting from V1.3, Hologres introduced significant performance optimizations for clustering key use cases. These optimizations provide better performance in the following two scenarios. If your Hologres instance is on a version earlier than 1.3, see Troubleshooting: Upgrade preparation errors or contact us for support by following the instructions in How to get online support?.

  • Use ORDER BY on clustering keys

    In Hologres, data within a file is sorted according to the defined clustering keys. In versions earlier than V1.3, the optimizer could not use the sorted property of clustering keys to generate an optimal execution plan. Additionally, data order could not be guaranteed after passing through a shuffle node due to multi-way merging. This often led to higher computational load and longer query times. Hologres V1.3 optimizes this process. The optimizer can now generate an execution plan that uses the sorted property of clustering keys and preserves the data order across shuffles, which improves query performance. However, note the following points:

    • If a query does not filter on the clustering keys of a table, it defaults to a SeqScan instead of an IndexScan. Only an IndexScan uses the sorted property of clustering keys.

    • The optimizer does not always choose an execution plan that relies on the sort order of the clustering key, as this approach has some overhead. For example, data that is sorted within files might still require additional sorting in memory.

    Example:

    • Syntax supported by Hologres V2.1 and later:

      DROP TABLE IF EXISTS test_use_sort_info_of_clustering_keys;
      CREATE TABLE test_use_sort_info_of_clustering_keys (
          a int NOT NULL,
          b int NOT NULL,
          c text
      )
      WITH (
          distribution_key = 'a',
          clustering_key = 'a,b'
      );
      INSERT INTO test_use_sort_info_of_clustering_keys SELECT i%500, i%100, i::text FROM generate_series(1, 1000) as s(i);
      ANALYZE test_use_sort_info_of_clustering_keys;

      Syntax supported by all versions:

      DROP TABLE if exists test_use_sort_info_of_clustering_keys;
      BEGIN;
      CREATE TABLE test_use_sort_info_of_clustering_keys
      (
                a int NOT NULL,
                b int NOT NULL,
                c text
      );
      CALL set_table_property('test_use_sort_info_of_clustering_keys', 'distribution_key', 'a');
      CALL set_table_property('test_use_sort_info_of_clustering_keys', 'clustering_key', 'a,b');
      COMMIT;
      INSERT INTO test_use_sort_info_of_clustering_keys SELECT i%500, i%100, i::text FROM generate_series(1, 1000) as s(i);
      ANALYZE test_use_sort_info_of_clustering_keys;
    • Query statement:

      explain select * from test_use_sort_info_of_clustering_keys where a > 100  order by a, b;
    • Execution plan comparison

      • The following code shows the execution plan in a version earlier than V1.3, such as V1.1. You can view the plan by running the EXPLAIN statement.

         Sort  (cost=0.00..0.00 rows=797 width=11)
           ->  Gather  (cost=0.00..2.48 rows=797 width=11)
                 Sort Key: a, b
                 ->  Sort  (cost=0.00..2.44 rows=797 width=11)
                       Sort Key: a, b
                       ->  Exchange (Gather Exchange)  (cost=0.00..1.11 rows=797 width=11)
                             ->  Decode  (cost=0.00..1.11 rows=797 width=11)
                                   ->  Index Scan using holo_index:[1] on test_use_sort_info_of_clustering_keys  (cost=0.00..1.00 rows=797 width=11)
                                         Cluster Filter: (a > 100)
      • The following code shows the execution plan in Hologres V1.3.

         Gather  (cost=0.00..1.15 rows=797 width=11)
           Merge Key: a, b
           ->  Exchange (Gather Exchange)  (cost=0.00..1.11 rows=797 width=11)
                 Merge Key: a, b
                 ->  Decode  (cost=0.00..1.11 rows=797 width=11)
                       ->  Index Scan using holo_index:[1] on test_use_sort_info_of_clustering_keys  (cost=0.00..1.01 rows=797 width=11)
                             Order by: a, b
                             Cluster Filter: (a > 100)

      The execution plan for V1.3 is more efficient. It uses the sorted property of the clustering key to directly merge the output. This creates a pipelined execution and prevents slow sorting operations when you work with large datasets. As shown in the comparison, the V1.3 plan replaces the Sort operator with a Merge Key, which indicates a more efficient merge-based process.

  • Use JOIN on clustering keys (Beta)

    Hologres V1.3 introduced the Merge Join operator, which allows the optimizer to leverage the sorted nature of clustering keys, reduce computational load, and improve performance. However, note the following points:

    • This feature is in beta and is disabled by default. To use it, you must enable the following parameter before you execute your query.

      -- Enable merge join
      set hg_experimental_enable_sort_merge_join=on;
    • If a query does not filter on the clustering keys of a table, it defaults to a SeqScan instead of an IndexScan. Only an IndexScan uses the sorted property of clustering keys.

    • The optimizer does not always generate an execution plan based on the sorted order of clustering keys because using this property has some costs. For example, data that is sorted within files might still require additional sorting in memory.

    Example:

    • Syntax supported by Hologres V2.1 and later:

      DROP TABLE IF EXISTS test_use_sort_info_of_clustering_keys1;
      CREATE TABLE test_use_sort_info_of_clustering_keys1 (
          a int,
          b int,
          c text
      )
      WITH (
          distribution_key = 'a',
          clustering_key = 'a,b'
      );
      INSERT INTO test_use_sort_info_of_clustering_keys1 SELECT i % 500, i % 100, i::text FROM generate_series(1, 10000) AS s(i);
      ANALYZE test_use_sort_info_of_clustering_keys1;
      DROP TABLE IF EXISTS test_use_sort_info_of_clustering_keys2;
      CREATE TABLE test_use_sort_info_of_clustering_keys2 (
          a int,
          b int,
          c text
      )
      WITH (
          distribution_key = 'a',
          clustering_key = 'a,b'
      );
      INSERT INTO test_use_sort_info_of_clustering_keys2 SELECT i % 600, i % 200, i::text FROM generate_series(1, 10000) AS s(i);
      ANALYZE test_use_sort_info_of_clustering_keys2;

      Syntax supported by all versions:

      drop table if exists test_use_sort_info_of_clustering_keys1;
      begin;
      create table test_use_sort_info_of_clustering_keys1
      (
        a int,
        b int,
        c text
      );
      call set_table_property('test_use_sort_info_of_clustering_keys1', 'distribution_key', 'a');
      call set_table_property('test_use_sort_info_of_clustering_keys1', 'clustering_key', 'a,b');
      commit;
      insert into test_use_sort_info_of_clustering_keys1 select i%500, i%100, i::text from generate_series(1, 10000) as s(i);
      analyze test_use_sort_info_of_clustering_keys1;
      drop table if exists test_use_sort_info_of_clustering_keys2;
      begin;
      create table test_use_sort_info_of_clustering_keys2
      (
        a int,
        b int,
        c text
      );
      call set_table_property('test_use_sort_info_of_clustering_keys2', 'distribution_key', 'a');
      call set_table_property('test_use_sort_info_of_clustering_keys2', 'clustering_key', 'a,b');
      commit;
      insert into test_use_sort_info_of_clustering_keys2 select i%600, i%200, i::text from generate_series(1, 10000) as s(i);
      analyze test_use_sort_info_of_clustering_keys2;
                    
    • Query statement:

      explain select * from test_use_sort_info_of_clustering_keys1 a join test_use_sort_info_of_clustering_keys2 b on a.a = b.a and a.b=b.b where a.a > 100 and b.a < 300;
    • Execution plan comparison

      • The following code shows the execution plan in a version earlier than V1.3, such as V1.1.

         Gather  (cost=0.00..3.09 rows=4762 width=24)
           ->  Hash Join  (cost=0.00..2.67 rows=4762 width=24)
                 Hash Cond: ((test_use_sort_info_of_clustering_keys1.a = test_use_sort_info_of_clustering_keys2.a) AND (test_use_sort_info_of_clustering_keys1.b = test_use_sort_info_of_clustering_keys2.b))
                 ->  Exchange (Gather Exchange)  (cost=0.00..1.14 rows=3993 width=12)
                       ->  Decode  (cost=0.00..1.14 rows=3993 width=12)
                             ->  Index Scan using holo_index:[1] on test_use_sort_info_of_clustering_keys1  (cost=0.00..1.01 rows=3993 width=12)
                                   Cluster Filter: ((a > 100) AND (a < 300))
                 ->  Hash  (cost=1.13..1.13 rows=3386 width=12)
                       ->  Exchange (Gather Exchange)  (cost=0.00..1.13 rows=3386 width=12)
                             ->  Decode  (cost=0.00..1.13 rows=3386 width=12)
                                   ->  Index Scan using holo_index:[1] on test_use_sort_info_of_clustering_keys2  (cost=0.00..1.01 rows=3386 width=12)
                                         Cluster Filter: ((a > 100) AND (a < 300))
      • The following code shows the execution plan in Hologres V1.3.

          Gather  (cost=0.00..2.88 rows=4762 width=24)
           ->  Merge Join  (cost=0.00..2.46 rows=4762 width=24)
                 Merge Cond: ((test_use_sort_info_of_clustering_keys2.a = test_use_sort_info_of_clustering_keys1.a) AND (test_use_sort_info_of_clustering_keys2.b = test_use_sort_info_of_clustering_keys1.b))
                 ->  Exchange (Gather Exchange)  (cost=0.00..1.14 rows=3386 width=12)
                       Merge Key: test_use_sort_info_of_clustering_keys2.a, test_use_sort_info_of_clustering_keys2.b
                       ->  Decode  (cost=0.00..1.14 rows=3386 width=12)
                             ->  Index Scan using holo_index:[1] on test_use_sort_info_of_clustering_keys2  (cost=0.00..1.01 rows=3386 width=12)
                                   Order by: test_use_sort_info_of_clustering_keys2.a, test_use_sort_info_of_clustering_keys2.b
                                   Cluster Filter: ((a > 100) AND (a < 300))
                 ->  Exchange (Gather Exchange)  (cost=0.00..1.14 rows=3993 width=12)
                       Merge Key: test_use_sort_info_of_clustering_keys1.a, test_use_sort_info_of_clustering_keys1.b
                       ->  Decode  (cost=0.00..1.14 rows=3993 width=12)
                             ->  Index Scan using holo_index:[1] on test_use_sort_info_of_clustering_keys1  (cost=0.00..1.01 rows=3993 width=12)
                                   Order by: test_use_sort_info_of_clustering_keys1.a, test_use_sort_info_of_clustering_keys1.b
                                   Cluster Filter: ((a > 100) AND (a < 300))

      The V1.3 plan uses a Merge Join by leveraging the sorted nature of the clustering key. It performs a sort-merge within each shard before the join, creating an efficient pipeline. This approach avoids potential out-of-memory (OOM) errors that can occur with a Hash Join when one side of the join is too large to fit in memory.

Related documentation

For more information about Data Definition Language (DDL) statements for Hologres internal tables, see the following topics: