跳转到主要内容
创建一个新表。该查询可根据不同的使用场景采用多种语法形式。 默认情况下,表只会在当前服务器上创建。分布式 DDL 查询通过 ON CLUSTER 子句来实现,相关内容另文说明

语法格式

使用显式 schema

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [NULL|NOT NULL] [DEFAULT|MATERIALIZED|EPHEMERAL|ALIAS expr1] [COMMENT 'comment for column'] [compression_codec] [TTL expr1],
    name2 [type2] [NULL|NOT NULL] [DEFAULT|MATERIALIZED|EPHEMERAL|ALIAS expr2] [COMMENT 'comment for column'] [compression_codec] [TTL expr2],
    ...
) ENGINE = engine
  [COMMENT 'comment for table']
db 数据库中创建一个名为 table_name 的表;如果未设置 db,则在当前数据库中创建。该表使用括号中指定的结构和 engine 引擎。 表结构由列描述、二级索引、投影和约束组成。如果该引擎支持主键,则会将其标明为表引擎的参数。 最简单情况下,列描述的形式为 name type。示例:RegionID UInt32 也可以为默认值定义表达式 (见下文) 。 如有需要,可以指定主键,其中包含一个或多个键表达式。 可以为列和表添加注释。

使用现有表的 schema

ClickHouse 支持复制现有表的 schema 和数据。 要复制现有表的 schema:
CREATE TABLE [IF NOT EXISTS] [db2.]table_clone AS [db.]table [ENGINE = engine]
这会创建一个与另一个表结构相同的表。

使用现有表的 schema 和数据

若要复制现有表的 schema 和数据:
CREATE TABLE [IF NOT EXISTS] [db2.]table_clone CLONE AS [db.]table [ENGINE = engine]
这会创建一个与现有表具有相同 schema 和数据的表。新表创建后,db.table 中的所有分区都会附加到该表。换句话说,在创建时,db.table 的数据会被克隆到 db2.table_clone。该查询等同于以下内容:
CREATE TABLE [IF NOT EXISTS] [db2.]table_clone AS [db.]table [ENGINE = engine];
ALTER TABLE [db2.]table_clone ATTACH PARTITION ALL FROM [db.]table;
对于这两项功能,你都可以为该表指定不同的引擎。如果未指定引擎,则默认使用与原始表 (db.table) 相同的引擎。

来自表函数

CREATE TABLE [IF NOT EXISTS] [db.]table_name AS table_function()
创建一个表,其结果与指定的表函数相同。创建后的表也会像所指定的对应表函数一样工作。

来自 SELECT 查询

CREATE TABLE [IF NOT EXISTS] [db.]table_name[(name1 [type1], name2 [type2], ...)] ENGINE = engine AS SELECT ...
使用 engine 引擎创建一个表,其结构与 SELECT 查询结果类似,并使用 SELECT 的数据填充该表。你也可以显式指定列描述。 如果表已存在且指定了 IF NOT EXISTS,则该查询不会执行任何操作。 查询中在 ENGINE 子句之后还可以有其他子句。有关如何创建表的详细文档,请参阅表引擎说明。 示例
Query
CREATE TABLE t1 (x String) ENGINE = Memory AS SELECT 1;
SELECT x, toTypeName(x) FROM t1;
Response
┌─x─┬─toTypeName(x)─┐
│ 1 │ String        │
└───┴───────────────┘

NULL 或 NOT NULL 修饰符

在列定义中,数据类型后面的 NULLNOT NULL 修饰符用于控制该列是否可以为 Nullable 如果该类型不是 Nullable,并且指定了 NULL,则会将其视为 Nullable;如果指定了 NOT NULL,则不会。例如,INT NULL 等同于 Nullable(INT)。如果该类型本身就是 Nullable,再指定 NULLNOT NULL 修饰符时,则会抛出异常。 另请参见 data_type_default_nullable 设置。

默认值

