Iceberg external tables (XIHE SQL)
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 |
|
|
|
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 |
|
|
Yes |
Declares the table format as Iceberg. |
|
|
No |
Partition expression. Identity partitioning (using column values directly) and transform function partitioning ( |
|
|
Required for external lake |
Points to a user-owned OSS path in the format |
|
|
Required for internal lake |
For the internal lake mode, you must include |
The following table properties can be configured in TBLPROPERTIES:
|
Property |
Default value |
Description |
|
|
N/A |
Required for the internal lake mode. Set the value to |
|
|
N/A |
Required for the internal lake mode. Set the value to the name of the OSS bucket allocated by AnalyticDB for MySQL. |
|
|
|
The Iceberg format version. Both |
|
|
|
The storage format of data files. Only |
|
|
N/A |
In external lake mode, if you want to point to existing Iceberg data, specify the OSS path of the |
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/';
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'
);
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 |
|
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 |
|
|
|
DATE / TIMESTAMP |
|
|
|
DATE / TIMESTAMP |
|
|
|
DATE / TIMESTAMP |
|
|
|
TIMESTAMP |
|
|
|
Any (commonly used for high-cardinality columns, such as IDs) |
|
|
|
STRING |
|
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 separatedtcolumn. Iceberg automatically extracts the date fromcreated_atfor 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;
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);
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';
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, orBETWEEN). -
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 |
|
|
The maximum number of partitions allowed per writer during writes. Default value: 100. |
No |
|
|
Enables or disables the metadata cache. Default value: true. |
No |
|
|
The manifest cache strategy for queries. Valid values: |
No |
The following instance-level parameters require an instance restart to take effect:
|
Parameter |
Description |
Default value |
|
|
The master switch for manifest file caching. |
false |
|
|
The maximum total bytes for the manifest cache. |
104857600 (100 MB) |
|
|
The expiration time for cache entries, in milliseconds. |
0 (never expires) |
|
|
The maximum bytes of a single manifest file that can be cached. Files exceeding this limit are not cached. |
8388608 (8 MB) |
|
|
The maximum buffer length used when caching the content of a manifest file. |
2097152 (2 MB) |
Limits
-
Use
STRINGfor string types.VARCHARandCHAR(N)are not supported. -
STORED AS ICEBERGis 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 directINSERT 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'andadb_lake_bucket. For an external lake table, you specify the path by usingLOCATIONand do not setcatalog_type. -
Too many partitions can degrade performance. For high-cardinality columns, use
day()ormonth()to limit the partition count.