Java技术债务Java技术债务

  •  首页
  •  分类
  •  归档
  •  标签
  • 博客日志
  • 资源分享
  •  友链
  •  关于本站
注册
登录

ClickHouse之Explain查看执行计划

ClickHouse,数据库,性能优化

文章目录

  • 前言
  • 基本语法
  • EXPLAIN 类型
  • 补充
  • 忠告

前言

在 clickhouse 20.6 版本之前要查看 SQL 语句的执行计划需要设置日志级别为 trace 才能 可以看到,并且只能真正执行 sql,在执行日志里面查看。在 20.6 版本引入了原生的执行计 划的语法。在 20.6.3 版本成为正式版本的功能。

基本语法

EXPLAIN [AST | SYNTAX | QUERY TREE | PLAN | PIPELINE | ESTIMATE | TABLE OVERRIDE] [setting = value, ...]
    [
      SELECT ... |
      tableFunction(...) [COLUMNS (...)] [ORDER BY ...] [PARTITION BY ...] [PRIMARY KEY] [SAMPLE BY ...] [TTL ...]
    ]
    [FORMAT ...]

示例:

explain select * from td_xplatform.stats_sd_campaign where profileId = 113434409578602 and date between '20220101' and '20220101';
-- 结果
Expression ((Projection + Before ORDER BY))
  ReadFromMergeTree (td_xplatform_local.stats_sd_campaign)


注意:ClickHouse官网也并没有过多讲解执行计划具体详细的解释,通过对sql执行的步骤分析出ClickHouse的执行计划是从底部往上逐一执行。

EXPLAIN 类型

  • PLAN:用于查看执行计划,默认值。
  • AST:用于查看语法树;
  • SYNTAX:用于优化语法,有时我们指定的查询语句未必是最优的,那么 ClickHouse 在底层会进行优化,EXPLAIN SYNTAX 可以返回对一条 SQL 语句进行优化后的结果。通过对比优化前和优化后的 SQL 语句,可以有助于我们理解 ClickHouse 的优化机制
  • PIPELINE:用于查看 PIPELINE 计划。
  • ESTIMATE:显示处理查询时要从表中读取的估计行数、标记数和部分数

注意:
explain默认值PLAN的结果不是那么的通俗易懂,可以使用setting进行查看详细的执行计划,判断sql是否合理化。

EXPLAIN PLAN

用于查看执行计划,默认值。以下是setting的key:

  • header:打印计划中各个步骤的 head 说明,默认关闭,默认值 0;
  • description:打印计划中各个步骤的描述,默认开启,默认值 1;
  • actions:打印计划中各个步骤的详细信息,默认关闭,默认值0。
  • json:以JSON格式将查询计划步骤打印为一行。默认:0
  • indexes:显示使用的索引、过滤部分的数量以及应用的每个索引的过滤颗粒数。默认值:0。支持MergeTree表。
    • NameSkip :索引名称(目前仅用于索引)。
    • Keys :索引使用的列数组。
    • Condition:使用情况。
    • DescriptionSkip:索引描述(目前仅用于索引)。
    • Parts:应用索引之前/之后的部分数。
    • Granules:应用索引之前/之后的颗粒数。

示例如下:

explain actions=1
select id,sum(totalCost) Spend
from xplatform_sd.sd_campaign ca
join td_xplatform.stats_sd_campaign report on ca.amazon_campaign_id = report.campaignId
where profileId = 113434409578602
group by id
order by Spend desc ;

-- 执行计划
Expression (Projection)
Actions: INPUT :: 0 -> id Int32 : 0
         INPUT : 1 -> sum(totalCost) Nullable(Decimal(38, 2)) : 1
         ALIAS sum(totalCost) :: 1 -> Spend Nullable(Decimal(38, 2)) : 2
