记一次clickhouse查询优化之惰性物化
记一次clickhouse查询优化之惰性物化
一、起因
测试clickhouse在10亿数据量下的查询性能,当limit<=10时,为21秒,当limit>10时,为63秒。下面是测试步骤。
- 建表语句
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 ctSETTINGS index_granularity = 8192;
- 查询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
- 查询结果
经过where过滤后,共20万条数据。
二、排查过程
- 查看执行计划
limit 10查询计划如下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 100时查询计划如下┌─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 10 时多了一行两行 LazilyRead (Lazily Read) 和 Lazily read columns: dip, sp, _id, dp,根据字面意思,可以猜测这个懒加载导致——当 limit 小于10时,会先加载条件中的sip和排序的ct,延迟加载其他列,而超过10时,会直接加载查询字段、条件、排序字段所有列,从而导致查询时间暴涨。┌─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──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
- 搜索原因
通过搜索引擎检索,查询到下面这篇文章。
https://clickhouse.com/blog/clickhouse-gets-lazier-and-faster-introducing-lazy-materialization
clickhouse在25.4中通过引入惰性物化,实现了延迟加载查询的列,从而大大减少需要从磁盘读取的数据。
下面是一段简单的介绍,详情可以查看原文。- 当查询上文中的sql时,最简单粗暴的方式就是读取select、where、order by中的所有列的所有行,然后先根据 where 过滤,再根据 order by 排序,最后取前 limit 条。这样会导致从磁盘读取所有列的所有行,性能最差。
- 这里很自然能想到,为什么不先读取 where 中的列过滤,其他列仅读取过滤后的行呢?于是 clickhouse 会自动执行 prewhere 优化,即通过自动判断,将 where 中的一些条件挪到 prewhere 中(这部分本文不做介绍,prewhere 也可以手动指定,详情可以去官网找),先只读取 prewhere 中的列的所有行,执行过滤,再取 select、order by 中的列时,仅读取过滤后的行即可。但是,由于 clickhouse 是以 granule (默认8192行)为单位存储的,所以需要读取其他列中过滤后的行对应的 granule。上文示例查询到20万行,由于是随机数据均匀分布,这20万行基本对应了全部 granule,所以 prewhere 优化没有起到什么效果。
- 那么最后一点,能不能先执行 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 或以上,以大幅度减少需要读取的数据量,加速查询时间。