列描述可以通过 DEFAULT exprMATERIALIZED exprALIAS expr 的形式指定默认值表达式。例如:URLDomain String DEFAULT domain(URL) 表达式 expr 是可选的。如果省略,则必须显式指定列类型,此时默认值分别为:数值列为 0,字符串列为 '' (空字符串) ,数组列为 [] (空数组) ,日期列为 1970-01-01,Nullable 列为 NULL 默认值列的列类型可以省略,这种情况下会根据 expr 的类型自动推断。例如,列 EventDate DEFAULT toDate(EventTime) 的类型将为日期类型。 如果同时指定了 Data type 和默认值表达式,系统会插入一个隐式类型转换函数,将表达式转换为指定类型。例如:Hits UInt32 DEFAULT 0 在内部会表示为 Hits UInt32 DEFAULT toUInt32(0) 默认值表达式 expr 可以引用任意表列和常量。ClickHouse 会检查对表结构的修改不会在表达式计算中引入循环。对于 INSERT,它还会检查这些表达式是否可解析——也就是说,用于计算它们的所有列都必须已传入。

DEFAULT

DEFAULT expr 普通默认值。如果在 INSERT 查询中未指定此类列的值,则会根据 expr 计算。 示例:
CREATE OR REPLACE TABLE test
(
    id UInt64,
    updated_at DateTime DEFAULT now(),
    updated_at_date Date DEFAULT toDate(updated_at)
)
ENGINE = MergeTree
ORDER BY id;

INSERT INTO test (id) VALUES (1);

SELECT * FROM test;
┌─id─┬──────────updated_at─┬─updated_at_date─┐
12023-02-24 17:06:462023-02-24
└────┴─────────────────────┴─────────────────┘

MATERIALIZED

MATERIALIZED expr 物化表达式。插入行时,这类列的值会根据指定的物化表达式自动计算,不能在 INSERT 时显式指定。 此外,此类默认值列不会包含在 SELECT * 的结果中。这是为了保持这样一个不变性:SELECT * 的结果始终都可以通过 INSERT 再次插入到表中。可通过设置 asterisk_include_materialized_columns 禁用此行为。 示例:
CREATE OR REPLACE TABLE test
(
    id UInt64,
    updated_at DateTime MATERIALIZED now(),
    updated_at_date Date MATERIALIZED toDate(updated_at)
)
ENGINE = MergeTree
ORDER BY id;

INSERT INTO test VALUES (1);

SELECT * FROM test;
┌─id─┐
1
└────┘

SELECT id, updated_at, updated_at_date FROM test;
┌─id─┬──────────updated_at─┬─updated_at_date─┐
12023-02-24 17:08:082023-02-24
└────┴─────────────────────┴─────────────────┘

SELECT * FROM test SETTINGS asterisk_include_materialized_columns=1;
┌─id─┬──────────updated_at─┬─updated_at_date─┐
12023-02-24 17:08:082023-02-24
└────┴─────────────────────┴─────────────────┘

EPHEMERAL

EPHEMERAL [expr] 临时列。此类型的列不会存储在表中,也无法对其执行 SELECT。临时列的唯一用途,是基于它们构建其他列的默认值表达式。 执行未显式指定列的插入时,会跳过此类型的列。这样做是为了保持这样一个不变性:SELECT * 的结果始终都可以通过 INSERT 再次插回表中。 示例:
CREATE OR REPLACE TABLE test
(
    id UInt64,
    unhexed String EPHEMERAL,
    hexed FixedString(4) DEFAULT unhex(unhexed)
)
ENGINE = MergeTree
ORDER BY id;

INSERT INTO test (id, unhexed) VALUES (1, '5a90b714');

SELECT
    id,
    hexed,
    hex(hexed)
FROM test
FORMAT Vertical;

Row 1:
──────
id:         1
hexed:      Z��
hex(hexed): 5A90B714

ALIAS

ALIAS expr 计算列 (同义概念) 。这种类型的列不会存储在表中,也无法向其中 INSERT 值。 SELECT 查询显式引用这种类型的列时,其值会在查询时根据 expr 计算。默认情况下,SELECT * 会排除 ALIAS 列。可通过设置 asterisk_include_alias_columns 禁用此行为。 使用 ALTER 查询添加新列时,不会为这些列补写旧数据。相反,在读取不包含这些新列值的旧数据时,默认会动态计算表达式。不过,如果计算这些表达式需要查询中未指出的其他列,则还会额外读取这些列,但仅限于需要它们的数据块。 如果向表中添加了一个新列,但之后又更改了它的默认表达式,那么旧数据使用的值也会发生变化 (即那些值未存储在磁盘上的数据) 。请注意,在执行后台合并时,如果参与合并的某个 parts 中缺少某列的数据,则会将该列的数据写入合并后的 part。 无法为嵌套数据结构中的元素设置默认值。
CREATE OR REPLACE TABLE test
(
    id UInt64,
    size_bytes Int64,
    size String ALIAS formatReadableSize(size_bytes)
)
ENGINE = MergeTree
ORDER BY id;