Positions: 0 2
  Sorting (Sorting for ORDER BY)
  Sort description: sum(totalCost) DESC
    Expression (Before ORDER BY)
    Actions: INPUT :: 0 -> id Int32 : 0
             INPUT :: 1 -> sum(totalCost) Nullable(Decimal(38, 2)) : 1
    Positions: 0 1
      Aggregating
      Keys: id
      Aggregates:
          sum(totalCost)
            Function: sum(Nullable(Decimal(9, 2))) → Nullable(Decimal(38, 2))
            Arguments: totalCost
            Argument positions: 1
        Expression (Before GROUP BY)
        Actions: INPUT :: 0 -> id Int32 : 0
                 INPUT :: 1 -> totalCost Nullable(Decimal(9, 2)) : 1
        Positions: 0 1
          Filter (WHERE)
          Filter column: equals(profileId, 113434409578602) (removed)
          Actions: INPUT :: 0 -> id Int32 : 0
                   INPUT : 1 -> profileId Int64 : 1
                   INPUT :: 2 -> totalCost Nullable(Decimal(9, 2)) : 2
                   COLUMN Const(UInt64) -> 113434409578602_8 UInt64 : 3
                   FUNCTION equals(profileId :: 1, 113434409578602_8 :: 3) -> equals(profileId, 113434409578602) UInt8 : 4
          Positions: 0 2 4
            Join (JOIN)
              Expression (Before JOIN)
              Actions: INPUT :: 0 -> id Int32 : 0
                       INPUT :: 1 -> amazon_campaign_id Nullable(Int64) : 1
                       INPUT : 2 -> profile_id Nullable(Int64) : 2
                       COLUMN Const(UInt64) -> 113434409578602 UInt64 : 3
                       FUNCTION equals(profile_id :: 2, 113434409578602 :: 3) -> equals(profile_id, 113434409578602) Nullable(UInt8) : 4
              Positions: 0 1 4
                SettingQuotaAndLimits (Set limits and quota after reading from storage)
                  ReadFromStorage (MySQL)
              Expression ((Joined actions + Rename joined columns))
              Actions: INPUT : 0 -> profileId Int64 : 0
                       INPUT : 1 -> campaignId Int64 : 1
                       INPUT : 2 -> totalCost Nullable(Decimal(9, 2)) : 2
                       ALIAS profileId :: 0 -> profileId Int64 : 3
                       ALIAS campaignId :: 1 -> campaignId Int64 : 0
                       ALIAS totalCost :: 2 -> totalCost Nullable(Decimal(9, 2)) : 1
              Positions: 0 3 1
                SettingQuotaAndLimits (Set limits and quota after reading from storage)
                  Expression ((Projection + Before ORDER BY))
                  Actions: INPUT :: 0 -> profileId Int64 : 0
                           INPUT :: 1 -> campaignId Int64 : 1
                           INPUT :: 2 -> totalCost Nullable(Decimal(9, 2)) : 2
                  Positions: 0 1 2
                    SettingQuotaAndLimits (Set limits and quota after reading from storage)
                      ReadFromMergeTree
                      ReadType: Default
                      Parts: 843
                      Granules: 936

简单解释一下特殊的名词:

  • SettingQuotaAndLimits: Set limits and quota after reading from storage(从存储读取后设置限制和配额)
  • Positions:当前执行计划的位置
  • ReadFromMergeTree:从MergeTree读取数据
  • ReadFromStorage:从存储读取数据
  • Filter (WHERE):是 WHERE 或 HAVING 的实现。拦截过滤数据
  • Sorting (Sorting for ORDER BY):排序
  • Expression:计算列上的函数,例如 x, y -> x + 1, y * 2。使用 explain actions = 1 select … 查看更多详细信息
  • Aggregating:负责 GROUP BY。

EXPLAIN AST

查看查询的抽象语法树(AST)。支持所有类型的查询,而不仅仅是 SELECT。

示例如下:

EXPLAIN AST ALTER TABLE t1 DELETE WHERE date = today();
--执行计划
AlterQuery  t1 (children 1)
   ExpressionList (children 1)
    AlterCommand 27 (children 1)
     Function equals (children 1)
      ExpressionList (children 2)
       Identifier date
       Function today (children 1)
        ExpressionList

EXPLAIN SYNTAX

在语法优化后返回查询。

可以使用此语法进行判断sql是否是当前查询方式的最佳性能,可以查看一些sql是会进行谓词下推

示例如下:

explain syntax
select target.expression,
       sum(totalCost) Spend
from (select targetId, totalCost,date from td_xplatform.stats_sd_target where  profileId = 113434409578602) report
left join (select amazon_target_id,expression from xplatform_sd.sd_product_ad_target where profile_id = 113434409578602) target on report.targetId = target.amazon_target_id
where date between '20220101' and '20220202'
group by target.expression
order by Spend;
-- 执行计划如下
SELECT
    expression,
    sum(totalCost) AS Spend
