All Products
Search
Document Center

ApsaraDB for SelectDB:Step 3: Database table design

Last Updated:Aug 28, 2026

Table schema design directly impacts database performance, maintainability, and scalability. Learn the key table properties in ApsaraDB for SelectDB—data models, partitioning, bucketing, and indexes—and choose the right design for your workload.

Key table properties

Choose SelectDB table properties that match your SelectDB workload.

Table property

Required

Description

References

Data model

Yes

The Unique model enforces primary key uniqueness for flexible, efficient updates.

The Duplicate model appends all rows for high-performance detail analysis.

The Aggregate model pre-aggregates value columns for summary analytics.

Data models

Bucketing

Yes

Distributes data across cluster nodes for parallel processing of large datasets.

Partition

No

Divides a table into sub-tables by fields such as time or region. Enables partition pruning to speed up queries.

Index

No

Accelerates queries by filtering or locating data.

Indexes

Data models

Each data model suits different analytics scenarios. Data models.

Basic concepts

In SelectDB, data is organized in tables. Each table has rows (records) and columns (fields).

Columns are divided into two main types:

  • Key columns: Columns specified by UNIQUE KEY, AGGREGATE KEY, or DUPLICATE KEY in a CREATE TABLE statement.

  • Value columns: All non-key columns.

Model selection guide

SelectDB provides three data models: Unique, Duplicate, and Aggregate.

Important
  • The data model is determined when you create a table and cannot be modified.

  • If you do not specify a data model when you create a table, the Duplicate model is used by default. The first three columns are automatically selected as key columns.

  • In the Unique, Duplicate, and Aggregate models, data is sorted and stored by key columns.

Model type

Characteristics

Scenarios

Disadvantages

Unique

Each row has a unique key.

Duplicate keys overwrite previous value columns with the latest values.

Primary key uniqueness or efficient updates: e-commerce orders, user profiles.

  • Synchronous materialized views support column reordering only, not aggregation.

Duplicate

Allows duplicate key values.

Rows with identical keys are stored together.

High write and query throughput. Retains all raw records: log and billing analytics.

  • Does not support updates to historical data.

Aggregate

Each row has a unique key.

Duplicate keys trigger pre-aggregation of value columns per the method defined at table creation.

Similar to a traditional data warehouse Cube model. Pre-aggregation boosts query performance: website traffic, custom reports.

  • Does not provide optimal support for count(*) queries.

  • The aggregation method for value columns is fixed.

Quick start with models

Unique model

The Unique model keeps only the latest value columns for duplicate keys. Two implementations: Merge on Read (MOR) and Merge on Write (MOW).

MOW is recommended for its maturity and query performance. The alternative is Merge on Read (MOR).

Notes

When creating a Unique model table with MOW:

  • Use UNIQUE KEY to specify the unique primary key fields.

  • Add the property to enable MOW in the PROPERTIES section.

    "enable_unique_key_merge_on_write" = "true"
Example

This creates the orders table with the Unique model, a composite primary key (order_id, order_time), and MOW enabled.

CREATE TABLE IF NOT EXISTS orders
(
    `order_id` LARGEINT NOT NULL COMMENT "Order ID",
    `order_time` DATETIME NOT NULL COMMENT "Order time",
    `customer_id` LARGEINT NOT NULL COMMENT "User ID",
    `total_amount` DOUBLE COMMENT "Total order amount",
    `status` VARCHAR(20) COMMENT "Order status",
    `payment_method` VARCHAR(20) COMMENT "Payment method",
    `shipping_method` VARCHAR(20) COMMENT "Shipping method",
    `customer_city` VARCHAR(20) COMMENT "User's city",
    `customer_address` VARCHAR(500) COMMENT "User's address"
)
UNIQUE KEY(`order_id`, `order_time`)
PARTITION BY RANGE(`order_time`) ()
DISTRIBUTED BY HASH(`order_id`)
PROPERTIES (
    "enable_unique_key_merge_on_write" = "true",
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "DAY",
    "dynamic_partition.start" = "-7",
    "dynamic_partition.end" = "3",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.create_history_partition" = "true",
    "dynamic_partition.buckets" = "16"
);

