在有效范围内旋转/非规范化

Posted

技术标签:

【中文标题】在有效范围内旋转/非规范化【英文标题】:Pivot/Denormalize Over Effective Ranges 【发布时间】:2021-03-05 19:22:00 【问题描述】:

我希望将事务数据集转入 SCD2,以便捕获每个组合在转点粒度上的有效间隔。

Snowflake 是我使用的实际 DBMS,但也标记了 Oracle,因为它们的方言几乎相同。不过,我可能会找到为任何 DBMS 提供的解决方案。

我有工作的 sql,但它源于反复试验,我觉得必须有一种更优雅的方式我错过了,因为它非常丑陋且计算成本很高。

(注意:输入数据中的第二条记录“过期”了第一条记录。可以假设感兴趣的每一天都会作为 add_dts 至少出现一次。) (在最后添加为图像,直到我弄清楚为什么标记不起作用)

输入:

Original_Grain Pivot_Grain Pivot_Column Pivot_Attribute ADD_TS
OG-1 PG-1 First_Col A 2020-01-01
OG-1 PG-1 First_Col B 2020-01-02
OG-2 PG-1 Second_Col A 2020-01-01
OG-3 PG-1 Third_Col C 2020-01-02
OG-3 PG-1 Third_Col B 2020-01-03

输出:

Pivot_Grain First_Col Second_Col Third_Col From_Dt To_Dt
PG-1 A A NULL 2020-01-01 2020-01-02
PG-1 B A C 2020-01-02 2020-01-03
PG-1 B A B 2020-01-03 9999-01-01
WITH INPUT AS 
  ( SELECT 'OG-1' AS Original_Grain,
           'PG-1' AS Pivot_Grain,
           'First_Col' AS Pivot_Column,
           'A' AS Pivot_Attribute,
           TO_DATE('2020-01-01','YYYY-MM-DD') AS Add_Dts
      FROM dual
     UNION
   SELECT 'OG-1' AS Original_Grain,
           'PG-1' AS Pivot_Grain,
           'First_Col' AS Pivot_Column,
           'B' AS Pivot_Attribute,
           TO_DATE('2020-01-02','YYYY-MM-DD')
      FROM dual
     UNION
   SELECT 'OG-2' AS Original_Grain,
           'PG-1' AS Pivot_Grain,
           'Second_Col' AS Pivot_Column,
           'A' AS Pivot_Attribute,
           TO_DATE('2020-01-01','YYYY-MM-DD')
      FROM dual
     UNION
   SELECT 'OG-3' AS Original_Grain,
           'PG-1' AS Pivot_Grain,
           'Third_Col' AS Pivot_Column,
           'C' AS Pivot_Attribute,
           TO_DATE('2020-01-02','YYYY-MM-DD')
      FROM dual
     UNION
   SELECT 'OG-3' AS Original_Grain,
           'PG-1' AS Pivot_Grain,
           'Third_Col' AS Pivot_Column,
           'B' AS Pivot_Attribute,
           TO_DATE('2020-01-03','YYYY-MM-DD')
      FROM dual     
  ),
  GET_NORMALIZED_RANGES AS 
  ( SELECT I.*,
           COALESCE(
             LEAD(Add_Dts) OVER (
             PARTITION BY I.Original_Grain
                 ORDER BY I.Add_Dts), TO_DATE('9000-01-01')
             ) AS Next_Add_Dts
      FROM INPUT I
  ),
  GET_DISTINCT_ADD_DATES AS 
  ( SELECT DISTINCT Add_Dts AS Driving_Date
      FROM Input  
  ),
  NORMALIZED_EFFECTIVE_AT_EACH_POINT AS 
  ( SELECT GNR.*,
           GDAD.Driving_Date
      FROM GET_NORMALIZED_RANGES GNR
     INNER
      JOIN GET_DISTINCT_ADD_DATES GDAD
        ON GDAD.driving_date >= GNR.add_dts
       AND GDAD.driving_Date < GNR.next_add_dts
  ),
  PIVOT_EACH_POINT AS 
  ( SELECT DISTINCT
           Pivot_Grain,
           Driving_Date,
           MAX("'First_Col'") OVER ( PARTITION BY Pivot_Grain, Driving_Date) AS First_Col,
           MAX("'Second_Col'") OVER ( PARTITION BY Pivot_Grain, Driving_Date) AS Second_Col,
           MAX("'Third_Col'") OVER ( PARTITION BY Pivot_Grain, Driving_Date) AS Third_Col
      FROM NORMALIZED_EFFECTIVE_AT_EACH_POINT NEP
     PIVOT (MAX(Pivot_Attribute) FOR PIVOT_COLUMN IN ('First_Col','Second_Col','Third_Col'))
  )
  SELECT Pivot_Grain,
         Driving_Date AS From_Dt,
         COALESCE(LEAD(Driving_Date) OVER ( PARTITION BY pivot_grain ORDER BY Driving_Date),TO_DATE('9999-01-01')) AS To_Dt,
         First_Col,
         Second_Col,
         Third_Col
    FROM PIVOT_EACH_POINT