FROM
(
    SELECT
        targetId,
        totalCost,
        date
    FROM td_xplatform.stats_sd_target
    WHERE (profileId = 113434409578602) AND ((date <= '20220202') AND (date >= '20220101'))
) AS report
ALL LEFT JOIN
(
    SELECT
        amazon_target_id,
        expression
    FROM xplatform_sd.sd_product_ad_target
    WHERE profile_id = 113434409578602
) AS target ON targetId = amazon_target_id
WHERE (date >= '20220101') AND (date <= '20220202')
GROUP BY expression
ORDER BY Spend ASC

EXPLAIN PIPELINE

  • header: 打印计划中各个步骤的 head 说明,默认关闭;
  • graph: 用DOT图形语言描述管道图,默认关闭,需要查看相关的图形需要配合graphviz 查看;
  • compact:如果开启了 graph,紧凑打印行开关。1:开启(默认值),0:关闭;

示例如下:

explain pipeline 
select sum(totalCost) 
from td_xplatform.stats_sd_campaign 
where profileId = 113434409578602
group by campaignId;
-- 结果如下
(SettingQuotaAndLimits)
  (Expression)
  ExpressionTransform
    (Aggregating)
    Resize 4 → 1
      AggregatingTransform × 4
        StrictResize 4 → 4
          (Expression)
          ExpressionTransform × 4
            (SettingQuotaAndLimits)
              (ReadFromMergeTree)
              MergeTreeThread × 4 0 → 1

简单解释一下特殊的名词:

  • SettingQuotaAndLimits: Set limits and quota after reading from storage(从存储读取后设置限制和配额)
  • AggregatingTransform x 4:4次聚合转换,是 Group By 高性能的核心所在。原因是因为ClickHouse是一个很耗cpu的数据库,会在不同的线程上进行统计计算,有多少线程取决于ClickHouse服务器多少核,多少线程。
  • ReadFromMergeTree:从MergeTree读取数据到内存中

EXPLAIN ESTIMATE

显示处理查询时要从表中读取的估计行数、标记数和部分数。使用MergeTree系列中的表。

示例如下:

explain estimate select sum(totalCost) from td_xplatform.stats_sd_target where profileId = 113434409578602 and date between '20220101' and '20220202';

补充

  • CreatingSets 为 IN(子查询)或哈希连接(在此查询中)填充哈希表。
  • ReadFromPreparedSource 实际上是从远程表中读取。
  • Union 是 UNION 的一个实现,或者只是来自多个来源
  • MergingAggregated 也是 GROUP BY 的一部分,它将来自不同来源(这里是远程和本地)的聚合函数状态合并在一起。

忠告

  • 尽量将查询条件靠近表
  • 尽量避免join,使用in代替join
  • 如果join的话,尽量join子查询,必须带上primary key
  • 右表尽可能的小

前言

在 clickhouse 20.6 版本之前要查看 SQL 语句的执行计划需要设置日志级别为 trace 才能 可以看到,并且只能真正执行 sql,在执行日志里面查看。在 20.6 版本引入了原生的执行计 划的语法。在 20.6.3 版本成为正式版本的功能。

基本语法

EXPLAIN [AST | SYNTAX | QUERY TREE | PLAN | PIPELINE | ESTIMATE | TABLE OVERRIDE] [setting = value, ...]
    [
      SELECT ... |
      tableFunction(...) [COLUMNS (...)] [ORDER BY ...] [PARTITION BY ...] [PRIMARY KEY] [SAMPLE BY ...] [TTL ...]
    ]
    [FORMAT ...]

示例:

explain select * from td_xplatform.stats_sd_campaign where profileId = 113434409578602 and date between '20220101' and '20220101';
-- 结果
Expression ((Projection + Before ORDER BY))
  ReadFromMergeTree (td_xplatform_local.stats_sd_campaign)


注意:ClickHouse官网也并没有过多讲解执行计划具体详细的解释,通过对sql执行的步骤分析出ClickHouse的执行计划是从底部往上逐一执行。

EXPLAIN 类型

  • PLAN:用于查看执行计划,默认值。
  • AST:用于查看语法树;
  • SYNTAX:用于优化语法,有时我们指定的查询语句未必是最优的,那么 ClickHouse 在底层会进行优化,EXPLAIN SYNTAX 可以返回对一条 SQL 语句进行优化后的结果。通过对比优化前和优化后的 SQL 语句,可以有助于我们理解 ClickHouse 的优化机制
  • PIPELINE:用于查看 PIPELINE 计划。
  • ESTIMATE:显示处理查询时要从表中读取的估计行数、标记数和部分数

