博客 / 工程

ClickHouse 24.4 版本发布

author avatar
ClickHouse 团队
2024 年 5 月 5 日 - 19 分钟阅读

又一个月过去了,这意味着又到了发布新版本的时候了!

ClickHouse 24.3 版本包含 13 个新功能 🎁 16 项性能优化 🛷 65 个错误修复 🐛

新贡献者

与往常一样,我们特别欢迎 24.4 版本中的所有新贡献者!ClickHouse 的受欢迎程度在很大程度上归功于社区的贡献。看到社区不断壮大总是令人感到谦卑。

以下是新贡献者的名字

Alexey Katsman, Anita Hammer, Arnaud Rocher, Chandre Van Der Westhuizen, Eduard Karacharov, Eliot Hautefeuille, Igor Markelov, Ilya Andreev, Jhonso7393, Joseph Redfern, Josh Rodriguez, Kirill, KrJin, Maciej Bak, Murat Khairulin, Paweł Kudzia, Tristan, dilet6298, loselarry

提示:如果您好奇我们是如何生成此列表的... 点击这里

您还可以查看演示文稿的幻灯片

递归 CTE

由 Maksim Kita 贡献

SQL:1999 引入了递归公共表表达式 (CTE),用于分层查询,使 SQL 成为一种图灵完备的编程语言。

到目前为止,ClickHouse 一直通过使用分层字典来支持分层查询。借助我们新的查询分析和优化基础设施,现在默认启用,我们终于具备了引入期待已久的强大功能(如递归 CTE)的一切条件。

ClickHouse 递归 CTE 具有标准的 SQL:1999 语法,并且通过了所有 PostgreSQL 递归 CTE 的测试。此外,ClickHouse 现在比 PostgreSQL 更好地支持递归 CTE。在 CTE 的 UNION ALL 子句的底部部分,可以指定多个(任意复杂的)查询,可以多次引用 CTE 基表等等。

递归 CTE 可以优雅而简单地解决分层问题。例如,它们可以轻松回答分层数据模型(例如树和图)的可达性问题。

具体来说,递归 CTE 可以计算关系的传递闭包。以伦敦地铁的线路连接作为二元关系示例,您可以想象所有直接连接的地铁站的集合:牛津环岛 -> 邦德街邦德街 -> 大理石拱门大理石拱门 -> 兰卡斯特门,等等。这些连接的传递闭包包括这些站点之间所有可能的连接,例如 牛津环岛 -> 兰卡斯特门牛津环岛 -> 大理石拱门,等等。

为了演示这一点,我们使用了数据集的改编版本,该数据集模拟了所有伦敦地铁连接,其中每个条目代表两个直接连接的站点。然后,我们可以使用递归 CTE 轻松回答这样的问题

牛津环岛站中央线上出发,最多换乘五站,我们可以到达哪些站?

我们用中央线车站地图的截图来可视化这一点

unnamed2.png

我们创建一个 ClickHouse 表来存储伦敦地铁连接数据集

CREATE OR REPLACE TABLE Connections (
    Station_1 String,
    Station_2 String,
    Line String,
    PRIMARY KEY(Line, Station_1, Station_2)
);

细心的读者会发现,我们在上面的 DDL 语句中没有指定表引擎(自 ClickHouse 24.3 以来这是可能的),并且在列定义中使用了 PRIMARY KEY 语法(自 ClickHouse 23.7 以来这是可能的)。这样一来,不仅递归 CTE,而且我们的 ClickHouse 表 DDL 语法也符合 SQL 标准。

通过使用 url 表函数自动模式推断,我们将数据集直接加载到我们的表中

INSERT INTO Connections
SELECT * FROM url('https://datasets-documentation.s3.eu-west-3.amazonaws.com/london_underground/london_connections.csv')

这是加载的数据的样子

SELECT
    *
FROM Connections
WHERE Line = 'Central Line'
ORDER BY Station_1, Station_2
LIMIT 10;

    ┌─Station_1──────┬─Station_2────────┬─Line─────────┐
 1. │ Bank           │ Liverpool Street │ Central Line │
 2. │ Bank           │ St. Paul's       │ Central Line │
 3. │ Barkingside    │ Fairlop          │ Central Line │
 4. │ Barkingside    │ Newbury Park     │ Central Line │
 5. │ Bethnal Green  │ Liverpool Street │ Central Line │
 6. │ Bethnal Green  │ Mile End         │ Central Line │
 7. │ Bond Street    │ Marble Arch      │ Central Line │
 8. │ Bond Street    │ Oxford Circus    │ Central Line │
 9. │ Buckhurst Hill │ Loughton         │ Central Line │