Duplicate model

The Duplicate model stores rows with identical keys together, with no pre-aggregation or uniqueness constraints.

To record and analyze log data sorted by time, type, and error code, use the Duplicate model. This creates a log table with the Duplicate model, sorted by log_time, log_type, and error_code.

CREATE TABLE IF NOT EXISTS log
(
    `log_time` DATETIME NOT NULL COMMENT "Log time",
    `log_type` INT NOT NULL COMMENT "Log type",
    `error_code` INT COMMENT "Error code",
    `error_msg` VARCHAR(1024) COMMENT "Error details",
    `op_id` BIGINT COMMENT "Owner ID",
    `op_time` DATETIME COMMENT "Processing time"
)
DUPLICATE KEY(`log_time`, `log_type`, `error_code`)
PARTITION BY RANGE(`log_time`) ()
DISTRIBUTED BY HASH(`log_type`)
PROPERTIES (
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "DAY",
    "dynamic_partition.start" = "-7",
    "dynamic_partition.end" = "3",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.create_history_partition" = "true",
    "dynamic_partition.buckets" = "16"
);

Aggregate model

Notes

In the Aggregate model, duplicate keys trigger pre-aggregation of value columns. When creating an Aggregate model table:

  • Use AGGREGATE KEY to specify the key columns. Rows with the same key column values will be aggregated.

  • Specify the aggregation method for the value columns. The following aggregation methods are supported:

    Aggregation type Description
    SUM Calculates the sum across rows. Applicable to numeric values.
    MIN Retains the minimum value. Applicable to numeric values.
    MAX Retains the maximum value. Applicable to numeric values.
    REPLACE Replaces the previous value with the newly imported value. For rows with the same key columns, values are replaced in import order.
    REPLACE_IF_NOT_NULL Same as REPLACE, but skips null values. Specify null (not an empty string) as the column default; otherwise empty strings are overwritten.
    HLL_UNION Aggregates columns of the HyperLogLog (HLL) type using the HLL algorithm.
    BITMAP_UNION Aggregates BITMAP columns using union aggregation.
Example

To track user behavior—last visit time, total cost, max and min dwell time—use the Aggregate model. This creates the user_behavior table. When multiple records share the same key column values (user ID, date, city, age, and gender), the value columns are pre-aggregated:

  • User's last visit time: The maximum value of the last_visit_date field is used.

  • Total User Consumption: The total consumption value aggregated from multiple data records.

  • User's maximum dwell time: The maximum value of the max_dwell_time field is used.

  • User's minimum dwell time: The minimum value of the min_dwell_time field is used.

CREATE TABLE IF NOT EXISTS user_behavior
(
    `user_id` LARGEINT NOT NULL COMMENT "User ID",
    `date` DATE NOT NULL COMMENT "Date and time of data write",
    `city` VARCHAR(20) COMMENT "User's city",
    `age` SMALLINT COMMENT "User's age",
    `sex` TINYINT COMMENT "User's gender",
    `last_visit_date` DATETIME REPLACE DEFAULT "1970-01-01 00:00:00" COMMENT "User's last visit time",
    `cost` BIGINT SUM DEFAULT "0" COMMENT "User's total cost",
    `max_dwell_time` INT MAX DEFAULT "0" COMMENT "User's maximum dwell time",
    `min_dwell_time` INT MIN DEFAULT "99999" COMMENT "User's minimum dwell time"
)
AGGREGATE KEY(`user_id`, `date`, `city`, `age`, `sex`)
PARTITION BY RANGE(`date`) ()
DISTRIBUTED BY HASH(`user_id`)
PROPERTIES (
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "DAY",
    "dynamic_partition.start" = "-7",
    "dynamic_partition.end" = "3",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.create_history_partition" = "true",
    "dynamic_partition.buckets" = "16"
);

Data partitioning overview

SelectDB uses two-layer data partitioning: partitions (logical, smallest management unit) and tablets (physical, smallest operation unit for distribution and movement).