注意:
explain默认值PLAN的结果不是那么的通俗易懂,可以使用setting进行查看详细的执行计划,判断sql是否合理化。

EXPLAIN PLAN

用于查看执行计划,默认值。以下是setting的key:

  • header:打印计划中各个步骤的 head 说明,默认关闭,默认值 0;
  • description:打印计划中各个步骤的描述,默认开启,默认值 1;
  • actions:打印计划中各个步骤的详细信息,默认关闭,默认值0。
  • json:以JSON格式将查询计划步骤打印为一行。默认:0
  • indexes:显示使用的索引、过滤部分的数量以及应用的每个索引的过滤颗粒数。默认值:0。支持MergeTree表。
    • NameSkip :索引名称(目前仅用于索引)。
    • Keys :索引使用的列数组。
    • Condition:使用情况。
    • DescriptionSkip:索引描述(目前仅用于索引)。
    • Parts:应用索引之前/之后的部分数。
    • Granules:应用索引之前/之后的颗粒数。

示例如下:

explain actions=1
select id,sum(totalCost) Spend
from xplatform_sd.sd_campaign ca
join td_xplatform.stats_sd_campaign report on ca.amazon_campaign_id = report.campaignId
where profileId = 113434409578602
group by id
order by Spend desc ;

-- 执行计划
Expression (Projection)
Actions: INPUT :: 0 -> id Int32 : 0
         INPUT : 1 -> sum(totalCost) Nullable(Decimal(38, 2)) : 1
         ALIAS sum(totalCost) :: 1 -> Spend Nullable(Decimal(38, 2)) : 2
Positions: 0 2
  Sorting (Sorting for ORDER BY)
  Sort description: sum(totalCost) DESC
    Expression (Before ORDER BY)
    Actions: INPUT :: 0 -> id Int32 : 0
             INPUT :: 1 -> sum(totalCost) Nullable(Decimal(38, 2)) : 1
    Positions: 0 1
      Aggregating
      Keys: id
      Aggregates:
          sum(totalCost)
            Function: sum(Nullable(Decimal(9, 2))) → Nullable(Decimal(38, 2))
            Arguments: totalCost
            Argument positions: 1
        Expression (Before GROUP BY)
        Actions: INPUT :: 0 -> id Int32 : 0
                 INPUT :: 1 -> totalCost Nullable(Decimal(9, 2)) : 1
        Positions: 0 1
          Filter (WHERE)
          Filter column: equals(profileId, 113434409578602) (removed)
          Actions: INPUT :: 0 -> id Int32 : 0
                   INPUT : 1 -> profileId Int64 : 1
                   INPUT :: 2 -> totalCost Nullable(Decimal(9, 2)) : 2
                   COLUMN Const(UInt64) -> 113434409578602_8 UInt64 : 3
                   FUNCTION equals(profileId :: 1, 113434409578602_8 :: 3) -> equals(profileId, 113434409578602) UInt8 : 4
          Positions: 0 2 4
            Join (JOIN)
              Expression (Before JOIN)
              Actions: INPUT :: 0 -> id Int32 : 0
                       INPUT :: 1 -> amazon_campaign_id Nullable(Int64) : 1
                       INPUT : 2 -> profile_id Nullable(Int64) : 2
                       COLUMN Const(UInt64) -> 113434409578602 UInt64 : 3
                       FUNCTION equals(profile_id :: 2, 113434409578602 :: 3) -> equals(profile_id, 113434409578602) Nullable(UInt8) : 4
              Positions: 0 1 4
                SettingQuotaAndLimits (Set limits and quota after reading from storage)
                  ReadFromStorage (MySQL)
              Expression ((Joined actions + Rename joined columns))
              Actions: INPUT : 0 -> profileId Int64 : 0
                       INPUT : 1 -> campaignId Int64 : 1
                       INPUT : 2 -> totalCost Nullable(Decimal(9, 2)) : 2
                       ALIAS profileId :: 0 -> profileId Int64 : 3
                       ALIAS campaignId :: 1 -> campaignId Int64 : 0
                       ALIAS totalCost :: 2 -> totalCost Nullable(Decimal(9, 2)) : 1
              Positions: 0 3 1
                SettingQuotaAndLimits (Set limits and quota after reading from storage)
                  Expression ((Projection + Before ORDER BY))
                  Actions: INPUT :: 0 -> profileId Int64 : 0
                           INPUT :: 1 -> campaignId Int64 : 1
                           INPUT :: 2 -> totalCost Nullable(Decimal(9, 2)) : 2
                  Positions: 0 1 2
                    SettingQuotaAndLimits (Set limits and quota after reading from storage)
                      ReadFromMergeTree
                      ReadType: Default
                      Parts: 843
                      Granules: 936

