Optimize Cube.js Performance with Pre-Aggregations
转自:https://cube.dev/blog/high-performance-data-analytics-with-cubejs-pre-aggregations/ 可以了解 Pre-Aggregations的处理流程
This is an advanced tutorial. If you are just getting started with Cube.js, I recommend checking this tutorial first and then coming back here.
One of the most powerful features of Cube.js is pre-aggregations. Coupled with data schema, it eliminates the need to organize, denormalize, and transform data before using it with Cube.js. The pre-aggregation engine builds a layer of aggregated data in your database during the runtime and maintains it to be up-to-date.


Upon an incoming request, Cube.js will first look for a relevant pre-aggregation. If it cannot find any, it will build a new one. Once the pre-aggregation is built, all the subsequent requests will go to the pre-aggregated layer instead of hitting the raw data. It could speed the response time by hundreds or even thousands of times.
Pre-aggregations are materialized query results persisted as tables. In order to start using pre-aggregations, Cube.js should have write access to the stb_pre_aggregations schema where pre-aggregation tables will be stored.
Cube.js also takes care of keeping the pre-aggregation up-to-date. It performs refresh checks and if it finds that a pre-aggregation is outdated, it schedules a refresh in the background.
Creating a Simple Pre-Aggregation
Let’s take a look at the example of how we can use pre-aggregations to improve query performance.
For testing purposes, we will use a Postgres database and will generate around ten million records using the generate_series function.
$ createdb cubejs_test
The following SQL creates a table, orders, and inserts a sample of generated records into it.
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
amount integer,
created_at timestamp without time zone
);
CREATE INDEX orders_created_at_amount ON orders(created_at, amount);
INSERT INTO orders (created_at, amount)
SELECT
created_at,
floor((1000 + 500*random())*log(row_number() over())) as amount
FROM generate_series
( '1997-01-01'::date
, '2017-12-31'::date
, '1 minutes'::interval) created_at
Next, create a new Cube.js application if you don’t have any.
$ npm install -g cube.js
$ cubejs create test-app -d postgres
Change the content of .env in the project folder to the following.
CUBEJS_API_SECRET=SECRET
CUBEJS_DB_TYPE=postgres
CUBEJS_DB_NAME=cubejs_test
Finally, generate a schema for the orders table and start the Cube.js server.
$ cubejs generate -t orders
$ npm run dev
Now, we can send a query to Cube.js with the Orders.count measure and Orders.createdAt time dimension with granularity set to month.
curl \
-H "Authorization: EXAMPLE-API-TOKEN" \
-G \
--data-urlencode 'query={
"measures" : ["Orders.amount"],
"timeDimensions":[{
"dimension": "Orders.createdAt",
"granularity": "month",
"dateRange": ["1997-01-01", "2017-01-01"]
}]
}' \
http://localhost:4000/cubejs-api/v1/load
Cube.js will respond with Continue wait, because this query takes more than 5 seconds to process. Let’s look at Cube.js logs to see exactly how long it took for our Postgres to execute this query.
Performing query completed:
{
"queueSize":2,
"duration":6514,
"queryKey":[
"
SELECT
date_trunc('month', (orders.created_at::timestamptz at time zone 'UTC')) \"orders.created_at_month\",
sum(orders.amount) \"orders.amount\"
FROM
public.orders AS orders
WHERE (
orders.created_at >= $1::timestamptz
AND orders.created_at <= $2::timestamptz
)
GROUP BY 1
ORDER BY 1 ASC limit 10000
",
[
"2000-01-01T00:00:00Z",
"2017-01-01T23:59:59Z"
],
[]
]
}
It took 6,514 milliseconds (6.5 seconds) for Postgres to execute the above query. Although we have an index on the created_at and amount columns, it doesn't help a lot in this particular case since we're querying almost all the dates we have. The index would help if we query a smaller date range, but still, it would be a matter of seconds, not milliseconds.
We can significantly speed it up by adding a pre-aggregation layer. To do this, add the following preAggregations block to src/Orders.js:
preAggregations: {
amountByCreated: {
type: `rollup`,
measureReferences: [amount],
timeDimensionReference: createdAt,
granularity: `month`
}
}
The block above instructs Cube.js to build and use a rollup type of pre-aggregation when the “Orders.amount” measure and “Orders.createdAt” time dimension (with “month” granularity) are requested together. You can read more about pre-aggregation options in the documentation reference.
Now, once we send the same request, Cube.js will detect the pre-aggregation declaration and will start building it. Once it's built, it will query it and send the result back. All the subsequent queries will go to the pre-aggregation layer.
Here is how querying pre-aggregation looks in the Cube.js logs:
Performing query completed:
{
"queueSize":1,
"duration":5,
"queryKey":[
"
SELECT
\"orders.created_at_month\" \"orders.created_at_month\",
sum(\"orders.amount\") \"orders.amount\"
FROM
stb_pre_aggregations.orders_amount_by_created
WHERE (
\"orders.created_at_month\" >= ($1::timestamptz::timestamptz AT TIME ZONE 'UTC')
AND
\"orders.created_at_month\" <= ($2::timestamptz::timestamptz AT TIME ZONE 'UTC')
)
GROUP BY 1 ORDER BY 1 ASC LIMIT 10000
",
[
"1995-01-01T00:00:00Z",
"2017-01-01T23:59:59Z"
],
[
[
"
CREATE TABLE
stb_pre_aggregations.orders_amount_by_created
AS SELECT
date_trunc('month', (orders.created_at::timestamptz AT TIME ZONE 'UTC')) \"orders.created_at_month\",
sum(orders.amount) \"orders.amount\"
FROM
public.orders AS orders
GROUP BY 1
",
[]
]
]
]
}
As you can see, now it takes only 5 milliseconds (1,300 times faster) to get the same data. Also, you can note that SQL has been changed and now it queries data from stb_pre_aggregations.orders_amount_by_created, which is the table generated by Cube.js to store pre-aggregation for this query. The second query is a DDL statement for this pre-aggregation table.
Pre-Aggregations Refresh
Cube.js also takes care of keeping pre-aggregations up to date. Every two minutes on a new request Cube.js will initiate the refresh check.
You can set up a custom refresh check strategy by using refreshKey. By default, pre-aggregations are refreshed every hour.
If the result of the refresh check is different from the last one, Cube.js will initiate the rebuild of the pre-aggregation in the background and then hot swap the old one.
Next Steps
This guide is the first step to learning about pre-aggregations and how to start using them in your project. But there is much more you can do with them. You can find the pre-aggregations documentation reference here.
Also, here are some highlights with useful links to help you along the way.
Pre-aggregate queries across multiple cubes
Pre-aggregations work not only for measures and dimensions inside the single cube, but also across multiple joined cubes as well. If you have joined cubes, you can reference measures and dimensions from any part of the join tree. The example below shows how the Users.country dimension can be used with the Orders.count and Orders.revenue measures.
cube(`Orders`, {
sql: `select * from orders`,
joins: {
Users: {
relationship: `belongsTo`,
sql: `${CUBE}.user_id = ${Users}.id`
}
},
// …
preAggregations: {
categoryAndDate: {
type: `rollup`,
measureReferences: [count, revenue],
dimensionReferences: [Users.country],
timeDimensionReference: createdAt,
granularity: `day`
}
}
});
Generate pre-aggregations dynamically
Since pre-aggregations are part of the data schema, which is basically a Javascript code, you can dynamically create all the required pre-aggregations. This guide covers how you can dynamically generate a Cube.js schema.
Time partitioning
You can instruct Cube.js to partition pre-aggregations by time using the partitionGranularity option. Cube.js will generate not a single table for the whole pre-aggregation, but a set of smaller tables. It can reduce the refresh time and cost in the case of BigQuery for example.
Time partitioning documentation reference.
preAggregations: {
categoryAndDate: {
type: `rollup`,
measureReferences: [count],
timeDimensionReference: createdAt,
granularity: `day`,
partitionGranularity: `month`
}
}
Data Cube Lattices
Cube.js can automatically build rollup pre-aggregations without the need to specify which measures and dimensions to use. It learns from query history and selects an optimal set of measures and dimensions for a given query. Under the hood it uses the Data Cube Lattices approach.
It is very useful if you need a lot of pre-aggregations and you don't know ahead of time which ones exactly. Using autoRollup will save you from coding manually all the possible aggregations.
You can find documentation for auto rollup here.
cube(`Orders`, {
sql: `select * from orders`,
preAggregations: {
main: {
type: `autoRollup`
}
}
});
Optimize Cube.js Performance with Pre-Aggregations的更多相关文章
- cube.js 开源模块化分析框架
cube.js 是一款很不错的模块化web 应用分析框架.cube.js 的设计主要是面向serverless 服务, 但是同时也支持所有rdbms, cube.js不是一个单体应用,包含了以下部分: ...
- cube.js 最近的一些更新
cube.js 是一个和不错的数据分析框架,最近又有了一些新的功能支持,以下是一些简单的 总结 基于web socket 的预览支持 react hooks api 支持 支持基于reecharts ...
- cube.js 通过presto-gateway 进行连接
cube.js 对于presto 的支持是通过presto-client 刚好简单修改了一个可以支持presto-gateway 连接的 以下是一个简单的集成,以及关于集成中原有的一些修改 环境准备 ...
- 通过patch 方式解决cube.js 集成cratedb 的问题
今天有写过一个简单的cube.js 集成cratedb 的说明,主要是在driver 上的兼容问题,处理方法是删除不兼容的代码 实际上我们也可以通过类似linux c 开发中的patch 方式解决,简 ...
- cube.js 集成cratedb 的尝试
cratedb 提供了pg协议的兼容,我们可以直接使用pg client 连接,但是也不是完整实现pg 协议的 以下是 cube.js 集成cratedb 的一些尝试 环境准备 docker-comp ...
- cube.js 新版本试用preosto
cube.js 新的版本添加了更多的数据库的支持,但是目前cubejs-cli 以及官方文档问题还挺多,使用不清晰,文档有明显的错误 以下演示presto 数据库的使用 环境准备 安装新版本的cube ...
- cube.js 最近版本的一些更新
有一段时间没有关注cube.js 了,刚好晚上收到一封来自官方的更新介绍,这里简单说明下 更多的数据驱动支持 bigquey, clickhouse snowflake,presto (很棒),hiv ...
- cube.js 学习 cube 连接mongodb 试用
cube.js 对于mongodb 的连接是通过mongodb bi connector(mysql 协议)处理的,以下为简单的试用 安装mongo bi connector 这个玩意用docker ...
- cube.js 学习 cube docker-compose 运行
cube.js 官方为我们也提供了backeng 部署的模型,为了测试方便以下是一个使用docker-compose 运行的demo 项目是一个集成gitbase 的demo,实际可以按照自己的项目修 ...
随机推荐
- Mysql中HAVING的相关使用方法
having字句可以让我们筛选分组之后的各种数据,where字句在聚合前先筛选记录,也就是说作用在group by和having字句前. 而having子句在聚合后对组记录进行筛选.我的理解就是真实表 ...
- Crontab详细用法-定时任务详解
crontab是linux系统或unix系统中常用的定时命令,使用crontab你可以在指定的时间执行一个shell脚本或者一系列Linux/unix命令.例如系统管理员安排一个备份任务使其每天都运行 ...
- 【转载】C#中string.IsNullOrEmpty和string.IsNullOrWhiteSpace区别
在C#中判断字段是否为空或者Null的时候,我们一般会使用到string.IsNullOrEmpty和string.IsNullOrWhiteSpace方法,这两个方法在大部分情况下判断的结果是一致的 ...
- commonjs 与 es6相关Module语法的区别
1.export 在接口名字与模块内部的变量之间建立了一一对应的关系,export输出的接口,与其模块内对应的变量值是动态绑定的,即通过暴露的接口可以取到模块内与之对应绑定变量的实时的值. commo ...
- js 字节操作
获取byte的高4位bit和低4位bit function getHeight4(ata){//获取高四位 int height; height = ((data & 0xf0) >&g ...
- [LeetCode] 448. 找到所有数组中消失的数字 ☆
描述 给定一个范围在 1 ≤ a[i] ≤ n ( n = 数组大小 ) 的 整型数组,数组中的元素一些出现了两次,另一些只出现一次. 找到所有在 [1, n] 范围之间没有出现在数组中的数字. 您 ...
- [LeetCode] 64. 最小路径和 ☆☆☆(动态规划)
描述 给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小. 说明:每次只能向下或者向右移动一步. 示例: 输入:[ [1,3,1], [1,5,1 ...
- ScheduledExecutorService周期性的定时任务
从j2se的api文档上查看ScheduledExecutorService的方法都是推迟一段时间然后相隔一段时间之后再去执行,没有想Timer定时器一样的可以在定点时间执行的api,如果也想像Tim ...
- Flask-SQLAlchemy操作指南
Flask-SQLAlchemy官方文档 from flask_sqlalchemy import SQLAlchemy app = Flask(__name__)app.config['SQLALC ...
- python中csv模块和join函数的使用
在看项目的时候恰好又碰到了这两个功能,然后稍微记录一下,关于join函数,是一个经常使用的联合函数,作用就是用自己规定的字符去串联字符串和列表之类的,对于字符串来说,join函数针对的是字符串中的每一 ...