Iceberg external tables (XIHE SQL)

Updated at:
Copy as MD

The XIHE engine of AnalyticDB for MySQL natively supports the Apache Iceberg lake table format. You can use standard SQL to create Iceberg tables and perform data writes and queries.

Prerequisites

  • The cluster edition is Enterprise Edition, Basic Edition, or Data Lakehouse Edition.

  • The kernel version of the cluster is 3.2.3.0 or later.

  • To use the internal lake mode (lake storage managed by AnalyticDB for MySQL), you must submit a ticket to enable the Lake storage feature.

Background

Apache Iceberg is an open data lake table format that supports features such as ACID transactions and partition transforms. Data is stored in Parquet format on OSS, and any Iceberg-compatible compute engine can directly read the data.

AnalyticDB for MySQL supports two storage modes. The storage mode is determined when you create a table and cannot be changed afterward:

Dimension

Internal lake (managed lake storage)

External lake (user-owned OSS)

Storage management

Fully managed by AnalyticDB for MySQL

User-managed OSS bucket

Key table creation parameter

catalog_type='ADB' + adb_lake_bucket

LOCATION 'oss://...'

Activation method

Submit a ticket to apply for activation

No additional operations are required within the same account

Applicable scenario

New projects that require simplified O&M

Existing OSS data or self-managed storage required

Create a table

Syntax

CREATE TABLE [IF NOT EXISTS] <db>.<table> (
    <col1>  <type1>  [COMMENT '<comment>'],
    <col2>  <type2>  [COMMENT '<comment>'],
    ...
)
[COMMENT '<table_comment>']
[PARTITIONED BY (<partition_expr1>[, <partition_expr2>, ...])]
STORED AS ICEBERG
[LOCATION '<oss_path>']
[TBLPROPERTIES (
    '<key1>' = '<value1>',
    ...
)];

Clause

Required

Description

STORED AS ICEBERG

Yes

Declares the table format as Iceberg.

PARTITIONED BY (...)

No

Partition expression. Identity partitioning (using column values directly) and transform function partitioning (year, month, day, hour, bucket, truncate) are supported.

LOCATION

Required for external lake

Points to a user-owned OSS path in the format oss://<bucket>/<path>/. We recommend that the path ends with /<database>/<table>/.

TBLPROPERTIES

Required for internal lake

For the internal lake mode, you must include catalog_type='ADB' and adb_lake_bucket='...'.

The following table properties can be configured in TBLPROPERTIES:

Property

Default value

Description

catalog_type

N/A

Required for the internal lake mode. Set the value to 'ADB'.

adb_lake_bucket

N/A

Required for the internal lake mode. Set the value to the name of the OSS bucket allocated by AnalyticDB for MySQL.

format_version

'2'

The Iceberg format version. Both '2' (default) and '3' are supported. Row-level DELETE requires '3'.

format

'PARQUET'

The storage format of data files. Only 'PARQUET' is supported.

metadata_location

N/A

In external lake mode, if you want to point to existing Iceberg data, specify the OSS path of the metadata.json file.

External lake table example

-- Create a database
CREATE DATABASE IF NOT EXISTS lake_db;

-- Create an external lake Iceberg table
CREATE TABLE lake_db.orders (
    order_id     BIGINT       COMMENT 'Order ID',
    user_id      BIGINT       COMMENT 'User ID',
    status       STRING       COMMENT 'Order status',
    total_amount DECIMAL(18, 2) COMMENT 'Order amount',
    created_at   TIMESTAMP    COMMENT 'Order time'
)
COMMENT 'Orders table'
PARTITIONED BY (dt DATE)
STORED AS ICEBERG
LOCATION 'oss://<your-bucket>/warehouse/lake_db/orders/';
Note

In external lake mode, you specify the path by using LOCATION and do not set catalog_type. We recommend that the LOCATION path ends with /<database>/<table>/, for example, oss://<your-bucket>/warehouse/lake_db/orders/. You do not need to create the OSS directory in advance. The engine automatically creates the directories and metadata files.

Internal lake table example

CREATE TABLE lake_db.orders (
    order_id     BIGINT       COMMENT 'Order ID',
    user_id      BIGINT       COMMENT 'User ID',
    status       STRING       COMMENT 'Order status',
    total_amount DECIMAL(18, 2) COMMENT 'Order amount',
    created_at   TIMESTAMP    COMMENT 'Order time'
)
COMMENT 'Orders table'
PARTITIONED BY (dt DATE)
STORED AS ICEBERG
TBLPROPERTIES (
    'catalog_type'    = 'ADB',
    'adb_lake_bucket' = 'adb-lake-cn-<region>-xxxx'
);
Note

