From 075403e56308388de3a137bb06b355b64c7288ff Mon Sep 17 00:00:00 2001 From: Sundar Annamalai Date: Wed, 29 Sep 2021 17:20:47 -0400 Subject: [PATCH] Update Star tree cube docs. --- hetu-docs/en/develop/star-tree-cube.md | 148 ------------ .../en/images/cube-logical-plan-optimizer.png | Bin 0 -> 38432 bytes hetu-docs/en/index.md | 9 +- hetu-docs/en/preagg/overview.md | 221 ++++++++++++++++++ hetu-docs/en/preagg/statements.md | 175 ++++++++++++++ hetu-docs/en/sql/create-cube.md | 69 ------ hetu-docs/en/sql/drop-cube.md | 33 --- hetu-docs/en/sql/insert-cube.md | 44 ---- hetu-docs/en/sql/insert-overwrite-cube.md | 28 --- hetu-docs/en/sql/show-cubes.md | 35 --- hetu-docs/zh/develop/star-tree-cube.md | 70 ------ hetu-docs/zh/index.md | 2 +- hetu-docs/zh/sql/create-cube.md | 51 ---- hetu-docs/zh/sql/drop-cube.md | 27 --- hetu-docs/zh/sql/insert-cube.md | 55 ----- hetu-docs/zh/sql/insert-overwrite-cube.md | 22 -- hetu-docs/zh/sql/show-cubes.md | 29 --- 17 files changed, 402 insertions(+), 616 deletions(-) delete mode 100644 hetu-docs/en/develop/star-tree-cube.md create mode 100644 hetu-docs/en/images/cube-logical-plan-optimizer.png create mode 100644 hetu-docs/en/preagg/overview.md create mode 100644 hetu-docs/en/preagg/statements.md delete mode 100644 hetu-docs/en/sql/create-cube.md delete mode 100644 hetu-docs/en/sql/drop-cube.md delete mode 100644 hetu-docs/en/sql/insert-cube.md delete mode 100644 hetu-docs/en/sql/insert-overwrite-cube.md delete mode 100644 hetu-docs/en/sql/show-cubes.md delete mode 100644 hetu-docs/zh/develop/star-tree-cube.md delete mode 100644 hetu-docs/zh/sql/create-cube.md delete mode 100644 hetu-docs/zh/sql/drop-cube.md delete mode 100644 hetu-docs/zh/sql/insert-cube.md delete mode 100644 hetu-docs/zh/sql/insert-overwrite-cube.md delete mode 100644 hetu-docs/zh/sql/show-cubes.md diff --git a/hetu-docs/en/develop/star-tree-cube.md b/hetu-docs/en/develop/star-tree-cube.md deleted file mode 100644 index 8eb54f0c7..000000000 --- a/hetu-docs/en/develop/star-tree-cube.md +++ /dev/null @@ -1,148 +0,0 @@ -# Star-Tree - -Star tree cubing is a pre-aggregation technique to achieve low latency runtime for iceberg queries. An iceberg query computes an aggregate function over -an attribute ( or set of attributes) in order to find aggregate values above a specified threshold. Using this technique, User is provided with an option to -create a cube with necessary aggregations and dimensions. Then, when aggregation queries are executed, the cube is used to executing the query instead of the -original table. The actual performance gain is achieved during the TableScan operation as cubes are pre-computed and pre-aggregated. - -For this reason, the cubing technique is highly effective when the group by cardinality results in lesser rows than the original table. - -## Supported functions - COUNT, COUNT DISTINCT, MIN, MAX, SUM, AVG - -## Enabling and Disabling Star-tree -To enable: -```sql -SET SESSION enable_star_tree_index=true; -``` -To disable: -```sql -SET SESSION enable_star_tree_index=false; -``` - -## Configuration Properties -| Property Name | Default Value | Required| Description| -|---------------------------------------------------|---------------------|---------|--------------| -| optimizer.enable-star-tree-index | false | No | Enables star-tree index| -| cube.metadata-cache-size | 5 | No | The maximum number of metadata for star-trees that could be loaded into cache before eviction happens| -| cube.metadata-cache-ttl | 1h | No | The maximum time to live of star-trees that are be loaded into cache before eviction happens | - -## Examples - -Creating a star-tree cube: -```sql -CREATE CUBE nation_cube -ON nation -WITH (AGGREGATIONS=(count(*), count(distinct regionkey), avg(nationkey), max(regionkey)), -GROUP=(nationkey), -format='orc', partitioned_by=ARRAY['nationkey']); -``` -Next, to add data to the cube: -```sql -INSERT INTO CUBE nation_cube WHERE nationkey >= 5; -``` -Creating a star-tree cube with CLI with WHERE clause: -```sql -CREATE CUBE nation_cube -ON nation -WITH (AGGREGATIONS=(count(*), count(distinct regionkey), avg(nationkey), max(regionkey)), -GROUP=(nationkey), -format='orc', partitioned_by=ARRAY['nationkey']) -WHERE nationkey >= 5; -``` - -To use the new cube, just query the original table using aggregations that were included in the cube: -```sql -SELECT count(*) FROM nation WHERE nationkey >= 5 GROUP BY nationkey; -SELECT nationkey, avg(nationkey), max(regionkey) FROM nation WHERE nationkey >= 5 GROUP BY nationkey; -``` - -Since the data inserted into the cube was for nationkey >= 5, only queries matching this condition will utilize the cube. -Queries not matching the condition will still work as usual. - -## Optimizer Changes - -The star tree aggregation rule is an Iterative optimizer that optimizes the logical plan by replacing the original aggregation sub-tree -and original table scan with pre-aggregation table scan. This optimizer uses the TupleDomain construct to match if predicates provided in the Query can -be supported by the Cubes. The exact rows are not queried to check if Cube is applicable or not. - -## Dependencies - -Star Tree index relies on Hetu metastore to store the cube related metadata. -Please check [Hetu Metastore](../admin/meta-store.md) for more information. - -### Building Cube for Large dataset -One of the limitations with the current implementation is that Cube cannot be built for a larger dataset at once. This is due to the cluster memory limitation. -Processing large number rows requires more memory than cluster is configured with. This results in query failing with message "Query exceeded per-node user memory -limit". To overcome this issue, "Insert into cube" query support was added. The user has ability to build a cube for larger data by executing multiple -insert into cube statements. The insert statement accepts a where clause, and it can be used to limit the number of processed and inserted into cube. - -This section explains the steps to build a cube for larger dataset. - -Let us take an example of TPCDS dataset and `store_sales` table. The table has 10 years worth of data -and user wants to build a cube for year 2001 and due to the cluster memory limit, the entire data set for year 2001 cannot be processed at once. - -```sql - -CREATE CUBE store_sales_cube ON store_sales WITH (AGGREGATIONS = (sum(ss_net_paid), sum(ss_sales_price), sum(ss_quantity)), GROUP = (ss_sold_date_sk, ss_store_sk)); - -SELECT min(d_date_sk) as year_start, max(d_date_sk) as year_end FROM date_dim WHERE d_year = 2001; - year_start | year_end -------------+---------- - 2451911 | 2452275 -(1 row) - -INSERT INTO CUBE store_sales_cube WHERE ss_sold_date_sk BETWEEN 2451911 AND 242275; --- This could result in query failure if the number of rows need to be processed is huge and the query memory exceeds the configured limit. - -To overcome this issue, multiple insert statements can be used into process rows and insert into cube and the number of rows canbe controlled by using where clause; - -INSERT INTO CUBE store_sales_cube WHERE ss_sold_date_sk BETWEEN 2451911 AND 2452010; -INSERT INTO CUBE store_sales_cube WHERE ss_sold_date_sk >= 2452011 AND ss_sold_date_sk <= 2452110; -INSERT INTO CUBE store_sales_cube WHERE ss_sold_date_sk BETWEEN 2452111 AND 2452210; -INSERT INTO CUBE store_sales_cube WHERE ss_sold_date_sk BETWEEN 2452211 AND 2452275; - -Alternative to overcome this issue, use CLI if expression is Between Predicate or Comparison Expression. CLI internally queries multiple insert statements. - -CREATE CUBE store_sales_cube ON store_sales WITH (AGGREGATIONS = (sum(ss_net_paid), sum(ss_sales_price), sum(ss_quantity)), GROUP = (ss_sold_date_sk, ss_store_sk)) WHERE ss_sold_date_sk BETWEEN 2451911 AND 242275; - -Internally the system will rewrite and merge all continuous range predicates into a single predicate; - -SHOW CUBES; - - Cube Name | Table Name | Status | Dimensions | Aggregations | Where Clause ----------------------------------+----------------------------+--------+-----------------------------+-------------------------------------------------------+-------------------------------------------------------+------------------------------ - hive.tpcds_sf1.store_sales_cube | hive.tpcds_sf1.store_sales | Active | ss_sold_date_sk,ss_store_sk | sum(ss_sales_price),sum(ss_net_paid),sum(ss_quantity) | (("ss_sold_date_sk" >= BIGINT '2451911') AND ("ss_sold_date_sk" < BIGINT '2452276')) - -Note: -1. The system will try to rewrite all type of Predicates into a Range to see if they can be merged together. - All continous predicates will be merged into a single range predicate and remainining predicates are untouched. - Only the following types are supported and can be merged together. - Integer, TinyInt, SmallInt, BigInt, Date; - For other types, its difficult to identify if two predicates are continous therefore they cannot be merged together. And because of this issue, there is - possibility that particular cube may not be used during query optimisation even if the cube has all the required data. For example, - - INSERT INTO CUBE store_sales_cube WHERE store_id BETWEEN 'A01' AND 'A10'; - INSERT INTO CUBE store_sales_cube WHERE store_id BETWEEN 'A11' AND 'A20'; - - Here these two predicates cannot be merged into store_id BETWEEN 'A01' AND 'A20'; So the cube won't be used - for queries that are spanning over two the predicates; - - SELECT ss_store_id, sum(ss_sales_price) WHERE ss_store_id BETWEEN 'A05' AND 'A15'; - Cube won't be used for optimizing this query. This is a limitation as of now. - -2. Only Single column predicates can be merged. -``` - -#CLI changes - -The CLI supports the create cube statement with where clause. The where clause used in the create cube statement is the range of data to be inserted into the cube. -Once the user runs create cube statement with where clause then the cli internally runs the insert cube statements. This process improves the user experience and -improves the memory footprint based on the cluster memory limits. As of now, only Between Predicate and Comparison Expressions are supported. We support only Integer and Long Literals. - -## Limitation - -1. Star tree cube is only effective when the group by cardinality is considerably lower than the number of rows in - source table. -2. A significant amount of user effort required in maintaining Cubes for large datasets. -3. Only incremental insert into cube is supported. Cannot delete specific rows from Cube. -4. For the create cube statement in CLI, we only support Between Predicate or Comparison Expression. We support only Integer and Long Literal. \ No newline at end of file diff --git a/hetu-docs/en/images/cube-logical-plan-optimizer.png b/hetu-docs/en/images/cube-logical-plan-optimizer.png new file mode 100644 index 0000000000000000000000000000000000000000..9ad984d519191eb1ffb4132eeb8d838d0961abaa GIT binary patch literal 38432 zcmdSBXIPWVw>JznR21xp2#APCGXe?%0xF;g3QCn46$nb*lt>TQ02QPYB2AifA~n*I zjfEDf3P_Fg5^5lXge1HZz+KMypYyyQpNH%6gZDkN?wK`fR{gEKysdM6@9tx}+1S|j z-q2LnV`JMw1^;w+?f_55AozW3YyoUH)UOzLT2A-x@;yZEuUk;HbIg|)*MrP=8olZ) zD=V`TbSyPWHqAW~Tz@|$Ry_Q~DOb-@-F(s0gGchO8(!wp-FCIUP-t7$@zgV!0X#vG zY&UM_Hp${p{4!MMH~rJnO2x?(BH1q@Q5jY=eVUL+w`FRvgU|n$zstT?VIhAob1jes zyA2p}&RqrCr7<}OGs|xU_ALiO@UMpsjmk zSIsCT8QLuIj%laVW4f$fK%`A*UgM8 z|Jmf$WTAkX#s{VxHVu`9AKU}dOoyZ1kuG&`2$_#ckR$%?>K&Mm$`aW_kzcSE9t zDg8y3san`e+@WH|d!0S5dU|QMUa`j_Q8|NdX9dsDf^Y>F79&s!FvMhEjRv#v*FekX zW_r)fh$7Vl%@Oi~=r~vCN-WA;Ry&5F7`ZBGw7e`ZSdo}k2v5qM?n>%g`$UW1!p3GD z>#4xvv(acpC1HlY$4hisoSLoE~lzBx%l9@PVqVgn1X9_~B}|Kl~5>fatm5 zkMn-du{sR3Eh2(|EqR@>zTKSWX;X8RcRG0FdBsnvN%r};ChvughvQq|dOKI&Qp->} zumKkf(U%z`m4e2Bapsd1I)Uhqf|Rj&R}=K=vzmuCYcW^ragdJ%tCCfH5z~>-_s0fi zKg^TfCq47T@^~~Lt8B)`8=2?a%TQ|%yVibU<24|CGU`*_vO#Yj*^WJ$p(*A2bUV37 ztu;2ans+q}P!+q#x)lZAyTl@J_v+miJ!}Mffe?mAn3|F1jP~LUt%j5PeWEkwm49P60FCd1@$(VHT}J~u4fiqA~_@Y z@b|-?Bgoq2ok932$Nuk~Kiu}UQc~v{kd7`<(#x}}kKHUXd1ea=h|lSS`tr3zV!zts zK0yKNcsOZo!W<&lXFZK=>!Rl5tXzuU6+;Izm(A^Wg_-!#W+D zq}V_U}OtSAsE;K%Bhc=T)pdEVZa^foS{j zhG7HY-89N+)Zt3S<`-W_`p4Rvl(+=$(kRveYAWPMPvo3Hfw1khM@?UI?(eERQ$-hd zq?QkkI?ZlXCx7ZTsEHA}I9tbvAu0wP=RWb-{Zxx5oOj1CmH_>X=T|fDx)FJYWJSGv z@~t=55Y@8k4Au4B2cMl0DzP|)_Pm2^Y$t?E%SH%k$Bsx+c<&sSDU4Z_yB>tZpF2iz zMU^z(auVgZtJp<+QJno$!Zyi8wV+Q2!xE+OAs%6KeEJ_+3-0j(*p?fLNIDR2h!*zl zxanE{n)U{cV!XM_Y1!AXL?EoZ4Hl$C*_`t{nOPpU_rCtPM~ZrXMUnSlQrfYk$!m~5 zUjspR$dixjCNxbps;E{}R!y#dv5#ZAG5Zu6Zb9Mcf*;`h46&wH_>@oIigm-hP7-(3 z%XB|WUGRli#`r4qXB3<35XCZ^-Rr08WcupMEt8y4FOAhV*f?^Q+OA&fqZD|-l@XM4 zpNgC|iYU%)yy(Wj&gx56r17-8-(3*Sz00)#PGQ&#y$>t3;WS`mA0_LnX^mW(qgh+% z^l^B;H){wR_gMgUhGQ>ft<^tZe*vmwb&jfSws zm}<=0RvRQP%J8$$aqmHgf?tXwO)6@dI=!Y)3XtdBsUa8|{FOevE4HC@gAi zWh&bU9-T&TSsNK%SB#~1A|RHrzCu1rqtmKxF^ns^OoDifvJQdw^w^PRMR3EyuQ4;G z@V)pRI5=FkZz0n*$ih_}>sWpHZhNADlWfso$NGy1pSaiRwXdL7W4bwmAGpL;Td314 zyPjV7Jz9AO!D5MQfGY1R2Wo>YU`doCcmVcZv)gtsVzI|rZ@H$(LxL+E+^_$K{xmr- z%2n_B1FCN02BVJL;#Vy5^?Q8f5&6T7*LnV;Sofv}f*Xu;ecCfSXYqRy} zi_Y~cFJyO&BRfL;sQo=&cnsfYZceC;;16E=GG9lA1ae%e*C|9A-73DbV=P1}If?Nw ztC)G0#hFX7vX2LsBBG8M$K=ub#FyINkIb}&4j$w&91lTz-tfQ48YEDE%-Dtgt`0vn zU|s}ATmG_Y$P+PJhKhdq5Nuf)akebI5 z9C#aSnvs1AoVHD-RU)YgqB3K{bItt53b&y0;8no3sPEQq5=yePAfpZSA6~|Em~20s zxa0_4v4vk&u#$k)dcSSzd~ZGarn-C>Z#&V;c506-hMg_orl{W;C0xlh$Z*3=BTEyD z&h%Ghv@$lA`(tuoZf`&pl~LPt*D#-F!TSBZp2jJ5uap7sKEJk!MVuV?2JLy}vBOlq zC<;~S9xv@~{1oAMRhLS|VxN_i4QPVx`XP^X;vZy<19xQ7kiWDmUMrij{$c z4>(7no=)cGaEu&wN%H*?xc)Zv`&`6OTJfS9+#7QPwqp!ATkc`7upg|b`N8^DCPr_D z@8731^j7;!PZN}hicoT5uaiCbxTz0#_xq}+ud5}F9#yL*oD%*lYB1a6ok!=O$+1b1 zrp~ymuM`oZROs3RL?x<1eyy}oHzZ+!GjoEtBXy{QYKbb}%f_~XYFaCP@I5IwDJob} zzS86|;vo{NbHZ-p3zban!x+H?Xl!&8au1N*#w%Ew;*%_|Qqg4A1_B7=ZrmYO{5IFU}p*Ur`7wwb(4LYv2qQoj#mN4zX>twrh%o=0h3TxWu2Ufzwvj&ny3 zPsu*e8?`Q8LPuHchg22SaZ!x}wZh_VE0{d?{1y~$u)gPT_dqP0rufE8MJ*M*u=S&g zd=<&Lw9PaeB)l1 zl`k(Mxe33%f{1M&zxP4kXFL)>%G@Vz&F{6$10vI&B3~o#SqSc)deFeO|EmX{9Hfzn zw)?JDg)*_*_Qm%KShhJ`w#*{T-+29=i{A{ZQu(fg_DRBLI{ZH<&u{Fwo#B7zowKvI z-z6jfcv3RJPdz-omlyX(?Js)K+d>G=Ov#!0IL+6vv~c>FqE&M&Iv>!fvgr+#9wVD< zPw7c09Q|G_xEJ0j$T@$2MJ?L*YSms5!H0CV5BHoUs+y6Kid-g=kKZmbOYqK4-NCj<=$j4aXH8-HEYmjEcB_P$IV&T$f3pbNT+s8q1cSuTX_a zr;AI)Ovdv{^BN3u0+_M2mE zo6F(WsopJLLQuWGME;^xKe3%?z3th6S0t*f(y;bddrAjOp96E?85)_qa%r(iwpQ1l zra#=K#(hCYmEoS|zs9UBbL>6o)OA-}Ts%Y@QvD-N#^x2AD!8^1bm($gditrMvgV9Z zif}eJ5Qw+OUr+2Y(J_p5%DPa*c~xo2kv$a{6HSh2`j@de|FufP0$s()Q)$L%%UTT{ z3u}wnzVhNixAB(XUZNL++?C&}y8P3sbCkL`&i6TpTfVcvg3qOQ>|4)j#E&4rF~=_l zU&~GKiyn?3gWaizqI$!*&QM>zJ~Z`=OR@eUF|Rm87c84Vnx1yZ{isTx|J*q$$ea(V z70XO`u;=*2wuilC`3u8>Yd_CONnMb0_<66tr6EdJrTbnOT>@S8z^^|;nnRS?fC}U zA42$K1np5;R#rWYHkFH(6Y!nCGCcp8TfU%*q(x-vk+?6-U(TI?`plGe2E{u2ulClK zk0;rO@XFS%bhjd4(>NdsIfl6>lQ<5Tf!p~V7#>w?hlin9smrf{MxJ~gnf$TOkJ4}S zC`r!I^b0(Q|M5?(@{mr0|6DaC504=rD&bz>y!U^GACsUK36$&9(v&Dsc)evM_lbQj zsz6D&kMK0O@=GHCh*$e<{Y!LQw%s&+rJ zcHmX1BM0rNqe!TeMQkMGvwZ^5hP%>yZMwMinjxv~4Zpp`_@2)u<*pfU>UCAxohT9i z_Ee3|+o}4x${jz8Y+je%EW7TMX4{gWVAP;G6fAtqSCB&R$GJ2~*G*UU1MZm-(T8Sx z!+)|A#{cY`ggXK0#ZPtQ<}XayCi|5Y!ik-J_@F(!4`POb1ib4jrl%W_z4%e~tvl`_ zNAH)AiG#QBbw z>5U;JO&<8&i*Q-%GIt9luMF3Q+Rq=~SXbx&<`$*%X(1lCful1WHRgIc(~CjF6>paJ zW$TNkRaEp;gzA(0$+=KoO20?)h3M;Ch?pX&4D*7h6j@>yT353HITMZ^jbl@&&P>nC zYl>6xt0=9~O;+iI3Ry$ec8Mn3oBmlSoI1jDEmD^UmiYm@2!)C zz)+os*gg#j__@E!{AxRlNoo!0bK$(0w7(E2KGx!fgkMnUi>(;0te7D>5=YOdN%9k; zl7ueT{t)yUiKx{@kfQvo2p)3-j(eXeto(Ww%46v%BqT!Mh0eW67|?g3)C-5*?LFBm zz8yg&jdr?Lk7x^c{$eMV6;alF2&+jE@$q~M)lut5h-5_)$E~s`@z&NW&l?j>!eZ7g z6L!4Qu;dqMX>xqHTSS!{5eXTELUm+hqkZ4mlpmDHY&I>rknOVw&82^-2j=z_*6D<} zW#U{6s<-Ws7Gg@RL39@Afr)y$Dllv>Clp@RyB5dWj0ZiQByL+*oAS~0ALFH6ma>)z z>S*xjDOn&*so&R9hL)c#;v-#2kyo~-m@3p8Lq+#pKrfHTy79fe+93L+7f{Wb$ux++ z)eUQh7D`L|EPeKwCpOhFG@-ov4x(%SJ#yN4%%1vy6`=4(Ln~H8zb4mSxVA?Ax*qt< zxX#p!R|j`)Fnz;tl{|#jc;FLnOT&MD(dhUp@gV+Q$;3=6uH62rO)08@W+nbg#q>4M z%|N=Jd-7?iwB=pn#0uxWOZ^`1!uj52G-&Ett8Zr6ACq$S+n(()jO9AD)@Wmrq&ky$ z3^gHc>r0J3q7&8Nb71qRH_c^s_tUx93)!hN{ApMHuHDQ@aEN8XT_uM=)Yyr~$ryNl zdEVGi^jS7g<;&{E)Puh*pZL_%Wc4cf{d_PdHN})o|Khp(nH8>q8y>>D zC+_0lVcPmb{>_K?lHDc2!^~g-F!J}qKODC{FT&nQS`wa88QFTL+JD^K$FWkuqGE}YOuSHzI?Out%BcfDptKq^A)V@(wnq=kD^A#0#G;Gh^_Jv5xY!ab32DpWC_B#_A$ps*>w=YX_-BKfHwv zvbCVBE^2uXTuS?}8hQagTba2WHK`vuQ*0^wIQB3fRFV-!Mw>IOdkn0)t0_N@Jf722 zT8o|iZVUBs)L&mv^}Bdqp_8(kzFT_k-_E9uL*4G!Dn_}q?@;Pg!op$5BW{$A-g+l^zE#o4`BY(7NzDt?}e>?nkHjp>6vi>egpBL?g^S>O#TL z%9Su=M|HWiy}2zTzEr)1W^5|FxFvgOTJ0sQ8YxlXu7I7dB@$J<7YIJ&ErQxAzJmmx z@yA3x{k~BT5?!b{eUbAmRQ$tl-bJZ!JE@-caqpguiN^(>naOW>b<1ZDihH7$px~Z} zEjVG9_Mptf0VEJnZhi6h_Jq{j50r91 zTVI9x?BT6Q12cC!PwOGGlvlj1!XPCLOY4g?T-M2+>i-?PG%5Q8BmuQBS;i|Vz$ zbdXjM&zvN3JxxuLZ~aOCt(u46>_nafK)w!IN<_ELJTe<1x^ZfMGhRd5*!xGPHCN10f-MUOpM8adb znIEd6M%bY$G1UPa;migH$Fgb}H*}sZVaLaFZe~%bG#lVn*Ie1qe1jsBYTeX~3*NlC zup(09RMBa}k`Bz*uypQ*Zs#SLq^2ffPli*w6y|t_vv#f3SXtJ2DrXR?_i50EFa242 zD`||vCu?O`YWXD!_4JWMF2~3VZ({Gzb+-z73ZZEAngLPc63WYd1asxJYwjZ{K-*_PpD}^|ZYi)-3$}lp5XQP^Y6&+vQ z`BpscMk}*E1#_~$K;!|z>vq14P$LK#7-IQB=i0f6L{ZePYBQTX0{o`=&aVZVaT!EK z-kNtFN!DY<@|Lo@D?QB8z;5o3m8}4d%x_*9&l~?O2eT5f>7hT4Il}is-U14kaBXuM zj3sa&@S6V?EPBMTO-V=mB$36rM%qBdmAQ-Anpt!Cd}POlGfjO*1Yu72J-?5OWT%TT zLYSObR`)3rC77eJQoPSly+eQ-sAs>KY8l!fC?j_$SbEelq=CSs8!$o-cl}jZOAB|$WdCUt$VgU<58U~BeFBKW*ZvBA z3-|AP*E34HE8*RXG(%(lY-jCX(r#wHRd353m&si_hNz2a>PTM(pFz41biBKWlBjj{j!SxorNi1bW>j^ZCX+))@_UfY*`p+kp&G)3e86-_BJWvtTi+F zaQw1*RqeruQJ6fwg?r{(7w@ST{Z3gCUI9xWAvV1`=ICOD$F8Ntu9KTb(%?;YykZCR z=|7j%{vSLa+wemkREyP5vldanA1b#%qhp@=lHssJ>;8}#QP_vnteg47lSvPb)YbHj z=&EK~28)1vQ4~`j!yoB*gmIYfVDWX;Dh}N2pe^Qj9$F%}3_j_qIe1I{v_lxSlmlmm ztdLUnw*Io%CAsc8nGuU;$i|^S7jFOWyLI}XvTz0O=4!f{iJdb)y(}uH2k7)-m(cdL zwyL$N_yR)Ag3mnv-)G%$ts!6jKd`dERn*`nznse*_`<87r;kc`uP!>zzP)YY2rFX7 zq0-Zh!?H#Ee2ksFyu8&ZkItIe-dugZqFbHw+J_L?-U{8uVlIy?Mr!mDH7RxE-~s3e z9F607uuDX6Qu2TH&bn~G!?5vUmU!4%r{*RsjOssq4D!w5)2_Ef70}gt%1ylI>U+YV zZyzKY-VPMk=7{vSQGX^0eN6cP+&Mc$Yka2l^dp&gpbr)n?8eH5EUUNtcR7CcAF_u* zw@gbcnK3Efgijmj#PTOynN+)@r~_>So=ZiE)U>xna*-+}=@QNs!}0@fB`VE;447+o zvbQc6yp(bT=t5sT1-ITz)Fn5ww?Mu9W5@F`vQ2&Vuws;M)A8P4JwN9M!Vb$~z5+`i z!}+fpcDYBXzeyQj5Q=-7{9`QIhLs=foMUv}+t9UR|E8*G?;PKqHubtX3er4M;na1k zied&l;dNd3BytW-r>AO%@>LM*{222SwjrEntG;+Hbm$F}BS$;CJXFI4U6dmG>WP;A zkS_Yqb(W}Qw8U4^Vj&O3T)_IPL+S%R(5DOWfIXikd_>r zqWF}8sER9>QUN0!fOZ~wT{TOlT$Kc=yhp7J0V%!Zql`BNHu+zDRPSsKyU7~HDI&P@ zJcHp?jI!w|>ij0ix3I~p`Stq@(r_Ol83Q+$hiy*-lXplKV{?MF1`zRW(-R*+X5ZzI zISL-KT$uHTIynPT*{0kwJ7qEV1%E%(5+{UZoAb!9$zoF2e?PpILwKESDY)egh_KjL zhX|4ZTWVfYQh12!JMN9i-92DI+STx7V(!IV@aEEZhfiBB`Q)#W7l5If4;w@WzS7Yl zcdya+YpMJagAn{J3;5Va5qE$}4@?{35PlUo2SO|MR!_#jqBmpbQO0&KkM~WaV8|Xp zl}itJ7p|Y728eze^h0v>1_Wp>zZUBb3p=%GRWCPY;C;?olxGL=p^qoSs_mxaL zWz7>>h;yI*;9mpT#Ppku1bdGW;{#IWrXdeo;nqZQ#NqUra({KbXv17iNkqahX-{YKKi z8j47$hQ&eC%A^13dQXP!x~Ru^szauQvw^wlcSwF9;tO{d^9!>H&Ao_d8@7QvpY5(N zhRhdQcScJ26qB^Kfo|Q~JbxVt2_Af`7`Jrx{KNo!vC6WPo7>Xoblh|;J=#%N29m(O{Tp2Eh; zeuP6^?|QjKT*)cM#qh=wO>u@b-1%Pq+xcH`At+CZ4(0nZky1 zA&jNoEsdX5q7$K}X~+fZ(-P>sS#+zUB1l_q({5yisIjIkkaFeZDpIqL^J9i<|Mc0+ zNz6;*RBbHtb8z^(pvdIo-VI3Kg-M5DhKU8v#L#^)Y^hf`&1+fUShzfn5XYOFLr&&w zxXCPu>F@{k2;}BG00EsTD`$Q)SpShdHi<2Re2(LO$gyni!RX0{#?_e z0e;FkUm$Bc^;UXSb{!&1b~qD`ZKys~g|{^7c#CarZASsB*B*s{T0bxGB-9rMw}g)x z*mMUfTHUsM6oZwT$3$b_p~{zQ!XgL)DBqmDTJz-(v6kXU&HFLVaf73vu8T%}ss3(j!T zB^yC_xbELY(?=dojwdTY=g!C*wa%Cnds|{uC&Hfnc_Wwd+SrE#G)@lL@rh~%eS340 zX|$8_n&e#JH8;vu>V8a^sCJhjFM-+Okyj_M_@gA4`27NHQOT<1 zB+=)i+ZWZ7PJG(2&Odn<66QdnxOUQ(`Dfhn*e6gHxbfrhmnef6UzEI6M;>2D(+!@q z<7PN=N5G*nhq@9GdZ$JobC0dzFOoxwLycbz^ibl0_z}gy zTR2UZ9hOj)Lo8OR9t{$B{LxTHu*t`cFf*!lWn9qWo-d`6gHLs3FzlqtDRbBEm`>=) zmbFPpUT+nnX4bDOMymhej1-ca7sD;^$ilyw+@DVz=r*_Q`>*eteQMMc8#drCVC_4%axlLx#&M5S?~i+{ zfg!$sdOM9O%qCyxt7s9U@|b(xcs726Nf>b%AzCtam||Q6Ae5w_JUoeAjqqj+J7@hO zG8xfb&7?vI$!?3H*EEpNmd5vE4|Vu_;oK@HH6^jQWfEL^6#dcMbd{9>2q9PFL?I8+ zAnJW<&~8MCt*wWYb2oDE^%|)pNw@oZl}1BU5=$x7Ed8dGi>Hvs(b7Q3bT6${7>-%% zGXG4W9Mi>K#NREkBVvi#%;K5~W+WbAFV*V*094aFy87;gtgN0H4p-yu#n}E7OVo?= zw~NK6YS=F;>*$LR{Cc70Bzq!h9;XbWPj%snnV-&$lEW_?8m}+A&TE?hf8#Qzdfn{B z>c@jRGDmQjW^YL<#f(6Ubs?8pgsuu6vsZSiNzY#GB;%osS{m{BA!lm9i7us4rPTtQ z2{Pjze!!h-3Zb?;SZNa@?#noaDb`jbN86O^B>6C?9e8*vc55D9W=Twff*?XplJ3`3 z`-wij_;tc#^)~TYfO(Py(NQzShbF?~{Ij{9sXwBmD6KaMJlXQ7P)6eV|cT)Ac>bUa!zX2)d1EH?%HghdsYkN`= zO2(~|1N#14aKPRJyn_B?-H9P`jy-q3>eM=|(HiYTY>2tSfl*Dl=8k^*;)?IZNYA2B zMhoAy3lF*Y@!yI8MG?*cW_^X^=NIYj*z@*J;3kcftc6xHDE@IHT<`oSN!Ti>baoXJ zRA+*;kwlCtB_CUz!{HVx2_A(7QG*cI{xxP{Jlur2tJW?c+|)j{DTXtgXsV zPpN+vM_z3fN7^$l`CajmcI)lS=nY937EkKHWJuE^UrWXFaHG9(AG$6wyxbS{8$62K zXSjw@k6v_!A6zs$H(N0m5qv;gL^rUuUZy*K;BJFIocS%d&M9X^2@|tL5=pL~=gq8| za^W(H>8&5_-7#P@GowO_v&v7Ak&v5~IgK-dS%`)SVJyh)Je@&onZ9@^TY_@$fo?0p z56bsGxv~-g>WPz|-K}-$Vws7L`pYsRA!~x9(#0nlGNd&DOsw(+u-V@6EqG6|qDxu>%>sq!=aw#H!F-~%W|%o*Fy;WUXwTtx?LvLU{zvQFNIB(d zB0wk)opxciPPOI@Z_k5c9aOpJ5FhI*bhN6buOM-k>dD^L5AO_qqU593JgszzSeXLL zvr40@Kj-+J58tPn*4E=a$?vhrV`YWqFLRu&l?rQlF7W;ZqoVBf1M7k<|I3&@? ztIm&|-F0K3TG%}j5epqfNa}k_jERPBU{Jl~L*7^+jvfK`HjmKnEh|{~X@T-k>Wm4- zO(#Okr)9Ko>D#1^wg|NI`($78T{YX0n+h$Kux_H{XN{3}J%Ve~C#`-w=LvZYss7Xh z>;62ur$6HA+@-SPY!dhaI)Rv9r%YT`Ex~f|`uh)r`8!#RN7Dr`1E4PJZ-esfH)wEn z;)^#6JM@5Rn)LZTMb*yz(x^L;|3^2(08>jP8Ra(OfqlT#Zu36<( zz0)>#-%L|uq^Y*aZUt~9UNh$6>$9c1llM%vX=f@)#rWP>h$MHm@pk)2sXYmNLm$Ty zlM+>0R}h(tc~>Qg9D_Jrl*8&E^lOafy=sngLXCkk@)U<3mM|?5+2FueA`M%#RXm(ez+sIw_s%m840?orZ-WJh>75pIe<2?m~IK zne_mb{}-Hq^%;jd%i36uviYq!(APo?1_mWT07v5Cx|a@ zoe0~1>#Z60OXX~FDoTEMW^a>cyh5rs_sUmR{HJsV=T6~!tr}_{!X&=NOgA@5^nMla zUAuwjDFKs@rSQ{oo zY!2H_9=O>6!ZWcqW5|T4MEkW;+vWrTd-8am!tb~xRg9XPf6TZN=7hGP`3-VXCODr; z+UsfbDqVX)3yCvO=$2# z0*Fj0?hI*DP?O#W*fNs+1sUT>Ii-u zX#~~3+18jO3#_~u$?m%pyMd7I}D z3Zbq#b}Di7j;Zc`3>q@+ z`VoPwAk75IypZ%$dk%N&hf5+RDR@^_r_;1f0 zjO%Zr5E=r)O3xHkh{^EZe>>{@?66bY) z_D$)F>JbjxtEY-a4$eJb8efwXX6_WhxwN$6{!Tim+$y;s>>o3G4;F?x2)YRb7BNi@ z(u?6B$+*l7?P_k~fOW=GJ1*2fJw~j#hOa<7HxQOm?o4}>TPxv)E*i+P3SlL@IjSF-Eem#<{ z*y>|Ud2FRNGXgeJHP_o*zb#H)L0*OthVqE6ej`1IPK@*qD#Dl~<#IHa?1FG_8CX?0 zl(lvq=Ct^xa>QT1wC%3GJF&pf%^Tj~%)`yfuAHG__kvB57M(k$3)cMRngxs}x-#~Q zjkG-1&}%FH1jP?Hh!y#tUzD;zI#K;&4&tkO=KSx5oS~v+?FApGNEWYW= zF1|!@8Z3qp^t%Ojj(Fr>!d^;R^!Z#ZN)V%R#6{>s@RXAj+}EaSHmU{DbaZpQ^ghzy zl~+~1XWDNxAnT&IIg(Kda#ej6q_x8>m>7of^Va4Bn`=6}@C(VpLP(L$KYlDhTR_*}(4oD~Uo;{&%&|${hvn?O5kn(u4t4~w`Ke%( z`wZnO+ky%P*sJk)6CTU4aNH@ndAA7eQ>pnIr89q6JiaIb)hzU{A(!ftMj-;TxGf7Es6F*LWuQmcX-z6id zApk`%(e4_{>GG5nQjO*^)BoZ7yWNyW1tZ!cpI^lvkCqrU(_(sdxq2nUYB7ZxEh)|h ze!#+2fc~eTp87}0qlMP|Ypzl}B;(zady5!+hM(vBA2XVL!zftYzBI^A8K7rEON%Sa ziz?>Fyw($H>jh8jg|?>GJu;~9bH?31H5uWGI)-Es^WE6WGlZODphv2Ui`Upcfk+Z- z9N-)18^#YHUQX}lk-2}@O=Qj2hP%rpsxB6HLX#b7wdHtww69Bk(VF53!F>tJ)XL`L zxVaLK0)QiSX7PP)60CGoIhPi*89n=zq~K`J@C{Xd6{-wkuvJ>jC2;yT;I+3H9me{ zR9BC$$-SzDbqjf>PqH~)b%-?3IPm=}A!^UTNbX(vQq&l!i}~c?j!#JR zimWxZo#c&L^RW@MKY=0SPRl?z1R=)D&|4!bTc|{h3W4YSpq4SnS9RAf7$m&o)?)VK zgH^8rm-7t_tG8@vi?NDUmek-n!^keGYj&2A{j-UupqDJYSXe}fBcx3Qy_*n$s%)$O z=&nF)y55L5eqBEd!hvgy^ENsA0?M@`7M^Gg3ohcOF*58tA14Zm`%G8wQI6#1FY(qvPQ7i*)$5op7Jjb$6c*o6auu}>lveJb&qZQH8pFqHL5@P z2x4nqcLaee6r4J;DhH~Sk=4J}^p@uj2&w5=Ln^}cp(=S(b(!PYKU+lzeZ!ws^_T;; zV{)J-kspn_yI=LM{IB5M>S({l>yvEGXI5{@Uk&_0*Y6&WMiQ<2DiV@pjH(TW!?@Qp zsXl9PP!G@};Q+$@NbUxSjpVd-UNn{ha($;o0G|H=@*9uDfz87BR$hsWIX{?Fu>wap zs8%`X(H>|#xJ`PtKajX04=N>A8B5gw-Y-YFGo&{SQdx>(euXEcS$m>Fc?&~UgxAMR zS3|~6*o?`=R{~_0*^=J!1C1dT5sd~43rJn7)$QZ|4zxN1A+W}v2B_0$nWvec9!u{P zlsJ7#IBlZ@xD)#y0Y?83SpHJ+<;|_Gr+Il*hB~#S-Qq#H-?SDYE~w?+04*N1zbYdh z?aOwnTo^Z0B{a;_ax?*IB)dH7VLvWKaEeCf$i@2Iwq>wfm;Vei&n;C1odIt4d93Z= zS2RY|HcYzUWh>X(vk5A$--dD`W0J4a`yQVCFOc)o&!)GpA?Hu06c`ch?Xruk*zBi{>_rtB>za`c${{Y2E+1n&W%vP?8 ze_mWoaLY#*yUrn9ABOmO2O2JovQmQAe$QX;MPk@q6_3PA;Y@=Q4#D%|j zMg!$P_s57Vowf4traeuN{zW9m)$0HWe;yBt%)8&`=Lv-KOyxUs>H+n_s%vaT{l_%X z{=!AD-g0dWuUehqrQCmwUv0l_t5AD;tvftqPx9?@Sz;kc5OkE4)W@fN$qlJVQ_KAe znE~$j{|IwUn1fFSnhIB*C{ZIEN#C(4MJG0~AoLJT_1O4+NhQ@1Z5DX773L({#af4O zI~aL0WCNpd;wY<5i1-6xqNigilXb#Cf`d8*ps{1%Q1a+S;(lh~$6iWOzf}HZjX0jB zt^IHevX0mA|62QHx}AHj2JN%bEyY13Zh0`&5l0uYQnO!LN0$7h9Y5^x@f|39M$-1c z6|Q{$SUZ8kdg2E_Lx#n4&GoCEK)d|~BxfN~o<$6P4!XYXTnp}e-?!_qe$Xv*1BtkbIZ#zzFI8?cHoBsbDKvcKCccro zBQ%Ms<2d-uv2*7R-W*AOO@ON`y5!%ZbU~)wuw{iLmT>yuuJMj0N6H&4=x1vwW9^Y1b{65$FmKn@%n>}i@WYlT?XX2odKl}cm83>0CjIb>E3z~ zbF1@uk+{%vPS_7zD*z1m>o{0zTiU?${p*vP#(W)591bRa2F+WveVaWwGDrTKAGN*; z(AG7j#;QuY{K{wb?)+=qMk#fDKfgVNouA=!R`vRSq5t<5e-nQ`sK^A}Q1y5J;U59@ z!=l6fCh@yY;(1)MqSSdqxTG0cu?1~(-JNhB^s4GgB&X_#Pvy3pQ8C3ygmY~wcUEK7 z&JV-8TlF(zTW<5evj5?wZQqd*zXwjO3#_np!xb0G< z_z00$vGS~X|8|vF^OE~$#q6wW{Q96mzdhWJo=Ot>Ex0$RDLHBlPH^&a{kMU)^}`A0 zao4gPgC^{^=)K&qZtK|dfNhF{G)zOI(vu$tJ_L4bi+pm!FAqu>-AKb4%# zcwCQjRk+T7(dM0=-<%<^`rm$cdHLRi70sN(olXrAm6cgu%hR|%8NcPrC|5=cUn*Ce z^xe|za$SB)B~yOq5M$9}x;asywjGn50A5j|*b+`GHi++0>R|s~90P9Id5sN7D7&E2 zBZMk^_S;nO$)qNCM6N0P%ygRb?lwEU&Y+yFIkjM<4g z!_x8r&_lg$v^T^tGZ2;ow(8S6av9L0`EOpI8EUxsq#Uc%0g4H| zPk(G;g#udn1*h_Kq;lV!8EL40G^*5bV^p5A{MU}^=b7sb+TpBB+`3i@wtCf@%k}lb{_V>96|%EB73+B( zN&%duZMK}@KeoK@R4>d9SBg$tcmF%FLof z2rErGj^5?U#=SNeQ{Rdtb6XS?}0J*{P9o7xrmZ_081NhWaJ7Nja`-$^eq$9Id z(X6$>CW;)i&D4=~gY|>DrT}NklNU#PUTw-g`{S$kMTt#HAioZ)Wh93N(weUa()x`m z+weG7lUNT+Q>y`|F!&CBov4@UmB!g=HakqK0jtJn%W*{Ie_C8$;zhqJQ95z`M({H; zX*y=-OyH6xw(qqP1x%e8z2Jr^<~tNA7Gz6t^ zsNPGP@Njmhf||*ydIwB4IZmOhru4ER-YCcIjp92~$Vfx5$-KwdnXk#|zehhQ@u)!g zH2L0xVNh#tBPdd$d!MY;50HJ~4tkWaFuj>jAV46~D7`5M>7|(F={w}F0}V)+s|98& zUD&KN+e<5vg;d(Fz=NZ%{Nv942aw@b-*lYb3`e{M6f*I${zQfy(2a}O_v~UvpYL-{ z!$H{76XhDuQgqsFv$*JvA$mc3jKuG6G#LpP0WBKv#jC4{LVm#(@I2xHF1d&@A#1=< zbM9ug6-TyQ<>JuswB*d5nHV8ON)d;k=6PP(o*U4eY??bM(B4S^Ghq-<2bv@w}$EzDbw0^|;&fgZ7|5eVZ;swvNcj&UPp5m+-mu-t~Z| zVF3{=v<~Yx8m`q`fA^6EPTH}Od@~RpiQUK6Cb?T>i-!Ld(9reE-v)N}8_yQDx9Jq2 zKf64yY^^b{8+!o0`0mb<(6-%>hbKOcKwSBkz!!IJJOIB6F*%{O^N?j5==lHM9<=XF z)w?H6ZTO~a+EuoI7b?#$4{qOU{iYm-NP}!+YYW;Z$$BqS*E9;=*Z;9eb5jI9 z@#emC$hLrh6mFs0fF}=MCJ3(`XHD7pztAlV?{;ea&a)+;b%QDFU#7%ltHJVqS#2&i ze<2;E0@gn-wz1WK&WgsSa@GT(|GM{oaQVQXe=}Vjy7R>77>eqAk)*`5?;0Jpz_|H* zYRad5`hbEJwDZVaJ^jT8e=`OZ17kjd;dWp&6DUnLJ7W0@rst%MNY&NpfJH~anH{jn zL*XVk**&MW%HrC1~wp@~w#zP!;eTY?XF{e|^&f8zj728==0U=+U6s-sB+L3x1Pl>p0q@zgcpZ z|I7*&wJ?aL$S($G@G0HK7URF$R>=^!eg(xgb0&;$YmP>O(*h(PEqp#?(no9Ow@`R-lou65U4H-E4M zC-3Z;J+o);XYYC5eGYCjvPx*jZg#n(x&;Yz;I2_(ox}xE-7D?&a;UKj6Au3E_2ak) z4sHuxN`ML}xDEGpl~{=$i?p|V^I?T+mE#aB_8x60WGD&W~u_-TXwwYbpxxwnVnM_Vf(NC;X#V_?ZYIk6kxDqXA zdsO=_3~y<);VQeLZ*7RNzw<4=58BshzB+%XZ)pAgD{ZAV&t33o)fzLHg}9Sj)CLK@5#N@uU-p1iIpB!1 zX19LzdoONvP@7ugZb^SYXCxcBGZb|N$kXU}D!iVzlv7|_0$(dEw+bEfZbsJ?y~4o7 z3vaA4?8DLvBiIjF_N zYZ2iE_5Nby=S>ohD$s!U-%taerjQ)LGWOM0#1?U-X~)>x8z*TtO3281AFMU|lqrM2 zGYPRP!m3gMmL;g*^LMj>EM{t^ji=}euFXi67PC8xtP9O5BWP?+xg{|v0bXVL;YQb|~LHrRp zu`^SlA?Y@{5ko9eo8cYQ5Xb@Rh;FPkU(uZsH0s7{!A#4UgAQERUashJe$zVj#iDf- zE?_0w{A$Vo-u<|;EAT&hLemrw*}!IoiI>Uv~R$?=f>ok$biuY0i>!s-dwf3DF0uYJHuA8Oa#$io(P2wkAoa_4 zgQQh&!}fZM;b|-X7mK5bc6?>cm4^|DdxN^~4hL3oDpdl8qmXs+fSsU1Ii~mu`<^;s z{mFP3NojDpRoR4?c8>RE#J+0Dj)b67me;^;yot-uA}XFRgN0xX^&ucN1FT9%Es2i> zT>p1*9Kx6nNytzqYotZ=FJA@hK)hJFei}H|u9)Afm>#+uxj5wN3ie(I&2QVKZmRR@ z`Sh3gp^|vTi}Yli7dj)x&^{`r+mblVBmr)^>o{aBeicX&e>c_X+$G{^@Obeaedq7G z-sTvT$~%h0GBa*P9@)3=8C$}X78;~EJzdTxZ3mo>4!1uVuS&kK*=vz^77p-JIzlLL6ksNJP!RJYN?oC0>aT?fm8{5GxG^mFX|A8~n1gZQS}W2GoWs zel>%oW3S$w+I^|8y{ORm!V%d6mWDkt;Z|@Xs!RurYSd9K zJb8L@JL7Mkw%h^|a&4*|YI_V5(>2yoyNn^x=%g}+BE_<+!{OG3zQ-3pCD%(iG5IOE z14Lh)J6hl&xvFwN!*=JKOQh#qJW4Pxg)bG%o zB*?dY>Pvf$$>pUdlFB+Bi*;9D_x9|_J-O3#`OAmJvW4r zdYi%aR0zHfw3Gq`Vy;E5TtAk-Y7OkjD^IN7M@-r?yo0MFV$$Pa30Mzi#iOjwm-TAW zYi?e7k)&YeDQm3mgo)mGc}uzd;?0<2=IXvWRQvT?d(SRo$+MGw41i-~%>Uz#L}MC{ zTRWdo{%r2MgCV}ytiEB5gWGZG%Zds1KerZy5a__$6;)=#lb`K9Pphg2e~E5?N$S== z{EBP8r>v5)F%`0*o-iPi1Bt(MExauM7<3riD#^ zjJmp(yrRT*z)oN6c9OFjPJi2(Gu7#}zLnr{e=xilk%St4mFt+0km8$HCChkT z5vyjNQp*I~ZY-|C^IQ0&z`$z1(Id|c;V8(Zs>{QDVx3Z1qbIGOeLCu!;+~yQ8o3L` zH4Ro&2etFDu!ra&{N-mgN9S7|?Vf+0>P%K6H;9WJv9G8)r?iB&Bs%QHoe9`=I9Pet zAnO$9<(8uJ*+OYuC9q7C}Vg?zNe=&ph3kT-gtd7FV$?O zMq0<+9`z;BO6n^OKOWeie`j=0jCMi((3QhSZ=5e-f}RPEsUr>@zYuePhv)6nI^Dx9 z@1I`%Dthd~HRWdt3eRMoo)WZUEvX!`z3}2VZ&b+fL+{0B;8__;NT?_ZiHN(`DjN%P^tI%eZO#xB6vVlRJr(WYVbkUwCYl8 z>efA|Wxmcu`sXfpgofhbMy|rDSFCJ13XqV$jn6Z7$yK#U+u(>TQ!LUyC6CiTGD8T0 zleFFZJ>x~k8`|`44?8z*dQPtt39ocu?X&)V^}2M8u3S*1SLEMc4Mxo89*q3?elI(! z3;o%0xCI?*p+10qUgN#sGvmKdxHYppR$Kq`v&-PsjC0&j>~3CAgN4)r^h7P3{n^`*BKxl&u>HK+J9V@sQK z>*4ZBhhfpWPY$}C{g0$(76sdN`B2G9H$2s!uPN)%V9{8)apB#-YGMbpXK}C?&&(`_ zTBRADn|r6uXX$lA1yBhsK4uohiUPiw#c)0=Y-^lY*0n5{R@zw?W0io-#YIvNyob<} zmGzhwv^ne)zOiU@SDbe^0jvw}h{!8Ss_D zYYC|?rNOja{IJH#ms>1z)}N91K-GZNM&_~?61tAM!X97qc9$ygw+Fe&I7^!H;6A^G zz@$lwVU@ine^j7f%VD8D98Q)ruY9!UP#?4h!>l&3)qf#%Ym~JoNaj`7TuN5+u=QyZ zLE3{!`0fOx-HTtDenOjE4#*^QM6_@5(M2>n0Uupq_b@#DcDc1_Zekv{C+N%n}SSf)xqdQ za_%LY1%riy#E{KG|2#i6PgKi4eWht32~cR6ek$i|ahAsIAgJV-;EsX|85&5KWTF0_ zNiMz(hF>u|kUyB1trK}>ShWEV7s(^QqzAOznO5zs`Ma?^fhx9dtc=&?DK$qc3r}u> zbw(|5krsLNqlCV~uOtk64_Yz<`mxneIFJ49_NyMW zaQ%j$Ue4N$o`3$PmzzoX`9?C!-w1`>RHC@KKkby;qp5`IZpV!Wn@Cs8U5cO zjGS1&uSPpc&CTq4_E%hV&2|4ynM!-Gqy|E(^rM&`st(&HEN+gt6^~rj$~z`UAM#D; z4b2S(Z&H_PPXxQb%~H#Gr`2DL;XC?5di{w1%*yARM=pmxm&2&J41B!fGwhJ$066BZ zA@%Jy7l*_B-^j|)_s*ExgdZy{>plk!SgxMw)p_OI z1KV{~@tXZsU|H)kOrFWgT9&==KWUsrWfpc8isjgKhTWSv%JPe9KFSw3nXUhHDRuxW z4Ew`6c+d5@Oa=CGj38R{ChtbN@s~f#qVSzx`9~A$z_YbrgOzRkFqhkHRmHr>2;3YV z20Ut&{hV_tN=}A8fP>4?DbvrfV!39)QONbvg^AsI>dwMk_8p+MmcOdhOVwRi>Qco^ zuRmA;v~A%KE%S$;@7N4=%zl4c#>#W*U1TN8WLsR%qq>bBOTAuqFozTsTIy}mGf*QF5c4Yu;PeQMP0Nj3<1B2ujT)V0(^Okw+4 zLF3n_JY~$HzOUUafl;ui7_xdv!ke*klOTg=i=lxux zEGw2x3Efznq1r^4Yvq_v=MM`+m-a79yO^`3fISYP0SNrc5)98t38}c6`hewiEP}c! zD*7OBGDfvj>Q=73SkShOip!6Jruf%>B*L-ou3*0|pG~8TbD!NcS0?w;BT0b1&o7)L z=JECLFjz)e8-Qy~-sL*jtZexdPfJZZc}HBbYiqPXyXt*S)J*yBOY}wm;AGd^Yrlb& z?EYif!FKG7;1bK+ThD{bFnMTi$<{93^nL#s@R@~Q$}zs{oKpY{O*rJj!=L5tKI48f zO@G92EB}5ue9}*B1-|x`hi%5rSiHMuMicP24NtWv^yxbv`QLJVNx1QWVP3t^_mADV zdzJLQf8W2?$?XRr|DL-uXV3q8{hS5%$GYLPv3wn8t&Qxhw{lNx*LeSKYBod1@(yj|uhHh8h5trTcMeQb1*E|>nD zks6|qNox(U1e#T&*o{uJ5a1RWG%Zq~3EdFAwt2O$rr}wAOK?IUeXkoii$3%Ou9IDB zbi2x!S6@@bh0uczN{%=zSUs7ApMTsn+ud9$3*~}Uac_g0qq{X*r19+ylc)6Q>Uc2I zA!E$$hwcb1DtJ|&r*L{I3HbuLH8ovjZ(XSDgT?nh!$0n=_Nt7E!uCo5ojxy;J5AF<~pU%S#j~Ut8zG{&Vk!7r0Ohik<83S{OsZdL{6${Tp&GNj8MMgbm#16UYRQWioN_JE=2@fmqoSu=?TTf6|!{S6nG zl^rD}$wrzx^V>yr3lJig*L@u1Ohol#tDmW=><_09j}pP-?!H%!Q>nhRF_i_((r^c~ zIQ`+5wbqlY<_qvJ2|b^^9?!zCHUODmH8>~BxAtloZ>x3EC$m;@QKBpot=`qXSE(FGa)1wE@+l8Z^C|=Owh)C41 za89e2{ADvC^l-vX2Xei4-}_>$I7N*C{CiD`uS_Rw`-tctSj zybzIWW&KM1lx*Y=5zDWi69dFJ;_7va`{L`XvP`n#>nosrox(gxH@tO${V(UHue9ff z$W=3$0i_%&L$6o&Ez{58YpKc~KJ7Jg!Erw<)=fS6j8Z>OuH*J&9b@??euafv{}e=pmhpSX=jXy({2u(tftDnSyKM_TZ3)x z^Yv51O&bPJb%J!@E3`L(iCzZOgzp$~k){hKNG+Ixv1}3o#`o@%e@vMEQ?3I3+-0N9 z0Ta6jR7CK%=I3Y&vQv57`RdD<;!5)Mb|1k5wS;yEB`4#DFIJumR2`wK!o_tPye42BQdJ2LFp6*A_$#Q>aI%V8NBRwD2Q|2Aj9&W1@GG)_g4hr7 zYO6o26W#Ga<&+rML+^QsjDqLz6~BL5yZvlxeoHq>?=JoKq)NOe9PnN;B3iyb=ZI)W z4zpGw2XQIp7%}U4MO% z_$a6`rF6h)=<pQtS+C@#2iU-F-owQ=cYH3mB9sQ=Je%;J&72wq_5tdE zRM(`&xkP$y*X55Bq_?_yUz3{RH_<6mQ=U#9Hv0W(@D#qyl&FTwd7MgKpWI{mDb7hw zlz9{QYr@<=rkN@q7pz$if9{pTk{#A@z3H=Y-w-6rW7kHgZVZ@Rg-mspLH@|m2N1X@ z#tTvEM!GL{GVBo{J+5*k*^Mjt*h~gW>?9|dgRFU*jqLF@bVlH}H+NOq(XG}0PT%)~ z5>p`bh1G&+SHGex`Pj$;4{@a2IpNV=>F2vt-~>yL-R7Y?52f)u1|uI!7dUdczMk$v%b5MIdXbLN_l_Ns^nJ)Tr-mUYyE+q+hHr00~L&6m~QTx zfRwCUMOUYq{_9Bv@YOajz(3=82Z3X5d03TaEF2ot9&$njQU0xtFbdsY&{+JW)9yp~ z>=eL4mcH!`NP3SF8u6}WU8QM{YCe1l{MGO{oTf15I~l|F-0E7on!*S;oXgP0ccsi7 z(057^JzACSqZBf!43IjXGxwWDvrVTXx}|ohH0oA;_%Zhs^Wgl#ijGWhWHfomVYY2; z(4+JKVgyC%VjH=iTmbx(VGs{wWCZQZg{yA~@eHE-Yo(*DmaCG2ZiIgmjkIW;2NdgK z&ClMm*GpBtzWq7g2(n7t=U5YRTkxgx{jV!Ff>$EaKRt7yU{)ekrM1DltxBJ#z+j<% z#1AXJ(g8IG8?xR8_RHi?EB0vp&el7+?4@a&Q-{xXA8V9phXtj15cuBf;rW0?3mVUD zhb>oy;pIg;F51j;_9YtXRouF~4ZNVJa&~Z($ZJziCRyBM>0GI&1=)-M3h2_agBD7) zfTnj|o0{U3nL-a?htt&&r5=;54q)ha$KIHD7-_cAiFd;%tw4<0^q}Hau0i`FaOM

