【问题标题】:How to optimize a join to a moderately large type II table on snowflake?如何优化雪花上中等大型 II 型表的连接?
【发布时间】:2018-05-31 22:30:58
【问题描述】:

背景

假设我有以下表格:

-- 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;

问题

即使仓库规模扩大,这个查询非常很慢。耗时 1h15m 完成,是仓库中唯一运行的查询。

我确认visitor_customer_hist 中没有重叠值,如果存在重叠值可能会导致重复连接。

snowflake 真的不擅长这种连接吗?我正在寻找有关如何优化此类查询、重新聚类或任何优化技术或重新处理查询的表的建议,例如可能是相关子查询之类的。

其他信息

简介:

【问题讨论】:

  • 这似乎出奇的长。我想知道 visitor_customer_history 中每个访问者有多少行——可能会导致连接爆炸。作为线索,运行以下命令需要多长时间(结果是什么)? SELECT count(*) FROM lkp.session s JOIN lkp.visitor_customer_hist vch ON vch.visitor_id = s.visitor_id
  • 我希望你能看到这个!重新加入爆炸,我们可以从插入中看到行没有被加入重复...但是 98% 的访问者有
  • 我会尝试排除超过 1000 条记录的访问者,看看它是否会更快...
  • 这绝对是出乎意料的。我会联系 Snowflake 支持,提供确切的查询 ID,他们可能会提供更多帮助。
  • 另外,@chorbs 所说的可能会有所帮助——即使没有重叠的值,连接在 visitor_id 上也是“快”的,因为它是相等的,然后在时间范围上是“慢”的。如果例如两个表中有 100,000 个相同的 visitor_id 实例,这将导致连接内的严重爆炸。

标签: snowflake-cloud-data-platform


【解决方案1】:

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

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);

另一方面,如果两个表格都涵盖了相似的广泛日期范围,并且随着时间的推移有大量客户与给定的访问者相关联,这将无济于事。

【讨论】:

    【解决方案2】:

    Stuart's answer 之后,我们可以通过查看访客的最小值和最大值对其进行更多过滤。像这样:

    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;
    

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

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

    不幸的是,在我的例子中,虽然我过滤掉了很大比例的行,但问题访问者(那些在 visitor_customer_hist 中有数千条记录的访问者)仍然存在问题(即他们仍然有数千条记录,导致连接爆炸)。

    不过,在其他情况下,这可能会奏效。

    【讨论】:

      【解决方案3】:

      如果两个表的每个访问者的记录数都很高,则此连接会出现问题,原因 Marcin 在 cmets 中进行了描述。因此,在这种情况下,如果可能,最好完全避免这种连接

      我最终解决此问题的方法是废弃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(如果存在)(按会话时间排序)。

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-07-22
        • 1970-01-01
        • 2010-11-14
        • 2019-06-18
        • 1970-01-01
        • 2023-03-22
        • 2023-04-09
        • 2012-12-17
        相关资源
        最近更新 更多