INSERT INTO test VALUES (1, 4678899);

SELECT id, size_bytes, size FROM test;
┌─id─┬─size_bytes─┬─size─────┐
146788994.46 MiB │
└────┴────────────┴──────────┘

SELECT * FROM test SETTINGS asterisk_include_alias_columns=1;
┌─id─┬─size_bytes─┬─size─────┐
146788994.46 MiB │
└────┴────────────┴──────────┘

主键

你可以在创建表时定义主键。主键可以通过两种方式指定:
  • 在列列表中
CREATE TABLE [db.]table_name
(
    name1 type1, name2 type2, ...,
    PRIMARY KEY(expr1[, expr2,...])
)
ENGINE = engine;
  • 不在列列表中
CREATE TABLE [db.]table_name
(
    name1 type1, name2 type2, ...
)
ENGINE = engine
PRIMARY KEY(expr1[, expr2,...]);
不能在一次查询中同时使用这两种方式。

约束

除列描述外,还可以定义约束:

CONSTRAINT

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1] [compression_codec] [TTL expr1],
    ...
    CONSTRAINT constraint_name_1 CHECK boolean_expr_1,
    ...
) ENGINE = engine
boolean_expr_1 可以是任意布尔表达式。如果为该表定义了约束,那么在执行 INSERT 查询时,每一行都会检查所有约束。如果有任何约束不满足,server 将抛出异常,并给出约束名称和检查表达式。 添加大量约束可能会对大型 INSERT 查询的性能产生负面影响。

ASSUME

ASSUME 子句用于在表上定义一个被假定为真的 CONSTRAINT。优化器随后可以利用该约束来提升 SQL 查询性能。 以下示例展示了在创建 users_a 表时如何使用 ASSUME CONSTRAINT
CREATE TABLE users_a (
    uid Int16, 
    name String, 
    age Int16, 
    name_len UInt8 MATERIALIZED length(name), 
    CONSTRAINT c1 ASSUME length(name) = name_len
) 
ENGINE=MergeTree 
ORDER BY (name_len, name);
这里,ASSUME CONSTRAINT 用于断言 length(name) 函数的结果始终等于 name_len 列的值。这意味着,每当在查询中调用 length(name) 时,ClickHouse 都可以将其替换为 name_len,这样通常会更快,因为不必调用 length() 函数。 随后,在执行查询 SELECT name FROM users_a WHERE length(name) < 5; 时,ClickHouse 可以根据 ASSUME CONSTRAINT 将其优化为 SELECT name FROM users_a WHERE name_len < 5;。这样可以让查询运行得更快,因为无需为每一行计算 name 的长度。 ASSUME CONSTRAINT 不会强制执行该约束,它只是告知优化器该约束成立。如果该约束实际上并不成立,查询结果可能会不正确。因此,只有在你确定该约束确实成立时,才应使用 ASSUME CONSTRAINT

TTL 表达式

定义值的存储时长。只能为 MergeTree 家族表指定。有关详细说明,请参阅列和表的 TTL

列压缩编解码器