Cfbd=R3x-DB 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 LUM$kS_vHTooxV@* literal 0 HcmV?d00001 diff --git a/hetu-docs/en/index.md b/hetu-docs/en/index.md index 09508e945..f5cc5d273 100644 --- a/hetu-docs/en/index.md +++ b/hetu-docs/en/index.md @@ -62,7 +62,11 @@ headless: true - [BTree Index]({{< relref "./docs/indexer/btree.md" >}}) - [HIndex Statements]({{< relref "./docs/indexer/hindex-statements.md" >}}) - [New Index]({{< relref "./docs/indexer/new-index.md" >}}) - + - +- [Star Tree Cubes](#) + - [Overview] ({{< relref "./docs/preagg/overview.md" >}}>) + - [Statements] ({{< "./docs/preagg/statements.md" >}}) + - [Connectors]({{< relref "./docs/connector/_index.md" >}}) - [Carbondata]({{< relref "./docs/connector/carbondata.md" >}}) - [ClickHouse]({{< relref "./docs/connector/clickhouse.md" >}}) @@ -132,7 +136,6 @@ headless: true - [CALL]({{< relref "./docs/sql/call.md" >}}) - [COMMENT]({{< relref "./docs/sql/comment.md" >}}) - [COMMIT]({{< relref "./docs/sql/commit.md" >}}) - - [CREATE CUBE]({{< relref "./docs/sql/create-cube.md" >}}) - [CREATE ROLE]({{< relref "./docs/sql/create-role.md" >}}) - [CREATE SCHEMA]({{< relref "./docs/sql/create-schema.md" >}}) - [CREATE TABLE]({{< relref "./docs/sql/create-table.md" >}}) @@ -144,7 +147,6 @@ headless: true - [DESCRIBE INPUT]({{< relref "./docs/sql/describe-input.md" >}}) - [DESCRIBE OUTPUT]({{< relref "./docs/sql/describe-output.md" >}}) - [DROP CACHE]({{< relref "./docs/sql/drop-cache.md" >}}) - - [DROP CUBE]({{< relref "./docs/sql/drop-cube.md" >}}) - [DROP ROLE]({{< relref "./docs/sql/drop-role.md" >}}) - [DROP SCHEMA]({{< relref "./docs/sql/drop-schema.md" >}}) - [DROP TABLE]({{< relref "./docs/sql/drop-table.md" >}}) @@ -206,7 +208,6 @@ headless: true - [Filesystem Access Utilities]({{< relref "./docs/develop/filesystem.md" >}}) - [Hive ORC Cache]({{< relref "./docs/develop/hive-orc-cache.md" >}}) - [External Function Registration and Push Down]({{< relref "./docs/develop/externalfunction-registration-pushdown.md" >}}) - - [Star Tree Cube]({{< relref "./docs/develop/star-tree-cube.md" >}}) - [openLooKeng REST API]({{< relref "./docs/rest/_index.md" >}}) - [Node Resource]({{< relref "./docs/rest/node.md" >}}) diff --git a/hetu-docs/en/preagg/overview.md b/hetu-docs/en/preagg/overview.md new file mode 100644 index 000000000..7456ba6fd --- /dev/null +++ b/hetu-docs/en/preagg/overview.md @@ -0,0 +1,221 @@ +# StarTree Cube +## Introduction +StarTree Cubes are materialized pre-aggregation results stored as tables. This technique is built to optimize low latency iceberg queries. +Iceberg queries are a special case of SQL queries involving **GROUP BY** and **HAVING** clauses, wherein the answer set is small relative to the data scanned size. +Queries can be characterized by their huge input-small output. + +This technique allows user to build Cubes on an existing table with aggregates and dimensions that are intended to optimize specific queries. +Cubes are rollup pre-aggregations that have fewer dimensions and rows compared to the original table. Smaller number of rows means the time spent +on table scan is significantly reduced which in turn reduces query latency. If a query is a subset of dimensions and measures of the pre-aggregated table, +then Cube can be used to calculate the query without accessing the original table. + +Few of the Cube properties are + - Cubes are stored in tabular format + - Generally speaking, Cubes can be created for any table in any connector and stored in another connector + - Query latency is reduced by rewriting the logical plan to use Cube instead of the original table. + +## Cube Optimizer Rule +As part of logical plan optimization, Cube optimizer rule analyzes and optimizes the aggregation sub-tree of the logical plan with Cubes. +The rule looks for the aggregation sub-tree that typically looks like the following + +``` +AggregationNode +|- ProjectNode[Optional] + |- ProjectNode[Optional] + |- FilterNode[Optional] + |- ProjectNode[Optional] + |- TableScanNode +``` + +The rule parses through the sub-tree and identifies the table name, aggregate functions, where clause, group by clause that is matched with Cube metadata +to identify any Cube that can help optimize the query. In case of multiple match, recently created Cube is selected for optimization. If any match found, entire +aggregation sub-tree is rewritten using the Cube. This optimizer uses the TupleDomain construct to match if predicates provided in the Query can be supported by the +Cubes. + +The following picture depicts the change in the logical plan after the optimization. + +![img](../images/cube-logical-plan-optimizer.png) + +## Recommended Usage +1. Cubes are mose useful for iceberg queries that takes huge input and produces small input +2. Query performance is best when size of the Cube is less that on the actual table on which Cube was built. +3. Cubes need to be rebuilt if the source table is updated. + +**Note:** +If the source table is updated once the Cubes are built, Cube optimizer ignores the set of Cubes created on the table. Reason being, any +operation on the update is considered as a change in the existing data even if only new rows are inserted on the original table. Since inserts and updates +can't be differentiated, Cubes can't be used as it might result in incorrect result. We are working on a solution to overcome this limitation. + +## Supported Connectors +The following are supported Connectors for storing a cube +1. Hive +2. Memory +3. Clickhouse + +## Future Work +1. Support for more JDBC connectors +2. Simplify Cube management + + 2.1. Overcome the limitation of Creating Cube for larger dataset. + + 2.2. Update cube if source table has been updated. + +## Enabling and Disabling StarTree Cube +To enable: +```sql +SET SESSION enable_star_tree_index=true; +``` +To disable: +```sql +SET SESSION enable_star_tree_index=false; +``` + +## Configuration Properties +| Property Name | Default Value | Required| Description| +|---------------------------------------------------|---------------------|---------|--------------| +| optimizer.enable-star-tree-index | false | No | Enables StarTree Cube| +| cube.metadata-cache-size | 50 | No | The maximum number of metadata for StarTree Cubes that could be loaded into cache before eviction happens| +| cube.metadata-cache-ttl | 1h | No | The maximum time to live of StarTree Cubes that are be loaded into cache before eviction happens | + +## Dependencies + +StarTree Cube relies on Hetu metastore to store the Cube related metadata. +Please check [Hetu Metastore](../admin/meta-store.md) for more information. + +## Examples + +Creating a StarTree Cube: +```sql +CREATE CUBE nation_cube +ON nation +WITH (AGGREGATIONS=(count(*), count(distinct regionkey), avg(nationkey), max(regionkey)), +GROUP=(nationkey), +format='orc', partitioned_by=ARRAY['nationkey']); +``` +Next, to add data to the Cube: +```sql +INSERT INTO CUBE nation_cube WHERE nationkey >= 5; +``` +Creating a StarTree Cube with WHERE clause: +Please note that the following query is only supported via the CLI + +```sql +CREATE CUBE nation_cube +ON nation +WITH (AGGREGATIONS=(count(*), count(distinct regionkey), avg(nationkey), max(regionkey)), +GROUP=(nationkey), +format='orc', partitioned_by=ARRAY['nationkey']) +WHERE nationkey >= 5; +``` + +To use the new Cube, just query the original table using aggregations that were included in the Cube: + +```sql +SELECT count(*) FROM nation WHERE nationkey >= 5 GROUP BY nationkey; +SELECT nationkey, avg(nationkey), max(regionkey) FROM nation WHERE nationkey >= 5 GROUP BY nationkey; +``` + +Since the data inserted into the Cube was for `nationkey >= 5`, only queries matching this condition will utilize the Cube. +Queries not matching the condition would continue to work but won't use the Cube. + +## Building Cube for Large dataset +One of the limitations with the current implementation is that Cube cannot be built for a larger dataset at once. This is due to the cluster memory limitation. +Processing large number of rows requires more memory than cluster is configured with. This results in query failing with message **Query exceeded per-node user memory +limit**. To overcome this issue, **INSERT INTO CUBE** sql support was added. The user has ability to build a Cube for larger data by executing multiple +insert into cube statements. The insert statement accepts a where clause, and it can be used to limit the number of processed and inserted into Cube. + +This section explains the steps to build a Cube for larger dataset. + +Let us take an example of TPCDS dataset and `store_sales` table. The table has 10 years worth of data and user wants to build a Cube for year 2001 +and due to the cluster memory limit, the entire data set for year 2001 cannot be processed at once. + +```sql +CREATE CUBE store_sales_cube ON store_sales WITH (AGGREGATIONS = (sum(ss_net_paid), sum(ss_sales_price), sum(ss_quantity)), GROUP = (ss_sold_date_sk, ss_store_sk)); + +SELECT min(d_date_sk) as year_start, max(d_date_sk) as year_end FROM date_dim WHERE d_year = 2001; + year_start | year_end +------------+---------- + 2451911 | 2452275 +(1 row) +``` +The following query could result in a failure if the number of rows need to be processed is huge and the query memory exceeds +the limit configured for the cluster. + +```sql +INSERT INTO CUBE store_sales_cube WHERE ss_sold_date_sk BETWEEN 2451911 AND 242275; +``` + +### Solution 1) +To overcome this issue, multiple insert statements can be used into process rows and insert into cube and the number of rows can be limited by using where clause; + +```sql +INSERT INTO CUBE store_sales_cube WHERE ss_sold_date_sk BETWEEN 2451911 AND 2452010; +INSERT INTO CUBE store_sales_cube WHERE ss_sold_date_sk >= 2452011 AND ss_sold_date_sk <= 2452110; +INSERT INTO CUBE store_sales_cube WHERE ss_sold_date_sk BETWEEN 2452111 AND 2452210; +INSERT INTO CUBE store_sales_cube WHERE ss_sold_date_sk BETWEEN 2452211 AND 2452275; +``` + +### Solution 2) +CLI has been modified to support creating Cubes for larger dataset and without need for multiple insert statements. CLI internally handles this process. +Once the user runs create cube statement with where clause, the CLI takes care of creating the cube as well as inserting the data into it. This process improves the user experience and +improves the memory footprint based on the cluster memory limits. CLI internally parses the converts the statement into one create cube statement followed by +one or more insert statements. This change is only works if user executes the command from CLI and not via any other means i.e. JDBC, etc... + +```sql +CREATE CUBE store_sales_cube ON store_sales WITH (AGGREGATIONS = (sum(ss_net_paid), sum(ss_sales_price), sum(ss_quantity)), GROUP = (ss_sold_date_sk, ss_store_sk)) WHERE ss_sold_date_sk BETWEEN 2451911 AND 242275; +``` + +Internally the system will rewrite and merge all continuous range predicates into a single predicate; + +```sql +SHOW CUBES; + + Cube Name | Table Name | Status | Dimensions | Aggregations | Where Clause +---------------------------------+----------------------------+--------+-----------------------------+-------------------------------------------------------+-------------------------------------------------------+------------------------------ + hive.tpcds_sf1.store_sales_cube | hive.tpcds_sf1.store_sales | Active | ss_sold_date_sk,ss_store_sk | sum(ss_sales_price),sum(ss_net_paid),sum(ss_quantity) | (("ss_sold_date_sk" >= BIGINT '2451911') AND ("ss_sold_date_sk" < BIGINT '2452276')) +``` + +**Note:** +1. The system will try to rewrite all type of Predicates into a Range to see if they can be merged together. + All continuous predicates will be merged into a single range predicate and remaining predicates are untouched. + + Only the following types are supported and can be merged together. + `Integer, TinyInt, SmallInt, BigInt, Date` + + For other data types, it is difficult to identify if two predicates are continuous therefore they cannot be merged together. And because of this issue, there is + possibility that particular cube may not be used during query optimization even if the cube has all the required data. For example, + +```sql + INSERT INTO CUBE store_sales_cube WHERE store_id BETWEEN 'A01' AND 'A10'; + INSERT INTO CUBE store_sales_cube WHERE store_id BETWEEN 'A11' AND 'A20'; +``` + Here these two predicates cannot be merged into store_id BETWEEN 'A01' AND 'A20'; So the cube won't be used + for queries that are spanning over two the predicates; + +```sql + SELECT ss_store_id, sum(ss_sales_price) WHERE ss_store_id BETWEEN 'A05' AND 'A15'; - Cube won't be used for optimizing this query. This is a limitation as of now. +``` + Because of the predicate rewrite some of the following queries can't be supported + +```sql + INSERT INTO CUBE store_sales_cube WHERE ss_sold_date_sk > 2451911; +``` + The predicate is rewriten as ss_sold_date_sk >= 2451912 to be prepare for merging continous predicates. + Since the predicate is rewritten, they query using ss_sold_date_sk > 2451911 predicate will not match with Cube predicate so Cube won't be used to + optimize the query. The same is applicable for predicates with <= operator. ie. ss_sold_date_sk <= 2451911 is rewritten as ss_sold_date_sk < 2451912 + +```sql + SELECT ss_sold_date_sk, .... FROM hive.tpcds_sf1.store_sales WHERE ss_sold_date_sk > 2451911 +``` +3. Only Single column predicates can be merged. + +## Open issues and Limitations +1. StarTree Cube is only effective when the group by cardinality is considerably fewer than the number of rows in source table. +2. A significant amount of user effort required in maintaining Cubes for large datasets. +3. Only incremental insert into cube is supported. Cannot delete specific rows from Cube. +4. Cubes created on a transaction table may expire automatically even if the source table has not been updated. This is due to the compaction policy which + merges delta files into single large ORC file which in turn changes the last modified of time of the table. Cube status is determined by comparing last modified + timestamp of table when cube was created with the last modified time of the table when queries are executed. +5. Openlookeng CLI has been modified to ease the process of creating Cubes for larger datasets. But still there are limitations with this implementation + as the process involves merging multiple cube predicates into one. Only cube predicates defined on Integer, Long and Date types can be merged properly. Support for Char, + String types still need to be implemented. \ No newline at end of file diff --git a/hetu-docs/en/preagg/statements.md b/hetu-docs/en/preagg/statements.md new file mode 100644 index 000000000..5b423ac13 --- /dev/null +++ b/hetu-docs/en/preagg/statements.md @@ -0,0 +1,175 @@ +# Usage +Cubes can be managed using any of the supported clients, such as hetu-cli located under the `bin` directory in the installation. + +## CREATE CUBE +### Synopsis + +``` sql +CREATE CUBE [ IF NOT EXISTS ] +cube_name ON table_name WITH ( + AGGREGATIONS = ( expression [, ...] ), + GROUP = ( column_name [, ...]) + [, FILTER = (expression)] + [, ( property_name = expression [, ...] ) ] +) +[WHERE predicate] +``` +### Description +Create a new, empty Cube with the specified group and aggregations. Use `INSERT INTO CUBE (see below)` to insert into data. + +The optional `IF NOT EXISTS` clause causes the error to be suppressed if the Cube already exists. +The optional `property_name` section can be used to set properties on the newly created Cube. + +To list all available table properties, run the following query: + + SELECT * FROM system.metadata.table_properties + +**Note:** These properties are limited to the Connector which the Cube is being created for. + +### Examples +Create a new Cube `orders_cube` on `orders`: + + CREATE CUBE orders_cube ON orders WITH ( + AGGREGATIONS = ( SUM(totalprice), AVG(totalprice) ), + GROUP = ( orderstatus, orderdate ), + format = 'ORC' + ) + +Create a new partitioned Cube `orders_cube`: + + CREATE CUBE orders_cube ON orders WITH ( + AGGREGATIONS = ( SUM(totalprice), AVG(totalprice) ), + GROUP = ( orderstatus, orderdate ), + format = 'ORC', + partitioned_by = ARRAY['orderdate'] + ) + +Create a new Cube `orders_cube` with some source data filter + + CREATE CUBE orders_cube ON orders WITH ( + AGGREGATIONS = ( SUM(totalprice), COUNT DISTINCT(orderid) ), + GROUP = ( orderstatus ), + FILTER = (orderdate BETWEEN 2512450 AND 2512460) + ) + +Create a new Cube `orders_cube` with some additional predicate on Cube columns + + CREATE CUBE orders_cube ON orders WITH ( + AGGREGATIONS = ( SUM(totalprice), COUNT DISTINCT(orderid) ), + GROUP = ( orderstatus ), + FILTER = (orderdate BETWEEN 2512450 AND 2512460) + ) WHERE orderstatus = 'PENDING'; + +This is same as following + + CREATE CUBE orders_cube ON orders WITH ( + AGGREGATIONS = ( SUM(totalprice), COUNT DISTINCT(orderid) ), + GROUP = ( orderstatus ), + FILTER = (orderdate BETWEEN 2512450 AND 2512460) + ); + INSERT INTO CUBE orders_cube WHERE orderstatus = 'PENDING'; + +The `FILTER` property can be used to filter out data from the source table while building the Cube. Cube is built on the data after +applying the `orderdate BETWEEN 2512450 AND 2512460` predicate on the source table. The columns used in the filter predicate must not be part the Cube. + +### Limitations +- Cubes can be created with only following aggregation functions. + In other words, Queries using the following functions can only be optimized using Cubes. + **COUNT, COUNT DISTINCT, MIN, MAX, SUM, AVG** +- Different connector might support different data type, and different table/column properties. + +## INSERT INTO CUBE + +### Synopsis +``` sql +INSERT INTO CUBE cube_name [WHERE condition] +``` + +### Description +`CREATE CUBE` statement creates Cube without any data. To insert data into Cube, use `INSERT INTO CUBE` sql. +The `WHERE` clause is optional. If predicate is provided, only data matching the given predicate are processed from the source table and inserted into the Cube. +Otherwise, entire data from the source table is processed and inserted into Cube. + +### Examples +Insert data into the `orders_cube` Cube + + INSERT INTO CUBE orders_cube WHERE orderdate > date '1999-01-01'; + INSERT INTO CUBE order_all_cube; + +### Limitations +1. Subsequent inserts to the same Cube need to use same set of columns + +```sql + CREATE CUBE orders_cube ON orders WITH (AGGREGATIONS = (count(*)), GROUP = (orderdate)); + + INSERT INTO CUBE orders_cube WHERE orderdate BETWEEN date '1999-01-01' AND date '1999-01-05'; + + -- This statement would fail because its possible the Cube already contain rows matching the given predicate. + INSERT INTO CUBE orders_cube WHERE location = 'Canada'; +``` +**Note:** This means that columns used in the first insert must be used in every insert predicate following the first to avoid inserting duplicate data. + +## INSERT OVERWRITE CUBE + +### Synopsis +``` sql +INSERT OVERWRITE CUBE cube_name [WHERE condition] +``` + +### Description +Similar to INSERT INTO CUBE statement but with this statement the existing data is overwritten. Predicates +are optional. + +### Examples +Insert data based on condition into the `orders_cube` Cube: + + INSERT OVERWRITE CUBE orders_cube WHERE orderdate > date '1999-01-01'; + INSERT OVERWRITE CUBE orders_cube; + +## SHOW CUBES + +### Synopsis +```sql +SHOW CUBES [ FOR table_name ]; +``` + +### Description +`SHOW CUBES` lists all Cubes. Adding the optional `table_name` lists only the Cubes for that table. + +### Examples + +Show all Cubes: +```sql + SHOW CUBES; +``` + +Show Cubes for `orders` table: + +```sql + SHOW CUBES FOR orders; +``` + +## DROP CUBE + +### Synopsis + +``` sql +DROP CUBE [ IF EXISTS ] cube_name +``` + +### Description +Drop an existing Cube. + +The optional `IF EXISTS` clause causes the error to be suppressed if the Cube does not exist. + +### Examples + +Drop the Cube `orders_cube`: + + DROP CUBE orders_cube + +Drop the Cube `orders_cube` if it exists: + + DROP CUBE IF EXISTS orders_cube + + diff --git a/hetu-docs/en/sql/create-cube.md b/hetu-docs/en/sql/create-cube.md deleted file mode 100644 index 214083d19..000000000 --- a/hetu-docs/en/sql/create-cube.md +++ /dev/null @@ -1,69 +0,0 @@ -CREATE CUBE -============ - -Synopsis --------- - -``` sql -CREATE CUBE [ IF NOT EXISTS ] -cube_name ON table_name WITH ( - AGGREGATIONS = ( expression [, ...] ), - GROUP = ( column_name [, ...]) - [, FILTER = (expression)] - [, ( property_name = expression [, ...] ) ] -) -``` - -Description ------------ - -Create a new, empty star-tree cube with the specified group and aggregations. Use `insert-into-cube` to insert data. - -The optional `IF NOT EXISTS` clause causes the error to be suppressed if the table already exists. - -The optional `property_name` section can be used to set properties on the newly created cube. To list all available table properties, run the following query: - - SELECT * FROM system.metadata.table_properties - -Examples --------- - -Create a new cube `orders_cube` on `orders`: - - CREATE CUBE orders_cube ON orders WITH ( - AGGREGATIONS = ( SUM(totalprice), AVG(totalprice) ), - GROUP = ( orderstatus, orderdate ), - format = 'ORC' - ) - -Create a new partitioned cube `orders_cube`: - - CREATE CUBE orders_cube ON orders WITH ( - AGGREGATIONS = ( SUM(totalprice), AVG(totalprice) ), - GROUP = ( orderstatus, orderdate ), - format = 'ORC', - partitioned_by = ARRAY['orderdate'] - ) - -Create a new cube 'orders_cube' with some source filter - - CREATE CUBE orders_cube ON orders WITH ( - AGGREGATIONS = ( SUM(totalprice), COUNT DISTINCT(orderid) ), - GROUP = ( orderstatus ), - FILTER = (orderdate BETWEEN 2512450 AND 2512460) - ) - -Filter is additional predicate that applied on the source table when building a cube. The columns used in the filter predicate must not be part the Cube. - -Limitations ------------ - -- Supported aggregate functions: - COUNT, COUNT DISTINCT, MIN, MAX, SUM, AVG -- Only one group supported per Cube. -- Different connector might support different data type, and different table/column properties. -- Can currently only create cubes in Hive connector, but the cubes can be created on a table from another connector. - -See Also --------- -[INSERT INTO CUBE](./insert-cube.md), [SHOW CUBES](./show-cubes.md), [DROP CUBE](./drop-cube.md) \ No newline at end of file diff --git a/hetu-docs/en/sql/drop-cube.md b/hetu-docs/en/sql/drop-cube.md deleted file mode 100644 index 5974705a7..000000000 --- a/hetu-docs/en/sql/drop-cube.md +++ /dev/null @@ -1,33 +0,0 @@ - -DROP CUBE -========== - -Synopsis --------- - -``` sql -DROP CUBE [ IF EXISTS ] cube_name -``` - -Description ------------ - -Drop an existing cube. - -The optional `IF EXISTS` clause causes the error to be suppressed if the cube does not exist. - -Examples --------- - -Drop the cube `orders_cube`: - - DROP CUBE orders_cube - -Drop the cube `orders_cube` if it exists: - - DROP CUBE IF EXISTS orders_cube - -See Also --------- - -[CREATE CUBE](./create-cube.md), [SHOW CUBES](./show-cubes.md), [INSERT INTO CUBE](./insert-cube.md) diff --git a/hetu-docs/en/sql/insert-cube.md b/hetu-docs/en/sql/insert-cube.md deleted file mode 100644 index fbebcf5c6..000000000 --- a/hetu-docs/en/sql/insert-cube.md +++ /dev/null @@ -1,44 +0,0 @@ -INSERT INTO CUBE -====== - -Synopsis --------- - -``` sql -INSERT INTO CUBE cube_name [WHERE condition] -``` - -Description ------------ - -Insert data into a star-tree cube. Predicate information is optional. If predicate provided, only data matching -the given predicate are processed from the source table and inserted into the cube. Otherwise, entire -data from the source table is processed and inserted into Cube. - -Examples --------- - -Insert data based on condition into the `orders_cube` cube: - - INSERT INTO CUBE orders_cube WHERE orderdate > date '1999-01-01'; - INSERT INTO CUBE order_all_cube; - -See Also --------- - -[INSERT OVERWRITE CUBE](./insert-overwrite-cube.md), [CREATE CUBE](./create-cube.md), [SHOW CUBES](./show-cubes.md), [DROP CUBE](./drop-cube.md) - - -Limitations ----------- -1. Insert statement does not allow different columns to be used in the where clause for successive inserts. - -```sql - CREATE CUBE orders_cube ON orders WITH (AGGREGATIONS = (count(*)), GROUP = (orderdate)); - - INSERT INTO CUBE orders_cube WHERE orderdate BETWEEN date '1999-01-01' AND date '1999-01-05'; - - -- This statement would fail because its possible the Cube already contain rows matching the given predicate. - INSERT INTO CUBE orders_cube WHERE location = 'Canada'; -``` -Note: this means that columns used in the first insert must be used in every insert predicate following the first to avoid inserting duplicate data. diff --git a/hetu-docs/en/sql/insert-overwrite-cube.md b/hetu-docs/en/sql/insert-overwrite-cube.md deleted file mode 100644 index 8c55f3817..000000000 --- a/hetu-docs/en/sql/insert-overwrite-cube.md +++ /dev/null @@ -1,28 +0,0 @@ -INSERT INTO CUBE -====== - -Synopsis --------- - -``` sql -INSERT OVERWRITE CUBE cube_name [WHERE condition] -``` - -Description ------------ - -Similar to INSERT INTO CUBE statement but with this statement the existing data is overwritten. Predicates -are optional. - -Examples --------- - -Insert data based on condition into the `orders_cube` cube: - - INSERT OVERWRITE CUBE orders_cube WHERE orderdate > date '1999-01-01'; - INSERT OVERWRITE CUBE orders_cube; - -See Also --------- - -[INSERT INTO CUBE](./insert-cube.md), [CREATE CUBE](./create-cube.md), [SHOW CUBES](./show-cubes.md), [DROP CUBE](./drop-cube.md) diff --git a/hetu-docs/en/sql/show-cubes.md b/hetu-docs/en/sql/show-cubes.md deleted file mode 100644 index 3d9b89c9f..000000000 --- a/hetu-docs/en/sql/show-cubes.md +++ /dev/null @@ -1,35 +0,0 @@ - -SHOW CUBES -========== - -Synopsis --------- - -``` sql -SHOW CUBES [ FOR table_name ]; -``` - -Description ------------ - -`SHOW CUBES` lists all cubes. Adding the optional `table_name` lists only the cubes for that table. - -Examples --------- - -Show all cubes: - -```sql - SHOW CUBES; -``` - -Show cubes for `orders` table: - -```sql - SHOW CUBES FOR orders; -``` - -See Also --------- - -[CREATE CUBE](./create-cube.md), [DROP CUBE](./drop-cube.md), [INSERT INTO CUBE](./insert-cube.md) diff --git a/hetu-docs/zh/develop/star-tree-cube.md b/hetu-docs/zh/develop/star-tree-cube.md deleted file mode 100644 index 1eb8083f1..000000000 --- a/hetu-docs/zh/develop/star-tree-cube.md +++ /dev/null @@ -1,70 +0,0 @@ -# Star-Tree - -Star-tree多维数据集是一种预聚合技术,用于实现低延迟冰山查询。冰山查询用于计算属性(或属性集)的聚合函数,以便查找高于指定阈值的聚合值。通过该技术,用户能够创建具有必要聚合和维度的多维数据集。然后,当执行聚合查询时,多维数据集用于执行查询而非原始表。实际性能提升是在TableScan操作期间实现的,因为多维数据集是预计算和预聚合的。 - -因此,当group by基数产生的行少于原始表时,多维数据集技术非常有效。 - -## 支持功能 - - COUNT, COUNT DISTINCT, MIN, MAX, SUM, AVG - -## 启用和禁用star-tree - -启用star-tree: - -```sql -SET SESSION enable_star_tree_index=true; -``` - -禁用star-tree: - -```sql -SET SESSION enable_star_tree_index=false; -``` - -## 配置属性 - -| 属性名称| 默认值| 是否必填| 说明| -|----------|----------|----------|----------| -| optimizer.enable-star-tree-index| false| 否| 启用star-tree索引| -| cube.metadata-cache-size| 5| 否| 在缓存清空前可以加载到缓存中的star-tree元数据的最大数量| -| cube.metadata-cache-ttl| 1h| 否| 在缓存清空前加载到缓存中的star-tree的最长保留时间| - -## 示例 - -创建star-tree多维数据集: - -```sql -CREATE CUBE nation_cube -ON nation -WITH (AGGREGATIONS=(count(*), count(distinct regionkey), avg(nationkey), max(regionkey)), -GROUP=(nationkey), -format='orc', partitioned_by=ARRAY['nationkey']); -``` - -向多维数据集添加数据: - -```sql -INSERT INTO CUBE nation_cube WHERE nationkey > 5; -``` - -要使用新多维数据集,只需使用多维数据集中包含的聚合来查询原始表: - -```sql -SELECT count(*) FROM nation WHERE nationkey > 5 GROUP BY nationkey; -SELECT nationkey, avg(nationkey), max(regionkey) WHERE nationkey > 5 GROUP BY nationkey; -``` - -## 优化器变更 - -Star-tree聚合规则为迭代优化器,通过将原始聚合子树和原始表扫描替换为预聚合表扫描来优化逻辑计划。 - -## 依赖 - -Star-tree索引依赖于Hetu元存储来存储多维数据集相关的元数据。有关更多信息,请查看[Hetu元存储](../admin/meta-store.md)。 - -## 限制 - -1. Star-tree多维数据集仅在group by基数远低于源表中的行数时有效。 -2. 维护大型数据集的多维数据集需要大量的用户人力。 -3. 仅支持增量插入多维数据集。无法从多维数据集删除特定行。 \ No newline at end of file diff --git a/hetu-docs/zh/index.md b/hetu-docs/zh/index.md index 175db94a6..6a16cedd8 100644 --- a/hetu-docs/zh/index.md +++ b/hetu-docs/zh/index.md @@ -200,7 +200,7 @@ headless: true - [文件系统访问实用程序]({{< relref "./docs/develop/filesystem.md" >}}) - [Hive ORC Cache]({{< relref "./docs/develop/hive-orc-cache.md" >}}) - [外部函数注册和下推]({{< relref "./docs/develop/externalfunction-registration-pushdown.md" >}}) - - [Star-tree多维数据集]({{< relref "./docs/develop/star-tree-cube.md" >}}) + - [openLooKeng REST接口说明]({{< relref "./docs/rest/_index.md" >}}) - [节点资源]({{< relref "./docs/rest/node.md" >}}) - [查询资源]({{< relref "./docs/rest/query.md" >}}) diff --git a/hetu-docs/zh/sql/create-cube.md b/hetu-docs/zh/sql/create-cube.md deleted file mode 100644 index 65d9ed37c..000000000 --- a/hetu-docs/zh/sql/create-cube.md +++ /dev/null @@ -1,51 +0,0 @@ -# CREATE CUBE - -## 使用方式 - -```sql -CREATE CUBE [ IF NOT EXISTS ] -cube_name ON table_name WITH ( - AGGREGATIONS = ( expression [, ...] ), GROUP = ( column_name [, ...] ) - [, ( property_name = expression [, ...] ) ] -) -``` - -## 介绍 - -使用指定的组和聚合创建新的star-tree多维数据集。使用`insert-into-cube`插入数据。 - -如果表已存在,可选的`IF NOT EXISTS`子句将导致错误被隐藏。 - -可以使用可选的`property_name`标签来设置创建的多维数据集的属性。要列出所有可用的表属性,请运行以下查询: - - SELECT * FROM system.metadata.table_properties - -## 示例 - -在`orders`上创建新多维数据集`orders_cube`: - - CREATE CUBE orders_cube ON orders WITH ( - AGGREGATIONS = ( SUM(totalprice), AVG(totalprice) ), - GROUP = ( orderstatus, orderdate ), - format = 'ORC' - ) - -创建新的分区多维数据集`orders_cube`: - - CREATE CUBE orders_cube ON orders WITH ( - AGGREGATIONS = ( SUM(totalprice), AVG(totalprice) ), - GROUP = ( orderstatus, orderdate ), - format = 'ORC', - partitioned_by = ARRAY['orderdate'] - ) - -## 限制 - -- 支持的聚合函数:COUNT、COUNT DISTINCT、MIN、MAX、SUM、AVG -- 每个多维数据集仅支持一个组。 -- 不同的连接器可能支持不同的数据类型和不同的表/列属性。 -- 当前只能在Hive连接器中创建多维数据集,但可以从另一个连接器在表中创建多维数据集。 - -## 另请参见 - -[INSERT INTO CUBE](./insert-cube.md)、[SHOW CUBES](./show-cubes.md)、[DROP CUBE](./drop-cube.md) \ No newline at end of file diff --git a/hetu-docs/zh/sql/drop-cube.md b/hetu-docs/zh/sql/drop-cube.md deleted file mode 100644 index af8d76544..000000000 --- a/hetu-docs/zh/sql/drop-cube.md +++ /dev/null @@ -1,27 +0,0 @@ -# DROP CUBE - -## 使用方式 - -```sql -DROP CUBE [ IF EXISTS ] cube_name -``` - -## 说明 - -删除已存在的多维数据集。 - -如果多维数据集不存在,可选子句`IF EXISTS`将导致错误被隐藏。 - -## 示例 - -删除多维数据集`orders_cube`: - - DROP CUBE orders_cube - -如果多维数据集`orders_cube`存在,则将其删除: - - DROP CUBE IF EXISTS orders_cube - -## 另请参见 - -[CREATE CUBE](./create-cube.md)、[SHOW CUBES](./show-cubes.md)、[INSERT INTO CUBE](./insert-cube.md) \ No newline at end of file diff --git a/hetu-docs/zh/sql/insert-cube.md b/hetu-docs/zh/sql/insert-cube.md deleted file mode 100644 index e6c23d134..000000000 --- a/hetu-docs/zh/sql/insert-cube.md +++ /dev/null @@ -1,55 +0,0 @@ -# INSERT INTO CUBE - -## 使用方式 - -```sql -INSERT INTO CUBE cube_name [WHERE condition] -``` - -## 介绍 - -将数据插入star-tree多维数据集。谓词信息为可选项。如果提供了谓词,则仅从源表处理与给定谓词匹配的数据并将其插入多维数据集。否则,将处理源表中的全部数据并将其插入多维数据集。 - -## 示例 - -根据条件将数据插入`orders_cube`多维数据集中: - - INSERT INTO CUBE orders_cube WHERE orderdate > date '1999-01-01'; - INSERT INTO CUBE order_all_cube; - -## 另请参见 - -[INSERT OVERWRITE CUBE](./insert-overwrite-cube.md)、[CREATE CUBE](./create-cube.md)、[SHOW CUBES](./show-cubes.md)、[DROP CUBE](./drop-cube.md) - -## 限制 - -1. 如果在where子句谓词中使用两个不同的列,则insert语句将无法正常运行。 - -```sql - CREATE CUBE orders_cube ON orders WITH (AGGREGATIONS = (count(*)), GROUP = (orderdate)); - - INSERT INTO CUBE orders_cube WHERE orderdate BETWEEN date '1999-01-01' AND date '1999-01-05'; - - -- This statement would fail because its possible the Cube already contain rows matching the given predicate. - INSERT INTO CUBE orders_cube WHERE location = 'Canada'; -``` - -2. 范围谓词问题。 - -```sql - CREATE CUBE orders_cube ON orders WITH (AGGREGATIONS = (count(*)), GROUP = (orderdate)); - - INSERT INTO CUBE orders_cube WHERE orderdate BETWEEN date '1999-01-01' AND date '1999-01-05'; - INSERT INTO CUBE orders_cube WHERE orderdate BETWEEN date '1999-01-06' AND date '1999-01-10'; - - SET SESSION enable_star_tree_index=true; - - -- This statement uses orders_cube -- - SELECT count(*) FROM orders WHERE orderdate BETWEEN date '1999-01-03' AND date '1999-01-04'; - -- This statement uses orders_cube -- - SELECT count(*) FROM orders WHERE orderdate BETWEEN date '1999-01-07' AND date '1999-01-09'; - -- This statement does not user orders_cube because the two predicates used in the INSERT statement cannot be merged - -- and the TupleDomain evaluation to check cubePredicate.contains(statementPredicate) evaluates to false - - SELECT count(*) FROM orders WHERE orderdate BETWEEN date '1999-01-04' AND date '1999-01-07'; -``` \ No newline at end of file diff --git a/hetu-docs/zh/sql/insert-overwrite-cube.md b/hetu-docs/zh/sql/insert-overwrite-cube.md deleted file mode 100644 index f3f60d1a2..000000000 --- a/hetu-docs/zh/sql/insert-overwrite-cube.md +++ /dev/null @@ -1,22 +0,0 @@ -# INSERT INTO CUBE - -## 使用方式 - -```sql -INSERT OVERWRITE CUBE cube_name [WHERE condition] -``` - -## 介绍 - -类似于INSERT INTO CUBE语句,但使用此语句将覆盖现有数据。谓词为可选项。 - -## 示例 - -根据条件将数据插入`orders_cube`多维数据集中: - - INSERT OVERWRITE CUBE orders_cube WHERE orderdate > date '1999-01-01'; - INSERT OVERWRITE CUBE orders_cube; - -## 另请参见 - -[INSERT INTO CUBE](./insert-cube.md)、[CREATE CUBE](./create-cube.md)、[SHOW CUBES](./show-cubes.md)、[DROP CUBE](./drop-cube.md) \ No newline at end of file diff --git a/hetu-docs/zh/sql/show-cubes.md b/hetu-docs/zh/sql/show-cubes.md deleted file mode 100644 index f229186dc..000000000 --- a/hetu-docs/zh/sql/show-cubes.md +++ /dev/null @@ -1,29 +0,0 @@ -# SHOW CUBES - -## 使用方式 - -```sql -SHOW CUBES [ FOR table_name ]; -``` - -## 说明 - -`SHOW CUBES`列举所有多维数据集。添加选项`table_name`仅列出该表的多维数据集。 - -## 示例 - -显示所有多维数据集: - -```sql - SHOW CUBES; -``` - -显示`orders`表的多维数据集: - -```sql - SHOW CUBES FOR orders; -``` - -## 另请参见 - -[CREATE CUBE](./create-cube.md)、[DROP CUBE](./drop-cube.md)、[INSERT INTO CUBE](./insert-cube.md) \ No newline at end of file