「ナゲッツ・セーリングプログラム」に参加しています
クリックハウスとオラクル
オープンソースの分析データベース ClickHouse は高速であることで知られていますが、本当ですか? 比較テストで検証してみましょう。
最初に ClickHouse (CH と呼ばれる) と Oracle データベース (ORA と呼ばれる) を使用して、同じソフトウェアとハードウェア環境で比較テストを行います。テスト ベンチマークは、国際的に認められた TPC-H を使用して、8 つのテーブルに対して 22 の SQL ステートメントで定義された計算要件 (Q1 から Q22) を完了します。テストでは 12 スレッドの単一マシンを使用し、合計データ サイズは 100G です。TPC-H に対応する SQL は比較的長いため、ここでは詳しく説明しません。
Q1 は単純な単一テーブル トラバーサル計算グループのまとめです。比較テストの結果は次のとおりです。
Q1 を計算する CH のパフォーマンスは ORA よりも優れており、CH の列ストレージがうまく機能しており、単一テーブルのトラバーサルが高速であることを示しています。ORA の主な欠点は、行ストレージを使用することです。これは明らかに非常に低速です。
しかし、計算の複雑さを増すと、CH はどのように機能するのでしょうか? 続いて、TPC-H の Q2、Q3、および Q7 を見てください。テスト結果は次のとおりです。
計算がより複雑になった後、CH のパフォーマンスは大幅に低下しました。Q2 はデータ量が少なく、列ストレージの影響はほとんどなく、CH のパフォーマンスは ORA とほぼ同じです。Q3はデータ量が多く、CHはカラムストレージを活かしてORAを追い抜いた。Q7 のデータも大きいですが、計算が複雑で、CH のパフォーマンスは ORA ほど良くありません。
複雑な計算を高速に実行できるかどうかは、主にパフォーマンス最適化エンジンがうまく機能しているかどうかに依存します。CH の列ストレージはストレージの大きな利点を占めていますが、ORA の行ストレージに追い抜かれています。これは、CH のアルゴリズム最適化能力が ORA よりもはるかに低いことを示しています。
TPC-H の Q8 はより複雑な計算です. サブクエリに複数のテーブル結合があります. CH は 2000 秒以上実行されましたが、それでも結果が得られません. スタックする必要があり、ORA は 192 秒間実行されました. Q9 は Q8 のサブクエリと同様に追加され、CH はメモリ不足エラーを直接報告し、ORA は 234 秒間実行されました。CH では実行できない複雑な操作が他にもいくつかあるため、全体的な比較を行う方法はありません。
CH と ORA はどちらも SQL 言語に基づいていますが、CH は ORA が最適化できるステートメントを実行できません。これは、CH の最適化エンジンが比較的貧弱であることをさらに証明しています。
CH は単一テーブルのトラバーサル操作を行うことだけが得意であり、関連付けられた操作がある場合は MySQL を実行することさえできないと噂されていますが、それは間違いではないようです。CH を使用したい学生は、それを比較検討する必要があります.このシナリオはどの程度適応できるでしょうか?
esProc SPL の登場
开源esProc SPL也是以高性能作为宣传点,那么我们再来比较一下。
仍然是跑TPC-H来看 :
Q2、Q3、Q7这些较复杂的运算,SPL比CH和ORA跑的都快。CH跑不出结果的Q8、Q9,SPL分别跑了37秒和68秒,也比ORA快。原因在于SPL可以采用更优的算法,其计算复杂度低于被ORA优化过的SQL,更远低于CH执行的SQL,再加上列存,最终是用Java开发的SPL跑赢了C++实现的CH和ORA。
大概可以得到结论,esProc SPL无论做简单计算,还是复杂计算性能都非常好。
不过,Q1这种简单运算,CH比SPL还是略胜了一筹。似乎可以进一步证明前面的结论,即CH特别擅长简单遍历运算。
且慢,SPL还有秘密武器。
SPL的企业版中提供了列式游标机制,我们再来对比测试一下:在8亿条数据量下,做最简单的分组汇总计算,对比SPL(使用列式游标)和CH的性能。(采用的机器配置比前面做TPC-H测试时略低,因此测出的结果不同,不过这里主要看相对值。)
简单分组汇总对应CH的SQL语句是:
SQL1:
SELECT mod(id, 100) AS Aid, max(amount) AS Amax FROM test.t GROUP BY mod(id, 100)
这个测试的结果是下图这样:
SPL使用列式游标机制之后,简单遍历分组计算的性能也和CH一样了。如果在TPC-H的Q1测试中也使用列式游标,SPL也会达到和CH同样的性能。
测试过程中发现,8亿条数据存成文本格式占用磁盘15G,在CH中占用5.4G,SPL占用8G。说明CH和SPL都采用了压缩存储,CH的压缩比更高些,也进一步证明CH的存储引擎做得确实不错。不过,SPL也可以达到和CH同样的性能,这说明SPL存储引擎和算法优化做得都比较好,高性能计算能力更加均衡。
当前版本的SPL是用Java写的,Java读数后生成用于计算的对象的速度很慢,而用C++开发的CH则没有这个问题。对于复杂的运算,读数时间占比不高,Java生成对象慢造成的拖累还不明显;而对于简单的遍历运算,读数时间占比很高,所以前面测试中SPL就会比CH更慢。列式游标优化了读数方案,不再生成一个个小对象,使对象生成次数大幅降低,这时候就能把差距拉回来了。单纯从存储本身看,SPL和CH相比并没有明显的优劣之分。
接下来再看常规TopN的对比测试,CH的SQL是:
SQL2:
SELECT * FROM test.t ORDER BY amount DESC LIMIT 100
对比测试结果是这样的:
单看CH的SQL2,常规TopN的计算方法是全排序后取出前N条数据。数据量很大时,如果真地做全排序,性能会非常差。SQL2的测试结果说明,CH应该和SPL一样做了优化,没有全排序,所以两者性能都很快,SPL稍快一些。
也就是说,无论简单运算还是复杂运算,esProc SPL都能更胜一筹。
进一步的差距
差距还不止于此。
正如前面所说,CH和ORA使用SQL语言,都是基于关系模型的,所以都面临SQL优化的问题。TPC-H测试证明,ORA能优化的一些场景CH却优化不了,甚至跑不出结果。那么,如果面对一些ORA也不会优化的计算,CH就更不会优化了。比如说我们将SQL1的简单分组汇总,改为两种分组汇总结果再连接,CH的SQL写出来大致是这样:
SQL3:
SELECT * FROM ( SELECT mod(id, 100) AS Aid, max(amount) AS Amax FROM test.t GROUP BY mod(id, 100) ) A JOIN ( SELECT floor(id / 200000) AS Bid, min(amount) AS Bmin FROM test.t GROUP BY floor(id / 200000) ) B ON A.Aid = B.Bid
这种情况下,对比测试的结果是CH的计算时间翻倍,SPL则不变:
这是因为SPL不仅使用了列式游标,还使用了遍历复用机制,能在一次遍历过程中计算出多种分组结果,可以减少很多硬盘访问量。CH使用的SQL无法写出这样的运算,只能靠CH自身的优化能力了。而CH算法优化能力又很差,其优化引擎在这个测试中没有起作用,只能遍历两次,所以性能下降了一倍。
SPL实现遍历复用的代码很简单,大致是这样:
A | B | |
1 | =file("topn.ctx").open().cursor@mv(id,amount) | |
2 | cursor A1 | =A2.groups(id%100:Aid;max(amount):Amax) |
3 | cursor | =A3.groups(id\200000:Bid;min(amount):Bmin) |
4 | =A2.join@i(Aid,A3:Bid,Bid,Bmin) |
再将SQL2常规TopN计算,调整为分组后求组内TopN。对应SQL是:
SQL4:
SELECT gid, groupArray(100)(amount) AS amount FROM ( SELECT mod(id, 10) AS gid, amount FROM test.topn ORDER BY gid ASC, amount DESC ) AS a GROUP BY gid
这个分组TopN测试的对比结果是下面这样的:
CH做分组TopN计算比常规TopN慢了42倍,说明CH在这种情况下很可能做了排序动作。也就是说,情况复杂化之后,CH的优化引擎又不起作用了。与SQL不同,SPL把TopN看成是一种聚合运算,和sum、count这类运算的计算逻辑是一样的,都只需要对原数据遍历一次。这样,分组求组内TopN就和分组求和、计数一样了,可以避免排序计算。因此,SPL计算分组TopN比CH快了22倍。
而且,SPL计算分组TopN的代码也不复杂:
A | |
1 | =file("topn.ctx").open().cursor@mv(id,amount) |
2 | =A1.groups(id%10:gid;top(10;-amount)).news(#2;gid,~.amount) |
不只是跑得快
再来看看电商系统中常见的漏斗运算。SPL的代码依然很简洁:
A | B | |
1 | =["etype1","etype2","etype3"] | =file("event.ctx").open() |
2 | =B1.cursor(id,etime,etype;etime>=date("2021-01-10") && etime<date("2021-01-25") && A1.contain(etype) && …) | |
3 | =A2.group(id).(~.sort(etime)) | =A3.new(~.select@1(etype==A1(1)):first,~:all).select(first) |
4 | =B3.(A1.(t=if(#==1,t1=first.etime,if(t,all.select@1(etype==A1.~ && etime>t && etime<t1+7).etime, null)))) | |
5 | =A4.groups(;count(~(1)):STEP1,count(~(2)):STEP2,count(~(3)):STEP3) |
CH的SQL无法实现这样的计算,我们以ORA为例看看三步漏斗的SQL写法:
with e1 as ( select gid,1 as step1,min(etime) as t1 from T where etime>= to_date('2021-01-10', 'yyyy-MM-dd') and etime= to_date('2021-01-10', 'yyyy-MM-dd') and e2.etime t1 and e2.etime < t1 + 7 and eventtype='eventtype2' and … group by 1 ), with e3 as ( select gid,1 as step3,min(e2.t1) as t1,min(e3.etime) as t3 from T as e3 inner join e2 on e3.gid = e2.gid where e3.etime>= to_date('2021-01-10', 'yyyy-MM-dd') and e3.etime t2 and e3.etime < t1 + 7 and eventtype='eventtype3' and … group by 1 ) select sum(step1) as step1, sum(step2) を step2 として、 sum(step3) を step3 として e1左 からe1.gid で e2 に結合 = e2.gid 左 e2.gid で e3 に結合 = e3.gid
ORA の SQL は 30 行以上も書く必要があり、かなりわかりにくいです。そして、このコードは目標到達プロセスのステップ数に関連しており、追加の各ステップには追加のサブクエリが必要です。対照的に、SPL ははるかに単純で、任意の数のステップを処理するのはこのコードです。
この種の複雑な SQL を記述するのは非常に面倒であり、パフォーマンスの最適化について語ることは不可能です。
そして、CH の SQL は ORA に比べてはるかに劣っており、基本的にそのような複雑なロジックを記述できず、C++ コードを外部でしか記述できません。つまり、この場合、CHのストレージエンジンしか使えません。C++ では外部計算を使用して良好なパフォーマンスを得ることができますが、開発コストは非常に高くなります。同様の例は数多くあり、CH はそれらを直接実装することはできません。
要約すると、CH は SPL とほぼ同じパフォーマンスで、いくつかの単純なシナリオ (単一テーブルのトラバーサルなど) を非常に高速に計算します。ただし、ハイ パフォーマンス コンピューティングは、単純な状況の速度だけでなく、さまざまなシナリオを検討することもできます。複雑な操作の場合、SPL は CH よりもはるかに優れたパフォーマンスを発揮するだけでなく、コードの記述もはるかに簡単になります。SPL は高性能データ コンピューティングのすべてのシーンをカバーでき、CH の完全な勝利と言えます。