默认情况下,ClickHouse 在自管理版本中使用 lz4 压缩,在 ClickHouse Cloud 中使用 zstd 对于 MergeTree 引擎家族,你可以在服务器配置的 compression 部分修改默认压缩方法。 你还可以在 CREATE TABLE 查询中为每一列单独指定压缩方法。
CREATE TABLE codec_example
(
    dt Date CODEC(ZSTD),
    ts DateTime CODEC(LZ4HC),
    float_value Float32 CODEC(NONE),
    double_value Float64 CODEC(LZ4HC(9)),
    value Float32 CODEC(Delta, ZSTD)
)
ENGINE = <Engine>
...
可以指定 Default 编解码器,以引用默认压缩;它在运行时可能会取决于不同的设置 (以及数据属性) 。 示例:value UInt64 CODEC(Default) — 等同于未指定编解码器。 你还可以从列中移除当前的 CODEC,并使用 config.xml 中的默认压缩:
ALTER TABLE codec_example MODIFY COLUMN float_value CODEC(Default);
编解码器可以串联使用,例如 CODEC(Delta, Default)
不能使用 lz4 等外部工具解压 ClickHouse 数据库文件。请改用专用的 clickhouse-compressor 工具。
以下表引擎支持压缩:
  • MergeTree 家族。支持列压缩编解码器,并可通过 compression 设置选择默认压缩方法。
  • Log 家族。默认使用 lz4 压缩方法,并支持列压缩编解码器。
  • Set。仅支持默认压缩。
  • Join。仅支持默认压缩。
ClickHouse 支持通用编解码器和专用编解码器。

通用编解码器

NONE

NONE — 不压缩。

LZ4

LZ4 — 默认使用的无损数据压缩算法。采用 LZ4 快速压缩。

LZ4HC

LZ4HC[(level)] — LZ4 HC (高压缩) 算法,支持配置级别。默认级别:9。设置 level <= 0 时,将使用默认级别。可选级别:[1, 12]。建议级别范围:[4, 9]。

ZSTD

ZSTD[(level)] — 使用可配置 levelZSTD 压缩算法。可选级别:[1, 22]。默认级别:1。 较高的压缩级别适用于非对称场景,例如只压缩一次、反复解压。级别越高,压缩效果越好,但 CPU 使用率也越高。

已废弃:ZSTD_QAT

已废弃:DEFLATE_QPL

专用编解码器

这些编解码器旨在利用数据的特定特性,进一步提升压缩效果。其中有些编解码器本身并不直接压缩数据,而是先对数据进行预处理,以便在第二阶段使用通用编解码器时获得更高的压缩率。

Delta

Delta(delta_bytes) — 一种压缩方法,其中原始值会替换为相邻两个值之差,但第一个值保持不变。delta_bytes 是原始值的最大大小,默认值为 sizeof(type)。将 delta_bytes 作为参数指定的做法已弃用,相关支持将在未来版本中移除。Delta 是一种数据预处理编解码器,也就是说,不能单独使用。

DoubleDelta

DoubleDelta(bytes_size) — 计算二阶增量,并以紧凑的二进制形式写入。bytes_size 的含义与 Delta 编解码器中的 delta_bytes 类似。将 bytes_size 指定为参数的做法已弃用,未来版本将移除对此用法的支持。对于步长固定的单调序列 (例如时间序列数据) ,可获得最佳压缩率。可用于任何数值类型。它实现了 Gorilla TSDB 中使用的算法,并将其扩展为支持 64 位类型。对于 32 位增量,会额外使用 1 个比特:使用 5 位前缀而不是 4 位前缀。更多信息请参见 Gorilla: A Fast, Scalable, In-Memory Time Series Database 中的 Compressing Time Stamps。DoubleDelta 是一种用于数据准备的编解码器,即不能单独使用。

GCD

GCD() - - 计算列中各个值的最大公约数 (GCD) ,然后将每个值除以该 GCD。可用于整数、小数和日期/时间列。该编解码器非常适合用于值按 GCD 的倍数变化 (增大或减小) 的列,例如 24、28、16、24、8、24 (GCD = 4) 。GCD 是一种数据预处理编解码器,也就是说,不能单独使用。

Gorilla

Gorilla(bytes_size) — 计算当前浮点值与前一个浮点值之间的 XOR,并将结果以紧凑的二进制形式写入。连续值之间的差异越小,也就是说序列中的值变化越慢,压缩率就越高。它实现了 Gorilla TSDB 使用的算法,并将其扩展为支持 64 位类型。bytes_size 的可选值为 1、2、4、8;如果 sizeof(type) 等于 1、2、4 或 8,则默认值为 sizeof(type)。在其他所有情况下,默认值为 1。更多信息请参见 Gorilla: A Fast, Scalable, In-Memory Time Series Database 的第 4.1 节。

ALP