10. │ Buckhurst Hill │ Woodford         │ Central Line │
    └────────────────┴──────────────────┴──────────────┘

现在,我们使用递归 CTE 来回答上述问题

WITH RECURSIVE Reachable_Stations AS
(
    SELECT Station_1, Station_2, Line, 1 AS stops
    FROM Connections
    WHERE Line = 'Central Line'
      AND Station_1 = 'Oxford Circus'
    UNION ALL
    SELECT rs.Station_1, c.Station_2, c.Line, rs.stops + 1 AS stops
    FROM Reachable_Stations AS rs, Connections AS c
    WHERE rs.Line = c.Line
      AND rs.Station_2 = c.Station_1
      AND rs.stops < 5
)
SELECT DISTINCT (Station_1, Station_2, stops) AS connections
FROM Reachable_Stations
ORDER BY stops ASC;

这是结果

    ┌─connections────────────────────────────────┐
 1. │ ('Oxford Circus','Bond Street',1)          │
 2. │ ('Oxford Circus','Tottenham Court Road',1) │
 3. │ ('Oxford Circus','Marble Arch',2)          │
 4. │ ('Oxford Circus','Oxford Circus',2)        │
 5. │ ('Oxford Circus','Holborn',2)              │
 6. │ ('Oxford Circus','Bond Street',3)          │
 7. │ ('Oxford Circus','Lancaster Gate',3)       │
 8. │ ('Oxford Circus','Tottenham Court Road',3) │
 9. │ ('Oxford Circus','Chancery Lane',3)        │
10. │ ('Oxford Circus','Marble Arch',4)          │
11. │ ('Oxford Circus','Oxford Circus',4)        │
12. │ ('Oxford Circus','Queensway',4)            │
13. │ ('Oxford Circus','Holborn',4)              │
14. │ ('Oxford Circus','St. Paul\'s',4)          │
15. │ ('Oxford Circus','Bond Street',5)          │
16. │ ('Oxford Circus','Lancaster Gate',5)       │
17. │ ('Oxford Circus','Tottenham Court Road',5) │
18. │ ('Oxford Circus','Notting Hill Gate',5)    │
19. │ ('Oxford Circus','Chancery Lane',5)        │
20. │ ('Oxford Circus','Bank',5)                 │
    └────────────────────────────────────────────┘

递归 CTE 具有简单的基于迭代的执行逻辑,其行为类似于递归的自连接-...-自连接,一旦找不到新的连接伙伴或满足中止条件,自连接就会停止。为此,我们的 CTE 上述内容首先执行 UNION ALL 子句的顶部部分,查询我们的 Connections 表,以查找所有直接连接到 牛津环岛 站在 中央线 上的站点。这将返回一个绑定到 Reachable_Stations 标识符的表,如下所示

 Initial Reachable_Stations table content
 ┌─Station_1─────┬─Station_2────────────┐
 │ Oxford Circus │ Bond Street          │
 │ Oxford Circus │ Tottenham Court Road │
 └───────────────┴──────────────────────┘

从现在开始,只会执行 CTE 的 UNION ALL 子句的底部部分(递归地)

Reachable_StationsConnections 表连接,以查找 Connections 表中以下连接伙伴,其 Station_1 值与 Reachable_StationsStation_2 值匹配

Connections table join partners

┌─Station_1────────────┬─Station_2─────┐
│ Bond Street          │ Marble Arch   │
│ Bond Street          │ Oxford Circus │
│ Tottenham Court Road │ Holborn       │
│ Tottenham Court Road │ Oxford Circus │
└──────────────────────┴───────────────┘