When you create an internal lake table, you do not need to specify LOCATION. The storage path is automatically allocated by AnalyticDB for MySQL. Before you use the internal lake mode, you must enable the lake storage feature. For the value of adb_lake_bucket, see Create data lake tables.

CTAS (CREATE TABLE AS SELECT)

CTAS creates a table and writes the query results at the same time. The column names and types are inferred from the SELECT statement.

CREATE TABLE lake_db.orders_copy
PARTITIONED BY (dt DATE)
STORED AS ICEBERG
LOCATION 'oss://<your-bucket>/warehouse/lake_db/orders_copy/'
AS SELECT * FROM lake_db.orders;

Partition management

Iceberg partitioning is fundamentally different from traditional Hive partitioning:

Dimension

Hive partitioning

Iceberg partitioning

Is the partition a column

The partition column is a separate column.

Partitions can be based on transforms of existing columns (such as day(dt)) and do not occupy an extra column.

Partition awareness during writes

INSERT must specify partition values.

INSERT does not need to specify partition values explicitly. The engine routes data automatically.

Partition awareness during queries

The WHERE clause must include the partition column.

The engine performs partition pruning automatically.

In addition to identity partitioning (using column values directly), Iceberg supports transform function partitioning based on existing columns. You do not need to specify partition values during writes. The engine automatically routes data based on column values.

Function

Applicable types

Example

identity

Any

PARTITIONED BY (dt DATE)

year

DATE / TIMESTAMP

PARTITIONED BY (year(dt))

month

DATE / TIMESTAMP

PARTITIONED BY (month(dt))

day

DATE / TIMESTAMP

PARTITIONED BY (day(created_at))

hour

TIMESTAMP

PARTITIONED BY (hour(created_at))

bucket[N]

Any (commonly used for high-cardinality columns, such as IDs)

PARTITIONED BY (bucket(user_id, 16))

truncate[len]

STRING

PARTITIONED BY (truncate(email, 10))

Note

Partition transforms are not separate columns: day(created_at) does not create a new column. Iceberg records the transformed partition values in manifest files. Too many partitions degrade performance, so for high-cardinality columns, use day() or month() to control the number of partitions.

The following examples show common partitioning scenarios for different data characteristics:

  • Partition by a time dimension (day, most common): Partitioning time-series data by day is the most common choice, balancing query performance and management overhead. When you use day(created_at) for partitioning, you do not need to maintain a separate dt column. Iceberg automatically extracts the date from created_at for partitioning.

    CREATE TABLE lake_db.orders_by_day (
        order_id     BIGINT,
        user_id      BIGINT,
        status       STRING,
        total_amount DECIMAL(18, 2),
        created_at   TIMESTAMP
    )
    PARTITIONED BY (day(created_at))
    STORED AS ICEBERG
    LOCATION 'oss://<your-bucket>/warehouse/lake_db/orders_by_day/';
  • Partition a low-cardinality categorical column (identity): For columns with only a few distinct values (such as market segment or region), partition by the column value directly.

    -- Partition by mktsegment, which has only a few enumerated values. Identity partitioning is a good fit.
    CREATE TABLE lake_db.customer (
        c_custkey     BIGINT,
        c_name        STRING,
        c_acctbal     DECIMAL(15,2),
        c_mktsegment  STRING   -- Low cardinality: 'AUTOMOBILE', 'BUILDING', 'FURNITURE', and so on
    )
    PARTITIONED BY (c_mktsegment)
    STORED AS ICEBERG
    LOCATION 'oss://<your-bucket>/warehouse/lake_db/customer/';
  • Partition high-frequency events by hour (hour): When data is written frequently and requires near real-time monitoring, partition by hour.

    -- Partition by the hour of the receipt timestamp to support near real-time monitoring.
    CREATE TABLE lake_db.lineitem_realtime (
        l_orderkey      BIGINT,
        l_partkey       BIGINT,
        l_quantity      DECIMAL(15,2),
        l_receipttime   TIMESTAMP   -- Receipt time with second-level precision
    )
    PARTITIONED BY (hour(l_receipttime))
    STORED AS ICEBERG
    LOCATION 'oss://<your-bucket>/warehouse/lake_db/lineitem_realtime/';
  • Partition a high-cardinality column by hash bucket (bucket): Hashing a high-cardinality column (such as an ID) into buckets avoids small files and improves JOIN performance.

    -- Hash the high-cardinality foreign key l_partkey into 64 buckets for even data distribution.
    CREATE TABLE lake_db.lineitem (
        l_orderkey      BIGINT,
        l_partkey       BIGINT,      -- High cardinality (about 20 million distinct values)
        l_quantity      DECIMAL(15,2),
        l_shipdate      DATE
    )
    PARTITIONED BY (bucket(l_partkey, 64))
    STORED AS ICEBERG
    LOCATION 'oss://<your-bucket>/warehouse/lake_db/lineitem/';
  • Partition by a string prefix (truncate): Group data by the first several characters of a string. This is useful when records are naturally grouped by a prefix.

    -- Partition by the first 3 characters of the phone number (for example, '13-' for a specific carrier prefix).
    CREATE TABLE lake_db.customer_by_phone (
        c_custkey     BIGINT,
        c_name        STRING,
        c_phone       STRING       -- Format: '13-888-999-1234'
    )
    PARTITIONED BY (truncate(c_phone, 3))
    STORED AS ICEBERG
    LOCATION 'oss://<your-bucket>/warehouse/lake_db/customer_by_phone/';
  • Multi-level composite partitioning: Combine multiple partition transforms to balance the number of partitions against data distribution. This is suitable for querying large datasets.

    -- User event log: partition by day, hash-bucket the high-cardinality ID, and truncate the region prefix (three-level composite partitioning).
    CREATE TABLE lake_db.user_event_log (
        event_id      BIGINT,
        user_id       BIGINT,          -- High-cardinality user ID
        event_type    STRING,
        event_time    TIMESTAMP,       -- Timestamp with second-level precision
        country_code  STRING           -- Country code, such as 'CN', 'US', or 'DE'
    )
    PARTITIONED BY (
        day(event_time),            -- Level 1: partition by day for efficient time-based pruning
        bucket(user_id, 64),        -- Level 2: hash the high-cardinality user_id into 64 buckets to avoid small files
        truncate(country_code, 2)   -- Level 3: truncate the country code to the first 2 characters for regional aggregation
    )
    STORED AS ICEBERG
    LOCATION 'oss://<your-bucket>/warehouse/lake_db/user_event_log/';