image

Relationship between partitions and tablets
  • A tablet belongs to a single partition. A partition can contain multiple tablets.

  • With partitions, data is first divided by partition rules, then subdivided by bucketing rules within each partition. Without partitions, bucketing rules apply directly to the entire table.

  • During writes, data enters the matching partition first, then distributes to tablets by bucketing rules. Bucketing subdivides partitioned data for even distribution and better query efficiency.

Partitions (Partition)

In SelectDB, partitioning divides table data into independent parts based on user-defined rules, improving query efficiency and simplifying management. Partitioning | Dynamic Partitioning.

Partitioning selection guide

SelectDB supports Range and List partitioning, plus dynamic partitioning for automated management.

Partitioning method

Supported column types

Method to specify partition information

Scenarios

Range

Column types: DATE, DATETIME, TINYINT, SMALLINT, INT, BIGINT, LARGEINT

Supports four syntaxes:

  1. VALUES [...): Defines a left-closed, right-open interval for the partition.

  2. VALUES LESS THAN (...): Defines only the upper bound of the partition. The lower bound is determined by the upper bound of the previous partition.

  3. BATCH RANGE: Creates multiple Range partitions for numeric and time types in a batch. Defines a left-closed, right-open interval for the partitions and sets a step size.

  4. MULTI RANGE: Creates multiple Range partitions in a batch. Defines a left-closed, right-open interval for the partitions.

Suitable for managing data division by intervals. A typical scenario is partitioning by time.

List

Column types: BOOLEAN, TINYINT, SMALLINT, INT, BIGINT, LARGEINT, DATE, DATETIME, CHAR, VARCHAR

Supports using VALUES IN (...) to specify the enumerated values that each partition contains.

Suitable for data management based on existing categories or fixed characteristics of the data. The partition key column is usually an enumerated value, such as partitioning data based on the user's region.

Notes

  • SelectDB tables can be partitioned or non-partitioned. This is set at table creation and cannot be changed. Partitioned tables support adding or deleting partitions later; non-partitioned tables do not.

  • Partition key columns must be key columns. You can specify one or more columns.

  • Regardless of the partition key column's data type, enclose the partition value in double quotation marks ("").

  • Theoretically, there is no upper limit on the number of partitions.

  • When you create partitions, ensure that the value ranges of the partitions do not overlap.

Use partitions

Range partitioning

Range partitioning divides and manages data based on the range of a partition field. It is the most common partitioning method. A typical scenario is to partition data by time. This makes it easier to manage and optimize queries on large amounts of time-series data.

The ultimate goal of partitioning and bucketing is to divide data reasonably. The main criteria for a reasonable partitioning rule are as follows:

  • The data volume of each tablet should be between 1 GB and 10 GB.

  • Determine the partition granularity based on your data management needs. For example, in a log scenario, you typically need to delete historical data daily. In this case, partitioning by day is appropriate.

For log data with time-range queries and daily retention, partition by day using log_time:

CREATE TABLE IF NOT EXISTS log
(
 `log_time` DATETIME NOT NULL COMMENT "Log time",
 `log_type` INT NOT NULL COMMENT "Log type",
 `error_code` INT COMMENT "Error code",
 `error_msg` VARCHAR(1024) COMMENT "Error details",
 `op_id` BIGINT COMMENT "Owner ID",
 `op_time` DATETIME COMMENT "Processing time"
)
DUPLICATE KEY(`log_time`, `log_type`, `error_code`)
PARTITION BY RANGE(`log_time`)
(
 PARTITION `p20240201` VALUES [("2024-02-01"), ("2024-02-02")),
 PARTITION `p20240202` VALUES [("2024-02-02"), ("2024-02-03")),
 PARTITION `p20240203` VALUES [("2024-02-03"), ("2024-02-04"))
)
DISTRIBUTED BY HASH(`log_type`);

View partition information:

SHOW partitions FROM log;
p20240201: [("2024-02-01"), ("2024-02-02"))
p20240202: [("2024-02-02"), ("2024-02-03"))
p20240203: [("2024-02-03"), ("2024-02-04"))