通过 UNION ALL 子句,这些连接伙伴被添加到 Reachable_Stations 表中(Station_1 列被 牛津环岛 替换),并且我们的递归 CTE 的第一次迭代完成。在下一次迭代中,通过执行 CTE 的 UNION ALL 子句的底部部分,Reachable_Stations 再次与 Connections 表连接,以识别(并添加到 Reachable_StationsConnections 表中所有新的连接伙伴,其 Station_1 值与 Reachable_StationsStation_2 值匹配。这些迭代持续进行,直到找不到新的连接伙伴或满足停止条件。在上面的查询中,我们使用 stops 计数器,以便在我们指定的最大停靠站数从起始站到达时中止 CTE 的迭代周期。

请注意,结果将 牛津环岛 列为从 牛津环岛 出发的可达站点,停靠站数为 2 和 4。这在理论上是正确的,但不是很实用,并且是由我们的查询不考虑任何方向或周期引起的。我们将其作为读者的一个有趣的练习。

作为奖励问题,我们对从 牛津环岛 站在 中央线 到达 斯特拉特福 站需要多少站停靠感兴趣。我们再次用中央线地图来可视化这一点

 (5).png

为此,我们只需要修改递归 CTE 的中止条件(一旦将目标站点为 斯特拉特福 的连接伙伴添加到 CTE 表中,就停止 CTE 的连接迭代)

WITH RECURSIVE Reachable_Stations AS
(
    SELECT Station_1, Station_2, Line, 1 AS stops
    FROM Connections
    WHERE Line = 'Central Line'
      AND Station_1 = 'Oxford Circus'
  UNION ALL
    SELECT rs.Station_1 c.Station_2, c.Line, rs.stops + 1 AS stops
    FROM Reachable_Stations AS rs, Connections AS c
    WHERE rs.Line = c.Line
      AND rs.Station_2 = c.Station_1
      AND 'Stratford' NOT IN (SELECT Station_2 FROM Reachable_Stations)
)
SELECT max(stops) as stops
FROM Reachable_Stations;

结果显示需要 9 个停靠站,这与上面的中央线地图计划相符

   ┌─stops─┐
1. │     9 │
   └───────┘

递归 CTE 可以轻松回答有关此数据集的更有趣的问题。例如,可以使用原始版本的数据集中的相对连接时间来发现从 牛津环岛 站到 希思罗机场 站的最快连接(以及跨地铁线路)。请继续关注后续文章中的解决方案。

QUALIFY

由 Maksim Kita 贡献

此版本中添加的另一个功能是 QUALIFY 子句,它允许我们根据窗口函数的值进行过滤。

我们将借助窗口函数 - 排名示例,了解如何使用它。该数据集包含假设的足球运动员及其薪资。我们可以像这样将其导入 ClickHouse

CREATE TABLE salaries ORDER BY team AS
FROM url('https://raw.githubusercontent.com/ClickHouse/examples/main/LearnClickHouseWithMark/WindowFunctions-Aggregation/data/salaries.csv')
SELECT * EXCEPT (weeklySalary), weeklySalary AS salary
SETTINGS schema_inference_make_columns_nullable=0;

让我们快速查看 salaries 表中的数据

SELECT * FROM salaries LIMIT 5;
   ┌─team──────────────┬─player───────────┬─position─┬─salary─┐
1. │ Aaronbury Seekers │ David Morton     │ D        │  63014 │
2. │ Aaronbury Seekers │ Edwin Houston    │ D        │  51751 │
3. │ Aaronbury Seekers │ Stephen Griffith │ M        │ 181825 │
4. │ Aaronbury Seekers │ Douglas Clay     │ F        │  73436 │
5. │ Aaronbury Seekers │ Joel Mendoza     │ D        │ 257848 │
   └───────────────────┴──────────────────┴──────────┴────────┘

接下来,让我们计算每个球员按位置的薪资排名。即,他们相对于在同一位置踢球的人的薪酬是多少?

SELECT player, team, position AS pos, salary,
       rank() OVER (PARTITION BY position ORDER BY salary DESC) AS posRank
FROM salaries
ORDER BY salary DESC
LIMIT 5
   ┌─player──────────┬─team────────────────────┬─pos─┬─salary─┬─posRank─┐
1. │ Robert Griffin  │ North Pamela Trojans    │ GK  │ 399999 │       1 │
2. │ Scott Chavez    │ Maryhaven Generals      │ M   │ 399998 │       1 │
3. │ Dan Conner      │ Michaelborough Rogues   │ M   │ 399998 │       1 │
4. │ Nathan Thompson │ Jimmyville Legionnaires │ D   │ 399998 │       1 │
5. │ Benjamin Cline  │ Stephaniemouth Trojans  │ D   │ 399998 │       1 │
   └─────────────────┴─────────────────────────┴─────┴────────┴─────────┘

假设我们想要过滤 posRank,仅返回每个位置薪酬最高的前 3 名球员。我们可能会尝试添加一个 WHERE 子句来做到这一点

SELECT player, team, position AS pos, salary,
       rank() OVER (PARTITION BY position ORDER BY salary DESC) AS posRank
FROM salaries
WHERE posRank <= 3
ORDER BY salary DESC
LIMIT 5
Received exception:
Code: 184. DB::Exception: Window function rank() OVER (PARTITION BY position ORDER BY salary DESC) AS posRank is found in WHERE in query. (ILLEGAL_AGGREGATION)

我们无法做到这一点,因为 WHERE 子句在窗口函数评估之前运行。在 24.4 版本之前,我们可以通过引入 CTE 来解决这个问题

WITH salaryRankings AS
    (
        SELECT player, 
               if(
                 length(team) <=25, 
                 team, 
                 concat(substring(team, 5), 1, '...')
               ) AS team, 
               position AS pos, salary,
               rank() OVER (
                 PARTITION BY position 
                 ORDER BY salary DESC
               ) AS posRank
        FROM salaries
        ORDER BY salary DESC
    )
SELECT *
FROM salaryRankings
WHERE posRank <= 3
    ┌─player────────────┬─team────────────────────┬─pos─┬─salary─┬─posRank─┐
 1. │ Robert Griffin    │ North Pamela Trojans    │ GK  │ 399999 │       1 │
 2. │ Scott Chavez      │ Maryhaven Generals      │ M   │ 399998 │       1 │
 3. │ Dan Conner        │ Michaelborough Rogue... │ M   │ 399998 │       1 │
 4. │ Benjamin Cline    │ Stephaniemouth Troja... │ D   │ 399998 │       1 │
 5. │ Nathan Thompson   │ Jimmyville Legionnai... │ D   │ 399998 │       1 │
 6. │ William Rubio     │ Nobleview Sages         │ M   │ 399997 │       3 │
 7. │ Juan Bird         │ North Krystal Knight... │ GK  │ 399986 │       2 │
 8. │ John Lewis        │ Andreaberg Necromanc... │ D   │ 399985 │       3 │
 9. │ Michael Holloway  │ Alyssaborough Sages     │ GK  │ 399984 │       3 │
10. │ Larry Buchanan    │ Eddieshire Discovere... │ F   │ 399973 │       1 │
11. │ Alexis Valenzuela │ Aaronport Crusaders     │ F   │ 399972 │       2 │
12. │ Mark Villegas     │ East Michaelborough ... │ F   │ 399972 │       2 │
    └───────────────────┴─────────────────────────┴─────┴────────┴─────────┘

此查询有效,但非常笨拙。现在我们有了 QUALIFY 子句,我们可以过滤数据,而无需引入 CTE,如下所示

SELECT player, team, position AS pos, salary,
       rank() OVER (PARTITION BY position ORDER BY salary DESC) AS posRank
FROM salaries
QUALIFY posRank <= 3
ORDER BY salary DESC;

我们将获得与之前相同的结果。

连接性能改进

由 Maksim Kita 贡献

对于非常特定的 JOIN 用例,还有一些性能改进。

第一个是更好的谓词下推,当分析器计算出何时可以将过滤器条件应用于 JOIN 的两侧时。

让我们看一个使用OpenSky 数据集的示例,该数据集包含 2019-2021 年的空中交通数据。我们想要获取通过旧金山的十个航班的列表,我们可以使用以下查询来完成

SELECT
    l.origin,
    r.destination AS dest,
    firstseen,
    lastseen
FROM opensky AS l
INNER JOIN opensky AS r ON l.destination = r.origin
WHERE notEmpty(l.origin) AND notEmpty(r.destination) AND (r.origin = 'KSFO')
LIMIT 10
SETTINGS optimize_move_to_prewhere = 0

我们禁用了 optimize_move_to_prewhere,以便 ClickHouse 不执行另一个优化,这将阻止我们看到 JOIN 改进的好处。如果我们对 24.3 运行此查询,我们将看到以下输出

    ┌─origin─┬─dest─┬───────────firstseen─┬────────────lastseen─┐
 1.00WA   │ 00CL │ 2019-10-14 21:03:192019-10-14 22:42:012.00WA   │ ZGGG │ 2019-10-14 21:03:192019-10-14 22:42:013.00WA   │ ZGGG │ 2019-10-14 21:03:192019-10-14 22:42:014.00WA   │ ZGGG │ 2019-10-14 21:03:192019-10-14 22:42:015.00WA   │ ZGGG │ 2019-10-14 21:03:192019-10-14 22:42:016.00WA   │ ZGGG │ 2019-10-14 21:03:192019-10-14 22:42:017.00WA   │ ZGGG │ 2019-10-14 21:03:192019-10-14 22:42:018.00WA   │ YSSY │ 2019-10-14 21:03:192019-10-14 22:42:019.00WA   │ YSSY │ 2019-10-14 21:03:192019-10-14 22:42:0110.00WA   │ YSSY │ 2019-10-14 21:03:192019-10-14 22:42:01 │
    └────────┴──────┴─────────────────────┴─────────────────────┘

10 rows in set. Elapsed: 0.656 sec. Processed 15.59 million rows, 451.90 MB (23.75 million rows/s., 688.34 MB/s.)
Peak memory usage: 62.79 MiB.

让我们看一下 24.4

    ┌─origin─┬─dest─┬───────────firstseen─┬────────────lastseen─┐
 1.00WA   │ 00CL │ 2019-10-14 21:03:192019-10-14 22:42:012.00WA   │ ZGGG │ 2019-10-14 21:03:192019-10-14 22:42:013.00WA   │ ZGGG │ 2019-10-14 21:03:192019-10-14 22:42:014.00WA   │ ZGGG │ 2019-10-14 21:03:192019-10-14 22:42:015.00WA   │ ZGGG │ 2019-10-14 21:03:192019-10-14 22:42:016.00WA   │ ZGGG │ 2019-10-14 21:03:192019-10-14 22:42:017.00WA   │ ZGGG │ 2019-10-14 21:03:192019-10-14 22:42:018.00WA   │ YSSY │ 2019-10-14 21:03:192019-10-14 22:42:019.00WA   │ YSSY │ 2019-10-14 21:03:192019-10-14 22:42:0110.00WA   │ YSSY │ 2019-10-14 21:03:192019-10-14 22:42:01 │
    └────────┴──────┴─────────────────────┴─────────────────────┘

10 rows in set. Elapsed: 0.079 sec.

因此,速度快了大约 8 倍。如果我们通过 SELECT * 返回所有列,则此查询在 24.3 中花费的时间会增加到 4 秒以上,而在 24.4 中为 0.4 秒,提高了 10 倍。

让我们看看是否可以理解为什么它更快。感兴趣的两行是

INNER JOIN opensky AS r ON l.destination = r.origin
WHERE notEmpty(l.origin) AND notEmpty(r.destination) AND (r.origin = 'KSFO')

我们的 WHERE 子句的最后一个谓词是 r.origin = 'KSFO'。在上一行中,我们说过,如果 l.destination = r.origin,我们才想进行连接,这意味着 l.destination = 'KSFO' 也是如此。24.4 中的分析器知道这一点,因此可以更早地过滤掉大量行。

换句话说,我们的 WHERE 子句在 24.4 中实际上看起来像这样

INNER JOIN opensky AS r ON l.destination = r.origin
WHERE notEmpty(l.origin) AND notEmpty(r.destination) 
AND (r.origin = 'KSFO') AND (l.destination = 'KSFO')

第二个改进是,如果 JOIN 之后的谓词过滤掉任何未连接的行,分析器现在会自动将 OUTER JOIN 转换为 INNER JOIN

例如,假设我们最初编写了一个查询来查找旧金山和纽约之间的航班,同时捕获直飞航班和中途停留航班。

SELECT
    l.origin,
    l.destination,
    r.destination,
    registration,
    l.callsign,
    r.callsign
FROM opensky AS l
LEFT JOIN opensky AS r ON l.destination = r.origin
WHERE notEmpty(l.destination) 
AND (l.origin = 'KSFO') 
AND (r.destination = 'KJFK') 
LIMIT 10

稍后,我们添加了一个额外的过滤器,仅返回 r.callsign = 'AAL1424' 的行。

SELECT
    l.origin,
    l.destination AS leftDest,
    r.destination AS rightDest,
    registration AS reg,
    l.callsign,
    r.callsign
FROM opensky AS l
LEFT JOIN opensky AS r ON l.destination = r.origin
WHERE notEmpty(l.destination) 
AND (l.origin = 'KSFO') 
AND (r.destination = 'KJFK') 
AND (r.callsign = 'AAL1424')
LIMIT 10
SETTINGS optimize_move_to_prewhere = 0

由于我们现在要求连接右侧的 callsign 列具有值,因此可以将 LEFT JOIN 转换为 INNER JOIN。让我们检查一下 24.3 和 24.4 中的查询性能。

24.3

    ┌─origin─┬─leftDest─┬─rightDest─┬─reg────┬─callsign─┬─r.callsign─┐
 1. │ KSFO   │ 01FA     │ KJFK      │ N12221 │ UAL423   │ AAL1424    │
 2. │ KSFO   │ 01FA     │ KJFK      │ N12221 │ UAL423   │ AAL1424    │
 3. │ KSFO   │ 01FA     │ KJFK      │ N12221 │ UAL423   │ AAL1424    │
 4. │ KSFO   │ 01FA     │ KJFK      │ N12221 │ UAL423   │ AAL1424    │
 5. │ KSFO   │ 01FA     │ KJFK      │ N12221 │ UAL423   │ AAL1424    │
 6. │ KSFO   │ 01FA     │ KJFK      │ N87527 │ UAL423   │ AAL1424    │
 7. │ KSFO   │ 01FA     │ KJFK      │ N87527 │ UAL423   │ AAL1424    │
 8. │ KSFO   │ 01FA     │ KJFK      │ N87527 │ UAL423   │ AAL1424    │
 9. │ KSFO   │ 01FA     │ KJFK      │ N87527 │ UAL423   │ AAL1424    │
10. │ KSFO   │ 01FA     │ KJFK      │ N87527 │ UAL423   │ AAL1424    │
    └────────┴──────────┴───────────┴────────┴──────────┴────────────┘

10 rows in set. Elapsed: 1.937 sec. Processed 63.98 million rows, 2.52 GB (33.03 million rows/s., 1.30 GB/s.)
Peak memory usage: 2.84 GiB.

24.4

    ┌─origin─┬─leftDest─┬─rightDest─┬─reg────┬─callsign─┬─r.callsign─┐
 1. │ KSFO   │ 01FA     │ KJFK      │ N12221 │ UAL423   │ AAL1424    │
 2. │ KSFO   │ 01FA     │ KJFK      │ N12221 │ UAL423   │ AAL1424    │
 3. │ KSFO   │ 01FA     │ KJFK      │ N12221 │ UAL423   │ AAL1424    │
 4. │ KSFO   │ 01FA     │ KJFK      │ N12221 │ UAL423   │ AAL1424    │
 5. │ KSFO   │ 01FA     │ KJFK      │ N12221 │ UAL423   │ AAL1424    │
 6. │ KSFO   │ 01FA     │ KJFK      │ N87527 │ UAL423   │ AAL1424    │
 7. │ KSFO   │ 01FA     │ KJFK      │ N87527 │ UAL423   │ AAL1424    │
 8. │ KSFO   │ 01FA     │ KJFK      │ N87527 │ UAL423   │ AAL1424    │
 9. │ KSFO   │ 01FA     │ KJFK      │ N87527 │ UAL423   │ AAL1424    │
10. │ KSFO   │ 01FA     │ KJFK      │ N87527 │ UAL423   │ AAL1424    │
    └────────┴──────────┴───────────┴────────┴──────────┴────────────┘

10 rows in set. Elapsed: 0.762 sec. Processed 23.22 million rows, 939.75 MB (30.47 million rows/s., 1.23 GB/s.)
Peak memory usage: 9.00 MiB.

在 24.4 中速度快了不到三倍。

如果您想了解有关 JOIN 性能改进如何实现的更多信息,请阅读 Maksim Kita 的博客文章,其中解释了所有内容。

这就是 24.4 版本的所有内容。我们希望您能加入我们参加 5 月 30 日的 24.5 版本发布会。请务必注册,以便您获得 Zoom 网络研讨会的所有详细信息

分享这篇文章

订阅我们的新闻通讯

随时了解功能发布、产品路线图、支持和云产品!
正在加载表单...
关注我们
X imageSlack imageGitHub image
Telegram imageMeetup imageRss image