记一次clickhouse查询优化之惰性物化

记一次clickhouse查询优化之惰性物化

一、起因

测试clickhouse在10亿数据量下的查询性能,当limit<=10时,为21秒,当limit>10时,为63秒。下面是测试步骤。

  1. 建表语句

    sql 复制代码
    create table test
    (
        _id  String,
        ct   UInt32,
        sip  IPv4,
        dip  IPv4,
        sp   UInt16,
        dp   UInt16
    )
        engine = MergeTree PARTITION BY toYYYYMMDD(toDateTime(ct, 'Asia/Shanghai'))
            ORDER BY ct
            SETTINGS index_granularity = 8192;
  2. 查询sql

    sql 复制代码
    select _id,ct,sip,dip,sp,dp from test where sip = '192.168.0.1' order by ct desc limit 10 settings optimize_read_in_order=0
  3. 查询结果
    经过where过滤后,共20万条数据。

二、排查过程

  1. 查看执行计划

    sql 复制代码
    explain actions=1 select _id,ct,sip,dip,sp,dp from test where sip = '192.168.0.1' order by ct desc limit 10 settings optimize_read_in_order=0

    limit 10查询计划如下

    txt 复制代码
     	    ┌─explain────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
     1. │ Expression (Project names)                                                                                                         │
     2. │ Actions: INPUT : 0 -> __table1._id String : 0                                                                                      │
     3. │          INPUT : 1 -> __table1.ct UInt32 : 1                                                                                       │
     4. │          INPUT : 2 -> __table1.sip IPv4 : 2                                                                                        │
     5. │          INPUT : 3 -> __table1.dip IPv4 : 3                                                                                        │
     6. │          INPUT : 4 -> __table1.sp UInt16 : 4                                                                                       │
     7. │          INPUT : 5 -> __table1.dp UInt16 : 5                                                                                       │
     8. │          ALIAS __table1._id :: 0 -> _id String : 6                                                                                 │
     9. │          ALIAS __table1.ct :: 1 -> ct UInt32 : 0                                                                                   │
    10. │          ALIAS __table1.sip :: 2 -> sip IPv4 : 1                                                                                   │
    11. │          ALIAS __table1.dip :: 3 -> dip IPv4 : 2                                                                                   │
    12. │          ALIAS __table1.sp :: 4 -> sp UInt16 : 3                                                                                   │
    13. │          ALIAS __table1.dp :: 5 -> dp UInt16 : 4                                                                                   │
    14. │ Positions: 6 0 1 2 3 4                                                                                                             │
    15. │   LazilyRead (Lazily Read)                                                                                                         │
    16. │   Lazily read columns: dip, sp, _id, dp                                                                                            │
    17. │     Limit (preliminary LIMIT (without OFFSET))                                                                                     │
    18. │     Limit 10                                                                                                                       │
    19. │     Offset 0                                                                                                                       │
    20. │       Sorting (Sorting for ORDER BY)                                                                                               │
    21. │       Sort description: __table1.ct DESC                                                                                           │
    22. │       Limit 10                                                                                                                     │
    23. │         Expression ((Before ORDER BY + Projection))                                                                                │
    24. │         Actions: INPUT :: 0 -> __table1._id String : 0                                                                             │
    25. │                  INPUT :: 1 -> __table1.ct UInt32 : 1                                                                              │
    26. │                  INPUT :: 2 -> __table1.sip IPv4 : 2                                                                               │
    27. │                  INPUT :: 3 -> __table1.dip IPv4 : 3                                                                               │
    28. │                  INPUT :: 4 -> __table1.sp UInt16 : 4                                                                              │
    29. │                  INPUT :: 5 -> __table1.dp UInt16 : 5                                                                              │
    30. │         Positions: 1 0 2 3 4 5                                                                                                     │
    31. │           Expression                                                                                                               │
    32. │           Actions: INPUT : 0 -> _id String : 0                                                                                     │
    33. │                    INPUT : 1 -> ct UInt32 : 1                                                                                      │
    34. │                    INPUT : 3 -> dip IPv4 : 2                                                                                       │
    35. │                    INPUT : 4 -> sp UInt16 : 3                                                                                      │
    36. │                    INPUT : 5 -> dp UInt16 : 4                                                                                      │
    37. │                    INPUT : 2 -> sip IPv4 : 5                                                                                       │
    38. │                    ALIAS _id :: 0 -> __table1._id String : 6                                                                       │
    39. │                    ALIAS ct :: 1 -> __table1.ct UInt32 : 0                                                                         │
    40. │                    ALIAS dip :: 2 -> __table1.dip IPv4 : 1                                                                         │
    41. │                    ALIAS sp :: 3 -> __table1.sp UInt16 : 2                                                                         │
    42. │                    ALIAS dp :: 4 -> __table1.dp UInt16 : 3                                                                         │
    43. │                    ALIAS sip :: 5 -> __table1.sip IPv4 : 4                                                                         │
    44. │           Positions: 6 0 4 1 2 3                                                                                                   │
    45. │             ReadFromMergeTree (default.test)                                                                                       │
    46. │             ReadType: Default                                                                                                      │
    47. │             Parts: 9                                                                                                               │
    48. │             Granules: 122500                                                                                                       │
    49. │             Prewhere info                                                                                                          │
    50. │             Need filter: 1                                                                                                         │
    51. │               Prewhere filter                                                                                                      │
    52. │               Prewhere filter column: equals(__table1.sip, '192.168.0.1'_String) (removed)                                         │
    53. │               Actions: INPUT : 0 -> sip IPv4 : 0                                                                                   │
    54. │                        COLUMN Const(String) -> '192.168.0.1'_String String : 1                                                     │
    55. │                        FUNCTION equals(sip : 0, '192.168.0.1'_String :: 1) -> equals(__table1.sip, '192.168.0.1'_String) UInt8 : 2 │
    56. │               Positions: 0 2                                                                                                       │
        └─explain────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

    limit 100时查询计划如下

    txt 复制代码
     	    ┌─explain──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
     1. │ Expression (Project names)                                                                                                       │
     2. │ Actions: INPUT : 0 -> __table1._id String : 0                                                                                    │
     3. │          INPUT : 1 -> __table1.ct UInt32 : 1                                                                                     │
     4. │          INPUT : 2 -> __table1.sip IPv4 : 2                                                                                      │
     5. │          INPUT : 3 -> __table1.dip IPv4 : 3                                                                                      │
     6. │          INPUT : 4 -> __table1.sp UInt16 : 4                                                                                     │
     7. │          INPUT : 5 -> __table1.dp UInt16 : 5                                                                                     │
     8. │          ALIAS __table1._id :: 0 -> _id String : 6                                                                               │
     9. │          ALIAS __table1.ct :: 1 -> ct UInt32 : 0                                                                                 │
    10. │          ALIAS __table1.sip :: 2 -> sip IPv4 : 1                                                                                 │
    11. │          ALIAS __table1.dip :: 3 -> dip IPv4 : 2                                                                                 │
    12. │          ALIAS __table1.sp :: 4 -> sp UInt16 : 3                                                                                 │
    13. │          ALIAS __table1.dp :: 5 -> dp UInt16 : 4                                                                                 │
    14. │ Positions: 6 0 1 2 3 4                                                                                                           │
    15. │   Limit (preliminary LIMIT (without OFFSET))                                                                                     │
    16. │   Limit 100                                                                                                                      │
    17. │   Offset 0                                                                                                                       │
    18. │     Sorting (Sorting for ORDER BY)                                                                                               │
    19. │     Sort description: __table1.ct DESC                                                                                           │
    20. │     Limit 100                                                                                                                    │
    21. │       Expression ((Before ORDER BY + Projection))                                                                                │
    22. │       Actions: INPUT :: 0 -> __table1._id String : 0                                                                             │
    23. │                INPUT :: 1 -> __table1.ct UInt32 : 1                                                                              │
    24. │                INPUT :: 2 -> __table1.sip IPv4 : 2                                                                               │
    25. │                INPUT :: 3 -> __table1.dip IPv4 : 3                                                                               │
    26. │                INPUT :: 4 -> __table1.sp UInt16 : 4                                                                              │
    27. │                INPUT :: 5 -> __table1.dp UInt16 : 5                                                                              │
    28. │       Positions: 1 0 2 3 4 5                                                                                                     │
    29. │         Expression                                                                                                               │
    30. │         Actions: INPUT : 0 -> _id String : 0                                                                                     │
    31. │                  INPUT : 1 -> ct UInt32 : 1                                                                                      │
    32. │                  INPUT : 3 -> dip IPv4 : 2                                                                                       │
    33. │                  INPUT : 4 -> sp UInt16 : 3                                                                                      │
    34. │                  INPUT : 5 -> dp UInt16 : 4                                                                                      │
    35. │                  INPUT : 2 -> sip IPv4 : 5                                                                                       │
    36. │                  ALIAS _id :: 0 -> __table1._id String : 6                                                                       │
    37. │                  ALIAS ct :: 1 -> __table1.ct UInt32 : 0                                                                         │
    38. │                  ALIAS dip :: 2 -> __table1.dip IPv4 : 1                                                                         │
    39. │                  ALIAS sp :: 3 -> __table1.sp UInt16 : 2                                                                         │
    40. │                  ALIAS dp :: 4 -> __table1.dp UInt16 : 3                                                                         │
    41. │                  ALIAS sip :: 5 -> __table1.sip IPv4 : 4                                                                         │
    42. │         Positions: 6 0 4 1 2 3                                                                                                   │
    43. │           ReadFromMergeTree (default.test)                                                                                       │
    44. │           ReadType: Default                                                                                                      │
    45. │           Parts: 9                                                                                                               │
    46. │           Granules: 122500                                                                                                       │
    47. │           Prewhere info                                                                                                          │
    48. │           Need filter: 1                                                                                                         │
    49. │             Prewhere filter                                                                                                      │
    50. │             Prewhere filter column: equals(__table1.sip, '192.168.0.1'_String) (removed)                                         │
    51. │             Actions: INPUT : 0 -> sip IPv4 : 0                                                                                   │
    52. │                      COLUMN Const(String) -> '192.168.0.1'_String String : 1                                                     │
    53. │                      FUNCTION equals(sip : 0, '192.168.0.1'_String :: 1) -> equals(__table1.sip, '192.168.0.1'_String) UInt8 : 2 │
    54. │             Positions: 0 2                                                                                                       │
        └─explain──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

    可以看到执行计划主要差异就是 limit 10 时多了一行两行 LazilyRead (Lazily Read)Lazily read columns: dip, sp, _id, dp,根据字面意思,可以猜测这个懒加载导致------当 limit 小于10时,会先加载条件中的sip和排序的ct,延迟加载其他列,而超过10时,会直接加载查询字段、条件、排序字段所有列,从而导致查询时间暴涨。

  2. 搜索原因
    通过搜索引擎检索,查询到下面这篇文章。
    https://clickhouse.com/blog/clickhouse-gets-lazier-and-faster-introducing-lazy-materialization
    clickhouse在25.4中通过引入惰性物化,实现了延迟加载查询的列,从而大大减少需要从磁盘读取的数据。
    下面是一段简单的介绍,详情可以查看原文。

    1. 当查询上文中的sql时,最简单粗暴的方式就是读取select、where、order by中的所有列的所有行,然后先根据 where 过滤,再根据 order by 排序,最后取前 limit 条。这样会导致从磁盘读取所有列的所有行,性能最差。
    2. 这里很自然能想到,为什么不先读取 where 中的列过滤,其他列仅读取过滤后的行 呢?于是 clickhouse 会自动执行 prewhere 优化,即通过自动判断,将 where 中的一些条件挪到 prewhere 中(这部分本文不做介绍,prewhere 也可以手动指定,详情可以去官网找),先只读取 prewhere 中的列的所有行,执行过滤,再取 select、order by 中的列时,仅读取过滤后的行即可。但是,由于 clickhouse 是以 granule (默认8192行)为单位存储的,所以需要读取其他列中过滤后的行对应的 granule。上文示例查询到20万行,由于是随机数据均匀分布,这20万行基本对应了全部 granule,所以 prewhere 优化没有起到什么效果。
    3. 那么最后一点,能不能先执行 limit,其他列只取 limit 结果中的10条数据对应的行 呢?当然可以,这就是惰性物化 的作用。那么整体流程就是,先读取 prewhere 中的列的所有行,过滤后,再读取 order by 中的列,排序后,根据 limit 取10行,最后读取 select 中的列时,仅读取这10行对应的 granule 即可。通过设置query_plan_max_limit_for_lazy_materialization可以配置 limit 不高于多少时开启惰性物化,如set query_plan_max_limit_for_lazy_materialization = 100,设置为0代表不限制。

三、总结

clickhouse 25.4及以上版本,当需要取Top N时,一定要设置惰性物化 limit 阈值为 N 或以上,以大幅度减少需要读取的数据量,加速查询时间。