如何在雪花上优化连接到中等大小的II型表?

时间:2018-05-31 22:30:58

标签: snowflake-datawarehouse

背景

假设我有以下表格:

-- 33M rows
CREATE TABLE lkp.session (
    session_id BIGINT,
    visitor_id BIGINT,
    session_datetime TIMESTAMP
);

-- 17M rows
CREATE TABLE lkp.visitor_customer_hist (
    visitor_id BIGINT,
    customer_id BIGINT,
    from_datetime TIMESTAMP,
    to_datetime TIMESTAMP
);

Visitor_customer_hist给出了对每个访问者在每个时间点生效的customer_id。

目标是使用visitor_id和session_datetime查找对每个会话有效的客户ID。

CREATE TABLE lkp.session_effective_customer AS
    SELECT
        s.session_id,
        vch.customer_id AS effective_customer_id
    FROM lkp.session s
    JOIN lkp.visitor_customer_hist vch ON vch.visitor_id = s.visitor_id
        AND s.session_datetime >= vch.from_datetime
        AND s.session_datetime < vch.to_datetime;

问题

即使仓库规模扩大,此查询极其缓慢。它需要1小时15分钟才能完成,这是仓库中运行的唯一查询。

我验证了visitor_customer_hist中没有重叠值,其存在可能会导致重复加入。

雪花在这种加入方面真的很糟糕吗?我正在寻找建议,我可以如何优化表格以进行此类查询,重新聚类,或任何优化技术或重新处理查询,例如也许是相关的子查询或其他东西。

其他信息

资料: Profile

3 个答案:

答案 0 :(得分:1)

如果 lkp.session 表包含 narrow 时间范围,并且 lkp.visitor_customer_hist 表包含 wide 时间范围,您可能会受益于重写查询以添加限制连接中考虑的行范围的冗余条件:

CREATE TABLE lkp.session_effective_customer AS
SELECT
    s.session_id,
    vch.customer_id AS effective_customer_id
FROM lkp.session s
JOIN lkp.visitor_customer_hist vch ON vch.visitor_id = s.visitor_id
    AND s.session_datetime >= vch.from_datetime
    AND s.session_datetime < vch.to_datetime
WHERE vch.to_datetime >= (select min(session_datetime) from lkp.session)
    AND  vch.from_datetime <= (select max(session_datetime) from lkp.session);

另一方面,如果两个表格覆盖相似的广泛日期范围并且随着时间的推移会有大量客户与特定访问者相关联,那么这将非常有帮助。

答案 1 :(得分:0)

关注Stuart's answer,我们可以通过查看访问者的最小值和最大值来过滤它。像这样:

void MainWindow::on_actionAuthor_descending_triggered()
{
QSortFilterProxyModel *m = new QSortFilterProxyModel(this);

m->setSourceModel(model);
m->setSortCaseSensitivity(Qt::CaseInsensitive);

m->sort(1, Qt::DescendingOrder);

ui->tableView->setModel(m);
}

然后用我们的轻量级组织表,也许我们会有更好的运气:

CREATE TEMPORARY TABLE _vch AS
    SELECT
        l.visitor_id,
        l.customer_id,
        l.from_datetime,
        l.to_datetime
    FROM (
             SELECT
                 l.visitor_id,
                 min(l.session_datetime) AS mindt,
                 max(l.session_datetime) AS maxdt
             FROM lkp.session l
             GROUP BY l.visitor_id
         ) a
    JOIN lkp.visitor_customer_hist l ON a.visitor_id = l.visitor_id
        AND l.from_datetime >= a.mindt
        AND l.to_datetime <= a.maxdt;

不幸的是,在我的情况下,虽然我过滤掉了很大比例的行,但是问题访问者(访问者数量上有成千上万条记录的访问者)仍然存在问题(即他们仍有数千条记录,导致加入爆炸)。

但在其他情况下,这可行。

答案 2 :(得分:0)

如果两个表的每个访问者的记录数都很高,那么这个连接是有问题的,因为Marcin在评论中描述了这个原因。因此,在这种情况下,如果可能的话,最好完全避免这种连接

我最终解决此问题的方法是废弃visitor_customer_hist表并编写自定义窗口函数/ udtf。

最初我创建了lkp.visitor_customer_hist表因为它可以使用现有的窗口函数创建,并且在非MPP sql数据库上可以创建适当的索引,这将使查找具有足够的性能。它是这样创建的:

CREATE TABLE lkp.visitor_customer_hist AS
    SELECT
        a.visitor_id AS visitor_id,
        a.customer_id AS customer_id,
        nvl(lag(a.session_datetime) OVER ( PARTITION BY a.visitor_id
            ORDER BY a.session_datetime ), '1900-01-01') AS from_datetime,
        CASE WHEN lead(a.session_datetime) OVER ( PARTITION BY a.visitor_id
            ORDER BY a.session_datetime ) IS NULL THEN '9999-12-31'
        ELSE a.session_datetime END AS to_datetime
    FROM (
             SELECT
                 s.session_id,
                 vs.visitor_id,
                 customer_id,
                 row_number() OVER ( PARTITION BY vs.visitor_id, s.session_datetime
                     ORDER BY s.session_id ) AS rn,
                 lead(s.customer_id) OVER ( PARTITION BY vs.visitor_id
                     ORDER BY s.session_datetime ) AS next_cust_id,
                 session_datetime
             FROM "session" s
             JOIN "visitor_session" vs ON vs.session_id = s.session_id
             WHERE s.customer_id <> -2
         ) a
    WHERE (a.next_cust_id <> a.customer_id
        OR a.next_cust_id IS NULL) AND a.rn = 1;

所以,废弃这种方法,我写了下面的UDTF:

CREATE OR REPLACE FUNCTION udtf_eff_customer(customer_id FLOAT)
    RETURNS TABLE(effective_customer_id FLOAT)
LANGUAGE JAVASCRIPT
IMMUTABLE
AS '
{
    initialize: function() {
        this.customer_id = -1;
    },

    processRow: function (row, rowWriter, context) {
        if (row.CUSTOMER_ID != -1) {
            this.customer_id = row.CUSTOMER_ID;
        }
        rowWriter.writeRow({EFFECTIVE_CUSTOMER_ID:  this.customer_id});
    },

    finalize: function (rowWriter, context) {/*...*/},
}
';

它可以这样应用:

SELECT
    iff(a.customer_id <> -1, a.customer_id, ec.effective_customer_id) AS customer_id,
    a.session_id
FROM "session" a
JOIN table(udtf_eff_customer(nvl2(a.visitor_id, a.customer_id, NULL) :: DOUBLE) OVER ( PARTITION BY a.visitor_id
    ORDER BY a.session_datetime DESC )) ec

这样就完成了预期的结果:对于每个会话,如果customer_id不是“未知”,那么我们继续使用它;否则,我们使用可以与该访问者关联的下一个customer_id(如果存在)(按会话时间排序)。

这是比创建查找表更好的解决方案;它本质上只需要一次传递数据,需要更少的代码/复杂性,并且速度非常快。