Other DDL operations

-- View the complete CREATE TABLE statement
SHOW CREATE TABLE lake_db.orders;

-- View the column structure
DESCRIBE lake_db.orders;

-- Drop a table
DROP TABLE IF EXISTS lake_db.orders;

-- Drop a database (the database must contain no tables, otherwise an error is returned)
DROP DATABASE IF EXISTS lake_db;
Warning

Executing DROP TABLE on an internal lake table permanently deletes both the data files and metadata on OSS. This operation is irreversible. Executing DROP TABLE on an external lake table only deletes the table definition in AnalyticDB for MySQL. The data files on OSS are not affected.

Write data

INSERT INTO (append)

INSERT INTO lake_db.orders
SELECT * FROM VALUES
    (1001, 501, 'paid', 299.90, TIMESTAMP '2026-06-11 10:00:00', DATE '2026-06-11'),
    (1002, 502, 'pending', 158.00, TIMESTAMP '2026-06-11 10:05:00', DATE '2026-06-11'),
    (1003, 503, 'shipped', 450.00, TIMESTAMP '2026-06-11 10:10:00', DATE '2026-06-12')
AS t(order_id, user_id, status, total_amount, created_at, dt);
Note

For kernel versions earlier than 3.2.8, the INSERT syntax must use the INSERT INTO ... SELECT * FROM VALUES (...) AS t(...) form. Kernel version 3.2.8 and later also support the direct INSERT INTO ... VALUES (...) form.

INSERT OVERWRITE (overwrite)

On a partitioned table, INSERT OVERWRITE uses a dynamic partition overwrite strategy: only partitions present in the SELECT result are replaced, leaving other partitions unchanged.

-- Only overwrite the data in the dt='2026-06-11' partition. Other partitions are not affected.
INSERT OVERWRITE lake_db.orders
SELECT * FROM VALUES
    (2001, 601, 'paid', 999.00, TIMESTAMP '2026-06-11 12:00:00', DATE '2026-06-11')
AS t(order_id, user_id, status, total_amount, created_at, dt);

DELETE (row-level delete)

Iceberg tables support row-level deletion. To use DELETE, set format_version to '3' when you create the table.

-- Create a table with format_version=3 to support row-level DELETE
CREATE TABLE lake_db.orders_v3 (
    order_id     BIGINT,
    user_id      BIGINT,
    status       STRING,
    total_amount DECIMAL(18, 2),
    created_at   TIMESTAMP
)
PARTITIONED BY (day(created_at))
STORED AS ICEBERG
LOCATION 'oss://<your-bucket>/warehouse/lake_db/orders_v3/'
TBLPROPERTIES (
    'format_version' = '3'
);