【问题讨论】:

表格标记在预览中有效,但在实际帖子中无效 我们通过对“跟踪”的列进行哈希来构建我们的 SCD1 和 2(又名 SCD6)表,因此每次我们拉取数据时,如果哈希相同,则剩余时间在遥远的未来,否则改变(现有的)并插入新行。我很确定这都是通过同一个 MERGE 指令完成的。 @SimeonPilgrim 是的,我熟悉这种模式。问题的症结在于通过跨枢轴粒度的组合来处理日期间隔。 通常降价就像在不同的格式块之间存在空白行。 【参考方案1】:

所以输入可以用 VALUES 操作符写入,列名放入 CTE 定义中,占用更少的空间。

WITH input(original_grain, pivot_grain, pivot_column, pivot_attribute, add_dts) AS ( 
    SELECT * FROM VALUES 
        ('OG-1', 'PG-1', 'First_Col', 'A', '2020-01-01'::date),
        ('OG-1', 'PG-1', 'First_Col', 'B', '2020-01-02'::date),
        ('OG-2', 'PG-1', 'Second_Col', 'A', '2020-01-01'::date),
        ('OG-3', 'PG-1', 'Third_Col', 'C', '2020-01-02'::date),
        ('OG-3', 'PG-1', 'Third_Col', 'B', '2020-01-03'::date) 
)
  

可以通过使用默认值来简化 LEAD,这是一个隐含的 COALESCE,但有时如果您的数据中有这种类型的空白,IGNORE NULLS OVER 是一个很棒的工具。

, get_normalized_ranges AS ( 
    SELECT 
        *
        ,LEAD(add_dts,1,'9000-01-01'::date) OVER (PARTITION BY original_grain ORDER BY add_dts) AS next_add_dts
    FROM input
)
  

get_distinct_add_dates 看起来不错。

, get_distinct_add_dates AS ( 
    SELECT DISTINCT add_dts AS driving_date
    FROM input  
)

取决于您的数据normalized_effective_at_each_point 会名副其实,并在每个时间/日期点为您提供一个值,这将过度分割不相关的值(我假设 pivot_grain 是一些全局事物 id,它是不同的数据因此这个输入支持它)

        ('OG-1', 'PG-1', 'First_Col', 'A', '2020-01-01'::date),
        ('OG-1', 'PG-1', 'First_Col', 'B', '2020-01-03'::date),
        ('OG-2', 'PG-1', 'Second_Col','A', '2020-01-01'::date),
        ('OG-3', 'PG-1', 'Third_Col', 'C', '2020-01-03'::date),
        ('OG-3', 'PG-1', 'Third_Col', 'B', '2020-01-05'::date),
        ('OG-4', 'PG-2', 'First_Col', 'D', '2020-02-02'::date),
        ('OG-4', 'PG-2', 'First_Col', 'E', '2020-02-04'::date),
        ('OG-5', 'PG-2', 'Second_Col','D', '2020-02-02'::date),
        ('OG-6', 'PG-2', 'Third_Col', 'F', '2020-02-04'::date),
        ('OG-6', 'PG-2', 'Third_Col', 'D', '2020-02-06'::date) 
        

此时get_distinct_add_dates 应该变成:

, get_distinct_add_dates AS ( 
    SELECT DISTINCT pivot_grain, add_dts AS driving_date
    FROM input  
)
        

INNER JOIN 是一个 JOIN,所以我们可以跳过不需要的 INNER

, normalized_effective_at_each_point AS ( 
    SELECT gnr.*,
        gdad.driving_date
    FROM get_normalized_ranges AS gnr
    JOIN get_distinct_add_dates AS gdad
        ON gnr.pivot_grain = gdad.pivot_grain 
        AND gdad.driving_date >= gnr.add_dts
        AND gdad.driving_date < gnr.next_add_dts
  ),
  

真的pivot_each_point 是一个三路 JOIN,或者可以写一个 GROUP BY,这是 DISTINCT 真正为我们做的,因此 PIVOT 消失了。

  , pivot_each_point AS (
    SELECT Pivot_Grain
        ,Driving_Date
        ,MAX(IFF(pivot_column='First_Col', Pivot_Attribute, NULL)) as first_col
        ,MAX(IFF(pivot_column='Second_Col', Pivot_Attribute, NULL)) as second_col
        ,MAX(IFF(pivot_column='Third_Col', Pivot_Attribute, NULL)) as third_col
    FROM normalized_effective_at_each_point
    GROUP BY 1,2
  )
  