This query hits only partition p20240202: [("2024-02-02"), ("2024-02-03")), skipping the others:

SELECT * FROM orders WHERE order_time = '2024-02-02';

List partitioning

List partitioning groups data by enumerated values. The system prunes non-matching partitions during queries to boost performance.

Choose partition columns used frequently in queries. Distribute data evenly across partitions to avoid skew.

In an e-commerce scenario with large order volumes and frequent city-based queries, partition by customer_city. Assume data is distributed by region:

  • Beijing, Shanghai, and Hong Kong (China): 6 GB

  • New York and San Francisco: 5 GB

  • Tokyo: 5 GB

In this case, you can partition the data as shown in the following example.

CREATE TABLE IF NOT EXISTS orders
(
    `order_id` LARGEINT NOT NULL COMMENT "Order ID",
    `order_time` DATETIME NOT NULL COMMENT "Order time",
    `customer_city` VARCHAR(20) COMMENT "User's city",
    `customer_id` LARGEINT NOT NULL COMMENT "User ID",
    `total_amount` DOUBLE COMMENT "Total order amount",
    `status` VARCHAR(20) COMMENT "Order status",
    `payment_method` VARCHAR(20) COMMENT "Payment method",
    `shipping_method` VARCHAR(20) COMMENT "Shipping method",
    `customer_address` VARCHAR(500) COMMENT "User's address"
)
UNIQUE KEY(`order_id`, `order_time`, `customer_city`)
PARTITION BY LIST(`customer_city`)
(
    PARTITION `p_cn` VALUES IN ("Beijing", "Shanghai", "Hong Kong"),
    PARTITION `p_usa` VALUES IN ("New York", "San Francisco"),
    PARTITION `p_jp` VALUES IN ("Tokyo")
)
DISTRIBUTED BY HASH(`order_id`) BUCKETS 16
PROPERTIES (
    "enable_unique_key_merge_on_write" = "true"
);

View partition information:

SHOW partitions FROM orders;
p_cn: ("Beijing", "Shanghai", "Hong Kong")
p_usa: ("New York", "San Francisco")
p_jp: ("Tokyo")

This query hits only partition p_jp: ("Tokyo"), skipping the others:

SELECT * FROM orders WHERE customer_city = 'Tokyo';

Use dynamic partitioning

Manual partition management becomes tedious as tables grow. SelectDB supports dynamic partitioning rules for automated partition management.

For an e-commerce order table where you filter by time range and archive old orders, specify order_time as the partition key and configure dynamic partitioning in PROPERTIES—for example, daily partitions (dynamic_partition.time_unit), 180-day retention (dynamic_partition.start), and 3-day lookahead (dynamic_partition.end).

Important

In the following statement, the parentheses () at the end of PARTITION BY RANGE(`order_time`) () are not a syntax error. If you want to use dynamic partitioning, these parentheses are required by the syntax.

CREATE TABLE IF NOT EXISTS orders
(
    `order_id` LARGEINT NOT NULL COMMENT "Order ID",
    `order_time` DATETIME NOT NULL COMMENT "Order time",
    `customer_id` LARGEINT NOT NULL COMMENT "User ID",
    `total_amount` DOUBLE COMMENT "Total order amount",
    `status` VARCHAR(20) COMMENT "Order status",
    `payment_method` VARCHAR(20) COMMENT "Payment method",
    `shipping_method` VARCHAR(20) COMMENT "Shipping method",
    `customer_city` VARCHAR(20) COMMENT "User's city",
    `customer_address` VARCHAR(500) COMMENT "User's address"
)
UNIQUE KEY(`order_id`, `order_time`)
PARTITION BY RANGE(`order_time`) ()
DISTRIBUTED BY HASH(`order_id`)
PROPERTIES (
    "enable_unique_key_merge_on_write" = "true",
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "DAY",
    "dynamic_partition.start" = "-180",
    "dynamic_partition.end" = "3",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.create_history_partition" = "true",
    "dynamic_partition.buckets" = "16"
);