ALP() — 一种基于十进制标度的浮点数据自适应无损压缩方法。ALP 会尝试将每个值用十进制幂表示为精确的缩放整数,然后使用 Frame-of-Reference 和位打包压缩得到的整数。无法被精确表示的值会作为原始例外值存储。它最适用于源自十进制的数值 (例如测量值、货币金额) 。支持 Float32Float64。详情参见 ALP: Adaptive lossless floating-point compression
此编解码器处于 Experimental 阶段,使用前需要设置 SET allow_experimental_codecs = 1

FPC

FPC(level, float_size) - 使用两种预测器中效果更好的一种,反复预测序列中的下一个浮点值,然后将实际值与预测值进行 XOR,并对结果执行前导零压缩。与 Gorilla 类似,在存储一系列变化缓慢的浮点值时,这种方式效率很高。对于 64 位值 (double) ,FPC 比 Gorilla 更快;对于 32 位值,则要视具体情况而定。可选的 level 值为:1-28,默认值为 12。可选的 float_size 值为:4、8;如果类型为 Float,则默认值为 sizeof(type)。其他所有情况下,默认值均为 4。有关该算法的详细说明,请参阅 High Throughput Compression of Double-Precision Floating-Point Data

T64

T64 —— 一种压缩方法,用于裁剪整数数据类型 (包括 EnumDateDateTime) 中未使用的高位。在该算法的每一步中,codec 都会取一个包含 64 个值的块,将其放入一个 64x64 位矩阵中,对矩阵进行转置,裁剪值中未使用的位,并将其余部分作为一个序列返回。所谓未使用的位,是指在使用该压缩的数据分区片段中,整体最大值与最小值之间没有差异的那些位。 DoubleDeltaGorilla codec 在 Gorilla TSDB 中用作其压缩算法的组成部分。Gorilla 方法在存在一组变化缓慢的值及其时间戳的场景下效果显著。时间戳可通过 DoubleDelta codec 进行高效压缩,而值可通过 Gorilla codec 进行高效压缩。例如,要获得存储效率更高的表,可以按如下配置创建:
CREATE TABLE codec_example
(
    timestamp DateTime CODEC(DoubleDelta),
    slow_values Float32 CODEC(Gorilla)
)
ENGINE = MergeTree()

加密编解码器

这些编解码器实际上并不压缩数据,而是对磁盘上的数据进行加密。只有在通过encryption设置指定加密密钥时,这些编解码器才可用。请注意,加密只有放在编解码器管道的末端才有意义,因为加密后的数据通常无法再以任何有意义的方式进行压缩。 加密编解码器:

AES_128_GCM_SIV

CODEC('AES-128-GCM-SIV') — 使用 RFC 8452 中定义的 GCM-SIV 模式,通过 AES-128 对数据进行加密。

AES-256-GCM-SIV

CODEC('AES-256-GCM-SIV') — 使用 GCM-SIV 模式的 AES-256 加密数据。 这些编解码器使用固定的 nonce,因此加密是确定性的。这使其与支持去重的引擎 (例如 ReplicatedMergeTree) 兼容,但也存在一个弱点:当同一个数据块被加密两次时,生成的密文会完全相同,因此能够读取磁盘的攻击者可以看出二者是相同的 (尽管只能看出这一点,无法获取其内容) 。
大多数引擎 (包括 “*MergeTree” 家族) 都会在磁盘上创建索引文件,而不会应用编解码器。这意味着如果加密列被建立索引,明文就会出现在磁盘上。
如果你执行的 SELECT 查询中指定了加密列中的某个特定值 (例如在 WHERE 子句中) ,该值可能会出现在 system.query_log 中。你可能需要禁用日志记录。
示例
CREATE TABLE mytable
(
    x String CODEC(AES_128_GCM_SIV)
)
ENGINE = MergeTree ORDER BY x;
如果需要启用压缩,必须显式指定。否则,数据只会加密,不会压缩。
示例
CREATE TABLE mytable
(
    x String CODEC(Delta, LZ4, AES_128_GCM_SIV)
)
ENGINE = MergeTree ORDER BY x;

临时表