最后,最后的潜在客户可以放弃 COALESCE 并移动到 pivot_each_point

WITH input(original_grain, pivot_grain, pivot_column, pivot_attribute, add_dts) AS ( 
    SELECT * FROM VALUES 
        ('OG-1', 'PG-1', 'First_Col', 'A', '2020-01-01'::date),
        ('OG-1', 'PG-1', 'First_Col', 'B', '2020-01-03'::date),
        ('OG-2', 'PG-1', 'Second_Col','A', '2020-01-01'::date),
        ('OG-3', 'PG-1', 'Third_Col', 'C', '2020-01-03'::date),
        ('OG-3', 'PG-1', 'Third_Col', 'B', '2020-01-05'::date),
        ('OG-4', 'PG-2', 'First_Col', 'D', '2020-02-02'::date),
        ('OG-4', 'PG-2', 'First_Col', 'E', '2020-02-04'::date),
        ('OG-5', 'PG-2', 'Second_Col','D', '2020-02-02'::date),
        ('OG-6', 'PG-2', 'Third_Col', 'F', '2020-02-04'::date),
        ('OG-6', 'PG-2', 'Third_Col', 'D', '2020-02-06'::date) 
), get_normalized_ranges AS (      
    SELECT 
        *
        ,LEAD(add_dts,1,'9000-01-01'::date) OVER (PARTITION BY original_grain ORDER BY add_dts) AS next_add_dts
    FROM input
), get_distinct_add_dates AS ( 
    SELECT DISTINCT pivot_grain, add_dts AS driving_date
    FROM input  
), normalized_effective_at_each_point AS ( 
    SELECT gnr.*,
        gdad.driving_date
    FROM get_normalized_ranges AS gnr
    JOIN get_distinct_add_dates AS gdad
        ON gnr.pivot_grain = gdad.pivot_grain 
        AND gdad.driving_date >= gnr.add_dts
        AND gdad.driving_date < gnr.next_add_dts 
)
SELECT pivot_grain
    ,driving_date
    ,LEAD(driving_date, 1, '9999-01-01'::date) OVER (PARTITION BY pivot_grain ORDER BY driving_date) AS to_dt
    ,MAX(IFF(pivot_column = 'First_Col', pivot_attribute, NULL)) AS first_col
    ,MAX(IFF(pivot_column = 'Second_Col', pivot_attribute, NULL)) AS second_col
    ,MAX(IFF(pivot_column = 'Third_Col', pivot_attribute, NULL)) AS third_col
FROM normalized_effective_at_each_point
GROUP BY pivot_grain, driving_date
ORDER BY pivot_grain, driving_date;

给出结果:

PIVOT_GRAIN  DRIVING_DATE   TO_DT       FIRST_COL   SECOND_COL  THIRD_COL
PG-1         2020-01-01     2020-01-03  A           A           null
PG-1         2020-01-03     2020-01-05  B           A           C
PG-1         2020-01-05     9999-01-01  B           A           B
PG-2         2020-02-02     2020-02-04  D           D           null
PG-2         2020-02-04     2020-02-06  E           D           F
PG-2         2020-02-06     9999-01-01  E           D           D

我不禁认为我已经将我处理数据的方式过度映射到您的 PIVOT_GRAIN 上。既然我理解了代码,我就开始尝试从第一原理再次解决这个问题,我认为前三个处理 CTE 是我会怎么做的,因此 GROUP BY 也是我会怎么做的,很多 JOIN 似乎真的恶心,进入雪花,我更喜欢这种爆炸数据,然后合并(或分组)数据,因为这一切都很好且可并行化。

【讨论】:

【参考方案2】:

不确定这是否能回答您的问题,但请参阅 https://jeffreyjacobs.wordpress.com/2021/03/03/pivoting-iiot-data-in-snowflake/

【讨论】:

谢谢,这是一篇很好的文章。这绝对与我的问题相同,但并不完全是我所希望的“你是个白痴,这样做更容易”。

以上是关于在有效范围内旋转/非规范化的主要内容,如果未能解决你的问题,请参考以下文章

如何在 javascript 中最有效地对规范化数据进行非规范化

将浮点数组规范化到一定范围内,并在 Python 中保持符号

如何使用模板来规范化 0 到 1 范围内的数字?

如果给定的数据在使用 R 或 Python 的范围(-1 到 1)内,如何以 (-3 ,-2,-1,0,1,2,3) 格式规范化数据?

四元数和归一化

变量定义和赋值(下)