For tables with many partitions, use Dynamic partitioning for automated management.

Bucketing (Tablet)

In SelectDB, data is divided into tablets based on the hash value of a specified column. Tablets are distributed across cluster nodes for parallel processing. Configure bucketing with DISTRIBUTED BY HASH(`<bucketing column>`) BUCKETS <number of buckets>. Bucketing.

Notes

  • With partitions, the DISTRIBUTED... clause defines data division within each partition. Without partitions, it applies to the entire table.

  • You can specify multiple bucketing columns.

    For the Aggregate and Unique models, bucketing columns must be key columns. For the Duplicate model, there is no restriction on bucketing columns.

    Choose high-cardinality columns as bucketing columns to distribute data evenly and avoid data skew.

  • Theoretically, there is no upper limit on the number of tablets.

    Theoretically, there is no upper or lower limit on the data volume of a single tablet, but we recommend a range of 1 GB to 10 GB.

    If a single tablet's data volume is too small, it can lead to too many tablets, which increases the pressure on metadata management.

    If a single tablet's data volume is too large, it hinders replica migration and the full utilization of the distributed cluster. It also increases the cost of retrying failed operations, such as schema changes or index creation, because the granularity of these retries is at the tablet level.

Bucketing column selection guide

Bucketing column selection impacts query performance and concurrency. If requirements conflict, prioritize your primary query pattern.

Selection principle

Effect

Prioritize even data distribution by choosing high-cardinality columns or a combination of columns.

Balanced distribution across nodes. For broad scans, this fully utilizes distributed resources.

Choose columns that are frequently used in query filter conditions to accelerate queries through data pruning.

Rows with the same bucketing column values are grouped. Point queries using the bucketing column prune data quickly, improving concurrency.

Note

Point queries retrieve a small amount of data using specific conditions such as primary key or high-cardinality column filters.

Example

In an e-commerce scenario, most queries filter by order while some perform full-table analytics. Choose the high-cardinality column order_id as the bucketing column to ensure even distribution and group data by order:

CREATE TABLE IF NOT EXISTS orders
(
    `order_id` LARGEINT NOT NULL COMMENT "Order ID",
    `order_time` DATETIME NOT NULL COMMENT "Order time",
    `customer_id` LARGEINT NOT NULL COMMENT "User ID",
    `total_amount` DOUBLE COMMENT "Total order amount",
    `status` VARCHAR(20) COMMENT "Order status",
    `payment_method` VARCHAR(20) COMMENT "Payment method",
    `shipping_method` VARCHAR(20) COMMENT "Shipping method",
    `customer_city` VARCHAR(20) COMMENT "User's city",
    `customer_address` VARCHAR(500) COMMENT "User's address"
)
UNIQUE KEY(`order_id`, `order_time`)
PARTITION BY RANGE(`order_time`) ()
DISTRIBUTED BY HASH(`order_id`)
PROPERTIES (
    "enable_unique_key_merge_on_write" = "true",
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "DAY",
    "dynamic_partition.start" = "-7",
    "dynamic_partition.end" = "3",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.create_history_partition" = "true",
    "dynamic_partition.buckets" = "16"
);

Indexes

Proper indexes improve query performance but use additional storage and reduce write throughput. Index acceleration.

Design guide

  • Specify the most frequent filter columns as key columns for the automatic prefix index. A table can have only one prefix index, so apply it to the most common filter pattern.

  • For other filtering needs, use inverted indexes. They support multiple condition combinations. For string equality and LIKE queries, consider BloomFilter or NGram BloomFilter indexes.

Index selection guide

In SelectDB, indexes are either built-in (created automatically) or custom (created manually at or after table creation).

Creation method

Index type

Supported query types

Unsupported query types

Advantages

Disadvantages

Built-in

Prefix index

  • Equality and inequality queries

  • Range queries

  • LIKE queries

  • MATCH (keyword, phrase)

Low space usage, fully cacheable in memory. Quickly locates data blocks.