请注意,临时表不会被复制。因此,无法保证插入临时表的数据在其他副本上也可用。临时表的主要用途是在单个会话期间查询或连接较小的外部数据集。
ClickHouse 支持临时表,具有以下特性:
  • 临时表会在会话结束时消失,包括连接中断时。
  • 如果未指定引擎,临时表会使用 Memory 表引擎;此外,它还可以使用除 Replicated 和 KeeperMap 引擎之外的任何表引擎。
  • 临时表不能指定 DB。它是在数据库之外创建的。
  • 无法使用分布式 DDL 查询通过 ON CLUSTER 在集群中的所有服务器上创建临时表:该表仅存在于当前会话中。
  • 如果临时表与其他表同名,且查询在未指定 DB 的情况下使用该表名,则会使用临时表。
  • 对于分布式查询处理,查询中使用的采用 Memory 引擎的临时表会被传递到远程服务器。
要创建临时表,请使用以下语法:
CREATE [OR REPLACE] TEMPORARY TABLE [IF NOT EXISTS] table_name
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) [ENGINE = engine]
在大多数情况下,临时表并不是手动创建的,而是在查询中使用外部数据,或使用分布式 (GLOBAL) IN 时创建。更多信息请参见相应章节 也可以使用 ENGINE = Memory 的表来替代临时表。

REPLACE TABLE

REPLACE 语句允许你以原子方式更新表。
该语句支持 AtomicReplicated 数据库引擎, 它们分别是 ClickHouse 和 ClickHouse Cloud 的默认数据库引擎。
通常,如果你需要从表中删除部分数据, 可以创建一个新表,并用不会检索到不需要数据的 SELECT 语句填充该表, 然后删除旧表,再将新表重命名。 下面的示例演示了这种方法:
CREATE TABLE myNewTable AS myOldTable;

INSERT INTO myNewTable
SELECT * FROM myOldTable 
WHERE CounterID <12345;

DROP TABLE myOldTable;

RENAME TABLE myNewTable TO myOldTable;
除了上述方法,也可以使用 REPLACE (前提是使用默认数据库引擎) 来达到相同效果:
REPLACE TABLE myOldTable
ENGINE = MergeTree()
ORDER BY CounterID 
AS
SELECT * FROM myOldTable
WHERE CounterID <12345;

语法

{CREATE [OR REPLACE] | REPLACE} TABLE [db.]table_name
CREATE 语句的所有语法形式同样适用于该语句。对不存在的表执行 REPLACE 会报错。

示例:

请看下表:
CREATE DATABASE base 
ENGINE = Atomic;

CREATE OR REPLACE TABLE base.t1
(
    n UInt64,
    s String
)
ENGINE = MergeTree
ORDER BY n;

INSERT INTO base.t1 VALUES (1, 'test');

SELECT * FROM base.t1;

┌─n─┬─s────┐
1 │ test │
└───┴──────┘
我们可以使用 REPLACE 语句清空所有数据:
CREATE OR REPLACE TABLE base.t1 
(
    n UInt64,
    s Nullable(String)
)
ENGINE = MergeTree
ORDER BY n;

INSERT INTO base.t1 VALUES (2, null);

SELECT * FROM base.t1;

┌─n─┬─s──┐
2 │ \N │
└───┴────┘
或者,也可以使用 REPLACE 语句修改表结构:
REPLACE TABLE base.t1 (n UInt64) 
ENGINE = MergeTree 
ORDER BY n;

INSERT INTO base.t1 VALUES (3);

SELECT * FROM base.t1;

┌─n─┐
3
└───┘

COMMENT 子句

您可以在创建表时添加注释。 语法
CREATE TABLE [db.]table_name
(
    name1 type1, name2 type2, ...
)
ENGINE = engine
COMMENT 'Comment'
COMMENT 子句必须在 PARTITION BYORDER BY 以及存储专用 SETTINGS 等所有存储相关子句之后指定。COMMENT 子句之后,只会解析查询专用 SETTINGS (如 max_threads 等) ,不会解析存储相关设置。这意味着,正确的子句顺序是:
  • ENGINE
  • 存储子句
  • COMMENT
  • 查询设置 (如有)
示例
Query
CREATE TABLE t1 (x String) ENGINE = Memory COMMENT 'The temporary table';
SELECT name, comment FROM system.tables WHERE name = 't1';
Response
┌─name─┬─comment─────────────┐
│ t1   │ The temporary table │
└──────┴─────────────────────┘
最后修改于 2026年6月10日