简单解释一下特殊的名词:

  • SettingQuotaAndLimits: Set limits and quota after reading from storage(从存储读取后设置限制和配额)
  • Positions:当前执行计划的位置
  • ReadFromMergeTree:从MergeTree读取数据
  • ReadFromStorage:从存储读取数据
  • Filter (WHERE):是 WHERE 或 HAVING 的实现。拦截过滤数据
  • Sorting (Sorting for ORDER BY):排序
  • Expression:计算列上的函数,例如 x, y -> x + 1, y * 2。使用 explain actions = 1 select … 查看更多详细信息
  • Aggregating:负责 GROUP BY。

EXPLAIN AST

查看查询的抽象语法树(AST)。支持所有类型的查询,而不仅仅是 SELECT。

示例如下:

EXPLAIN AST ALTER TABLE t1 DELETE WHERE date = today();
--执行计划
AlterQuery  t1 (children 1)
   ExpressionList (children 1)
    AlterCommand 27 (children 1)
     Function equals (children 1)
      ExpressionList (children 2)
       Identifier date
       Function today (children 1)
        ExpressionList

EXPLAIN SYNTAX

在语法优化后返回查询。

可以使用此语法进行判断sql是否是当前查询方式的最佳性能,可以查看一些sql是会进行谓词下推

示例如下:

explain syntax
select target.expression,
       sum(totalCost) Spend
from (select targetId, totalCost,date from td_xplatform.stats_sd_target where  profileId = 113434409578602) report
left join (select amazon_target_id,expression from xplatform_sd.sd_product_ad_target where profile_id = 113434409578602) target on report.targetId = target.amazon_target_id
where date between '20220101' and '20220202'
group by target.expression
order by Spend;
-- 执行计划如下
SELECT
    expression,
    sum(totalCost) AS Spend
FROM
(
    SELECT
        targetId,
        totalCost,
        date
    FROM td_xplatform.stats_sd_target
    WHERE (profileId = 113434409578602) AND ((date <= '20220202') AND (date >= '20220101'))
) AS report
ALL LEFT JOIN
(
    SELECT
        amazon_target_id,
        expression
    FROM xplatform_sd.sd_product_ad_target
    WHERE profile_id = 113434409578602
) AS target ON targetId = amazon_target_id
WHERE (date >= '20220101') AND (date <= '20220202')
GROUP BY expression
ORDER BY Spend ASC

EXPLAIN PIPELINE

  • header: 打印计划中各个步骤的 head 说明,默认关闭;
  • graph: 用DOT图形语言描述管道图,默认关闭,需要查看相关的图形需要配合graphviz 查看;
  • compact:如果开启了 graph,紧凑打印行开关。1:开启(默认值),0:关闭;

示例如下:

explain pipeline 
select sum(totalCost) 
from td_xplatform.stats_sd_campaign 
where profileId = 113434409578602
group by campaignId;
-- 结果如下
(SettingQuotaAndLimits)
  (Expression)
  ExpressionTransform
    (Aggregating)
    Resize 4 → 1
      AggregatingTransform × 4
        StrictResize 4 → 4
          (Expression)
          ExpressionTransform × 4
            (SettingQuotaAndLimits)
              (ReadFromMergeTree)
              MergeTreeThread × 4 0 → 1

简单解释一下特殊的名词:

  • SettingQuotaAndLimits: Set limits and quota after reading from storage(从存储读取后设置限制和配额)
  • AggregatingTransform x 4:4次聚合转换,是 Group By 高性能的核心所在。原因是因为ClickHouse是一个很耗cpu的数据库,会在不同的线程上进行统计计算,有多少线程取决于ClickHouse服务器多少核,多少线程。
  • ReadFromMergeTree:从MergeTree读取数据到内存中