A table can have only one prefix index.

Custom

Inverted index (recommended)

  • Equality, inequality, and range queries for string, numeric, and date/time types

  • String MATCH (keyword, phrase)

  • Full-text search for text types

None

Broad query type support. Create on demand at or after table creation.

Higher storage overhead.

BloomFilter index

Equality query

  • Not-equal-to query

  • Range queries

  • LIKE queries

  • MATCH (keyword, phrase)

Index creation uses few computing and storage resources.

Supports few query types. Only supports equality queries.

NGram BloomFilter index

LIKE queries

  • Equality and inequality queries

  • Range queries

  • MATCH (keyword, phrase)

Improves LIKE query speed. Index creation uses few computing and storage resources.

Only accelerates LIKE queries.

Quick start index

Inverted index

SelectDB inverted indexes support full-text search on text fields and equality or range queries on other fields. Inverted indexes.

Create an index when you create a table

To speed up queries by user ID and address keywords, create inverted indexes on customer_id and customer_address:

CREATE TABLE IF NOT EXISTS orders
(
    `order_id` LARGEINT NOT NULL COMMENT "Order ID",
    `order_time` DATETIME NOT NULL COMMENT "Order time",
    `customer_id` LARGEINT NOT NULL COMMENT "User ID",
    `total_amount` DOUBLE COMMENT "Total order amount",
    `status` VARCHAR(20) COMMENT "Order status",
    `payment_method` VARCHAR(20) COMMENT "Payment method",
    `shipping_method` VARCHAR(20) COMMENT "Shipping method",
    `customer_city` VARCHAR(20) COMMENT "User's city",
    `customer_address` VARCHAR(500) COMMENT "User's address",
    INDEX idx_customer_id (`customer_id`) USING INVERTED,
    INDEX idx_customer_address (`customer_address`) USING INVERTED PROPERTIES("parser" = "chinese")
)
UNIQUE KEY(`order_id`, `order_time`)
PARTITION BY RANGE(`order_time`) ()
DISTRIBUTED BY HASH(`order_id`)
PROPERTIES (
    "enable_unique_key_merge_on_write" = "true",
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "DAY",
    "dynamic_partition.start" = "-7",
    "dynamic_partition.end" = "3",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.create_history_partition" = "true",
    "dynamic_partition.buckets" = "16"
);
Create an index on an existing table

To add an inverted index on customer_id to an existing table:

ALTER TABLE orders ADD INDEX idx_customer_id (`customer_id`) USING INVERTED;

Prefix index

A prefix index is built on one or more prefix key columns and relies on the underlying data being sorted by those key columns. It is essentially a binary search based on the sorted nature of the data. Prefix indexes are built-in indexes that SelectDB creates automatically after table creation.

No special syntax defines a prefix index. The system selects fields covered by the first 36 bytes of key columns. A VARCHAR column truncates the prefix index; subsequent key columns are excluded.

Key column order determines the prefix index. Order key columns by these principles:

  • Place high-cardinality key columns that are frequently used for filtering before other fields. For example, in the log scenario in the Duplicate model section, the log time log_time is placed before the error code error_code.

  • Place key columns for equality filters before key columns for range filters. For example, in the e-commerce scenario in the Inverted index section, the time order_time is usually filtered by range and is placed after the order ID order_id.

  • Place regular type fields before VARCHAR type fields. For example, place INT type key columns before VARCHAR type key columns.

Example

In the e-commerce scenario in the Inverted index section, the prefix index for the order information table is order_id+order_time. When the query condition is a prefix of the prefix index (that is, the query condition includes order_id, or both order_id and order_time), the query speed can be significantly increased. As shown in the following two examples, the query in Example 1 is much faster than the query in Example 2.

Example 1

SELECT * FROM orders WHERE order_id = 1829239 and order_time = '2024-02-01';

Example 2

SELECT * FROM orders WHERE order_time = '2024-02-01';

Next steps

With these fundamentals, you can design SelectDB tables for your workload. Explore data migration, external data source queries, and version upgrades in What to do next.