-- Delete rows that meet the condition
DELETE FROM lake_db.orders_v3 WHERE status = 'cancelled';
Note

Row-level deletion only supports the write.delete.mode='merge-on-read' strategy: the delete operation writes delete files instead of immediately rewriting data files, and the deletes are merged with the data files at query time.

Query data

Basic queries

SELECT * FROM lake_db.orders WHERE dt = DATE '2026-06-11';

SELECT dt, COUNT(*) AS cnt, SUM(total_amount) AS total
FROM lake_db.orders
GROUP BY dt;

Partition pruning

When the WHERE clause references partition columns or transform functions, the engine automatically prunes irrelevant partitions and scans only matching data files.

-- Identity partitioning: match the partition column directly
SELECT * FROM lake_db.orders WHERE dt = DATE '2026-06-11';

-- day(col) partitioning: match the time range
SELECT * FROM lake_db.orders_by_day
WHERE created_at >= TIMESTAMP '2026-06-11 00:00:00'
  AND created_at <  TIMESTAMP '2026-06-12 00:00:00';

Predicate pushdown

Iceberg pushes WHERE predicates down to the data file level and uses Parquet file statistics (min/max/null count) to skip row groups that do not match. This reduces the amount of data read.

Predicate pushdown takes effect when the following conditions are met:

  • The columns in the WHERE clause have statistics.

  • The predicate is a simple comparison (=, <, >, <=, >=, IN, or BETWEEN).

  • The column type matches the statistics type.

-- Predicate pushdown takes effect: order_id has min/max statistics
SELECT * FROM lake_db.orders
WHERE order_id = 1001;

-- Range predicate pushdown
SELECT * FROM lake_db.orders
WHERE total_amount BETWEEN 100 AND 500;

Query notes

  • Partition pruning takes effect only when the WHERE clause directly references a partition column or a partition transform function.

  • Complex predicates (such as function calls or OR conditions) may affect the pushdown result.

  • The performance of JOINs across Iceberg tables depends on the data volume and partition design.

Configuration parameters

You can set these parameters by using Config or Hint to adjust the write and query behavior of Iceberg tables.

Parameter

Description

Restart required

iceberg_write_max_partition

The maximum number of partitions allowed per writer during writes. Default value: 100.

No

iceberg_metadata_cache_enabled

Enables or disables the metadata cache. Default value: true.

No

iceberg_manifest_cache_query_strategy

The manifest cache strategy for queries. Valid values: none (default, use the cache normally), bypass (skip the cache), clear (clear the cache before reading), and reload (clear the cache and re-cache).

No

The following instance-level parameters require an instance restart to take effect:

Parameter

Description

Default value

ICEBERG_IO_MANIFEST_CACHE_ENABLED

The master switch for manifest file caching.

false

ICEBERG_IO_MANIFEST_CACHE_MAX_TOTAL_BYTES

The maximum total bytes for the manifest cache.

104857600 (100 MB)

ICEBERG_IO_MANIFEST_CACHE_EXPIRATION_INTERVAL_MS

The expiration time for cache entries, in milliseconds.

0 (never expires)

ICEBERG_IO_MANIFEST_CACHE_MAX_CONTENT_LENGTH

The maximum bytes of a single manifest file that can be cached. Files exceeding this limit are not cached.

8388608 (8 MB)

ICEBERG_IO_MANIFEST_CACHE_MAX_CHUNK_SIZE

The maximum buffer length used when caching the content of a manifest file.

2097152 (2 MB)

Limits

  • Use STRING for string types. VARCHAR and CHAR(N) are not supported.

  • STORED AS ICEBERG is a required clause. The table is not created as an Iceberg table if this clause is missing.

  • Both format_version='2' (default) and '3' are supported. Row-level DELETE requires '3'.

  • For kernel versions earlier than 3.2.8, the INSERT syntax must use the INSERT INTO t SELECT * FROM VALUES (...) form. Kernel version 3.2.8 and later also support the direct INSERT INTO t VALUES (...) form.

  • INSERT OVERWRITE uses a dynamic partition overwrite strategy for partitioned tables. Only the partitions involved in the SELECT result are replaced.

  • Row-level DELETE requires format_version='3'.

  • The storage mode (internal lake or external lake) cannot be changed after the table is created.

  • To create an internal lake table, you must specify both catalog_type='ADB' and adb_lake_bucket. For an external lake table, you specify the path by using LOCATION and do not set catalog_type.

  • Too many partitions can degrade performance. For high-cardinality columns, use day() or month() to limit the partition count.