EXPLAIN ESTIMATE

显示处理查询时要从表中读取的估计行数、标记数和部分数。使用MergeTree系列中的表。

示例如下:

explain estimate select sum(totalCost) from td_xplatform.stats_sd_target where profileId = 113434409578602 and date between '20220101' and '20220202';

补充

  • CreatingSets 为 IN(子查询)或哈希连接(在此查询中)填充哈希表。
  • ReadFromPreparedSource 实际上是从远程表中读取。
  • Union 是 UNION 的一个实现,或者只是来自多个来源
  • MergingAggregated 也是 GROUP BY 的一部分,它将来自不同来源(这里是远程和本地)的聚合函数状态合并在一起。

忠告

  • 尽量将查询条件靠近表
  • 尽量避免join,使用in代替join
  • 如果join的话,尽量join子查询,必须带上primary key
  • 右表尽可能的小
完
  • 本文作者:Java技术债务
  • 原文链接: https://cuizb.top/myblog/article/1683518225
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY 3.0 CN协议进行许可。转载请署名作者且注明文章出处。
阅读全文
Java技术债务

Java技术债务

Java技术债务
Java技术债务
热门文章
  1. ClickHouse使用过程中的一些查询优化(六)2003
  2. MySQL数据库被攻击,被删库勒索,逼迫我使出洪荒之力进行恢复数据764
  3. MySQL主从同步原理458
  4. 线程池的理解以及使用414
  5. Spring Cloud Gateway整合nacos实战(三)409
分类
  • Java
    30篇
  • 设计模式
    27篇
  • 数据库
    20篇
  • Spring
    18篇
  • MySQL
    13篇
  • ClickHouse
    11篇
  • Kubernetes
    10篇
  • Redis
    9篇
  • Docker
    8篇
  • SpringBoot
    7篇
  • JVM
    6篇
  • Linux
    5篇
  • Spring Cloud
    5篇
  • 多线程
    5篇
  • Netty
    4篇
  • Kafka
    4篇
  • 面经
    4篇
  • Nginx
    3篇
  • JUC
    3篇
  • 随笔
    2篇
  • 分布式
    1篇
  • MyBatis
    1篇
  • 报错合集
    1篇
  • 生活记录
    1篇
  • 源码
    1篇
  • 性能优化
    1篇

最新评论

  • MySQL数据库被攻击,被删库勒索,逼迫我使出洪荒之力进行恢复数据2022-05-06
    Java技术债务:@capture 一起探讨学习,服务器被黑很正常,及时做好备份以及做好防护
  • MySQL数据库被攻击,被删库勒索,逼迫我使出洪荒之力进行恢复数据2022-04-13
    capture:我的刚上线两天,网站里就两篇文章也被攻击了,纳闷
  • Java常用集合List、Map、Set介绍以及一些面试问题2022-01-18
    Java技术债务:HashSet和TreeSet 相同点:数据不能重复 不同点: 1、底层存储结构不同; HashSet底层使用HashMap哈希表存储 TreeSet底层使用TreeMap树结构存储 2、唯一性方式不同 HashSet底层使用hashcode()和equal()方法判断 TreeSet底层使用Comparable接口的compareTo判断的 3、HashSet无序,TreeSet有序
  • undefined2021-12-14
    Java技术债务:如果不指定线程池,CompletableFuture会默认使用ForkJoin线程池,如果同一时间出现大量请求的话,会出现线程等待问题,建议使用自定义线程池。。。
  • undefined2021-12-02
    you:很好,对于小白相当不错了,谢谢
  • CSDN
  • 博客园
  • 程序猿DD
  • 纯洁的微笑
  • spring4all
  • 廖雪峰的官方网站
  • 猿天地
  • 泥瓦匠BYSocket
  • crossoverJie
  • 张先森个人博客
  • 越加网

© 2021-2023 Java技术债务 - Java技术债务 版权所有
总访问量 0 次 您是本文第 0 位童鞋
豫ICP备2021034516号
Java技术债务 豫公网安备 51011402000164号

微信公众号

Java技术债务
Java技术债务

专注于Spring,SpringBoot等后端技术探索

以及MySql数据库开发和Netty等后端流行框架学习

日志
分类
标签
RSS

有不足之处也希望各位前辈指出