在此教程中您将学习如何使用 Oracle 数据库 10g SQL 中新的 SQL 联接语法填补稀疏数据中的空隙。
大约 30 分钟
本教程介绍了以下主题:
| 概述 | |
| 语法 | |
| 稀疏数据示例 | |
| 填补数据中的空隙 | |
| 总结 |
将光标置于此图标上以加载和查看本教程的所有屏幕截图。(警告:此操作会同时加载所有屏幕截图,所以网速较慢时,响应时间可能会比较长。)
注意:此外,您可以将光标放在下列步骤的单个图标上,以便仅加载和查看与该步相关的屏幕截图。您可以通过单击单个屏幕截图将其隐藏。
数据通常以稀疏形式进行存储。也就是说,如果在某一给定时间没有任何值存在,则在事实表中没有行存在。然而,如果时间维上的数据密集,则执行时间序列计算最为简单。这是因为密集数据对于每个时段所填充的行数一致,从而更易于使用带有物理偏移的分析窗口函数。请参考第 21 章:数据仓库指南 以获得更多信息。
要想克服稀疏问题,您可以使用分区外部联接填补时间序列中的空隙。此种联接通过将外部联接应用于查询中定义的各个逻辑分区,扩展了常规外部联接语法。Oracle 数据库根据 PARTITION BY 子句中指定的表达式,对查询中的行进行逻辑分区。分区外部联接的结果是逻辑分区表中各个组的外部联接与联接另一侧的表的联合。
请注意,您可以使用这种类型的联接填补任何维(而不仅仅是时间维)中的空隙。本教程将专注于时间维,因为它是最常用作比较基础的的维。
分区外部联接的语法为 PARTITION BY 加表达式列表,它扩展了 ANSI SQL JOIN 子句。此列表中的表达式指定了要应用外部联接的组。以下是通常用于分区外部联接的两种语法形式:
SELECT select_expression FROM table_reference PARTITION BY (expr [, expr ]... ) RIGHT OUTER JOIN table_reference |
SELECT select_expression
FROM table_reference
LEFT OUTER JOIN table_reference
PARTITION BY {expr [,expr ]...)
|
请注意,分区外部联接不支持 FULL OUTER JOIN(完全外部联接)。请参阅 Oracle 数据库 10g SQL 参考 以获取有关语法和限制的进一步信息。
以下示例是一个典型的稀疏维情形,其中将计算产品“Bounce”2000 年和 2001 年中第 2030 周的周销售额和从年初至今的销售额:
SELECT |
PRODUCT_NAME YEAR WEEK SALES 18 rows selected. |
本例中,如果数据密集,预期应有 22 行数据(2 年,每年 11 周)。但是,您只会看到 18 行数据,因为其中缺失了 2000 年第 25 周和第 26 周以及 2001 年第 26 周和第 28 周的数据。
时间序列中的空隙会使诸如按年度进行比较之类的计算难以进行。在没有空隙时,可以通过使用分析函数 LEAD() 和 LAG(),从一行到固定间隔以外的另一行进行参考来比较数据。例如,如果您收到了月度数据,并希望参考 12 个月以前的数据,那么,访问当前值 12 行之前的数据很方便。如果每个时段(或其他任何用作分隔的维)的行数不一致,使用 LEAD() 和 LAG() 函数则会不可靠。
如何用分区外部连接填补前面示例中的空隙?
可以用密集的时间数据集对我们查询中的稀疏数据进行一次分区外部联接。在以下所示的查询中,原始查询以 v 为别名,从时间表检索到的数据以 t 为别名。现在您将看到 22 行数据,因为序列中没有空隙。所添加的 4 行中,每行的销售值都为 0 — 这些值通过使用 NVL() 函数设置为了 0。
SELECT Product_Name, t.Year, t.Week, NVL(Sales,0) dense_sales |
PRODUCT_NAME YEAR WEEK DENSE_SALES |
请注意,在以上查询中,把用于第 20-30 周间的 WHERE 条件放置在了时间维的內联视图中。此步骤减少了由外部联接处理的行数,这节约了处理时间。
如何将此技术与分析 SQL 函数相结合以获得所需各周的累积销售额?
| 1. |
从终端窗口中,执行以下命令: cd /home/oracle/wkdir sqlplus sh/sh @fg fg.sql 脚本包含以下内容: SELECT Product_Name, t.Year, t.Week, Sales, Weekly_ytd_sales
FROM:
(SELECT
SUBSTR(p.Prod_Name,1,15) Product_Name,
t.Calendar_Year Year,
t.Calendar_Week_Number Week,
NVL(SUM(Amount_Sold),0) Sales,
SUM(SUM(Amount_Sold)) OVER
(PARTITION BY p.Prod_Name, t.Calendar_Year
ORDER BY t.Calendar_Week_Number) Weekly_ytd_sales
FROM Sales s, Times t, Products p
WHERE s.Time_id = t.Time_id AND
s.Prod_id = p.Prod_id AND
p.Prod_name IN ('Bounce') AND
t.Calendar_Year IN (2000,2001) AND
t.Calendar_Week_Number BETWEEN 20 AND 30
GROUP BY p.Prod_Name, t.Calendar_Year, t.Calendar_Week_Number
) v
PARTITION BY (v.Product_Name)
RIGHT OUTER JOIN
(SELECT DISTINCT
Calendar_Week_Number Week,
Calendar_Year Year
FROM Times
WHERE Calendar_Year in (2000, 2001)
) t
ON (v.week = t.week AND v.Year = t.Year)
WHERE t.Week BETWEEN 20 AND 30
ORDER BY 1, 2, 3;
在这个查询中,在每周销售额的旁边,计算出了每周的年初至今销售额。由分区外部连接插入以使时间序列密集的 NULL 值按通常方法进行了处理:SUM 函数将其都视为 0。 |
在某些查询中,分区外部连接将返回带有 NULL 值的行,但您可能希望这些行保存序列中最近的非 NULL 值。即,您是否希望在列中进行向上扫描时,用看到的第一个非 NULL 值替换 NULL。
跟踪不同产品的可用单位数量的库存表,是需要此类输出的一种常见情况。库存表是稀疏的:与销售表相同,它们只需在有事件发生时针对产品存储行。对于销售表来说,事件是销售;对于库存表来说,事件是可用产品数量的变化。如果对于库存使用密集的时间维,则表示用户希望看到每一天的数量值。要输出的值是最近的非 NULL 值。请注意,这与之前的累积销售示例不同。在那个查询中,累积总和计算将 NULL 都视为 0,因此显示了正确值。那种方法不适用于库存表及与其类似的表,因为放置在带有 NULL 的行中的值不是总和。
以下示例中的库存表带有分区外部联接。它将用最近的非 NULL 值替换 NULL。
在下一个任务中,您将使用外部联接特性跨时段比较值。特别是,您将按周计算年与年之间的销售比较。查询将在同一行中显示针对每种产品返回的其 2001 年和 2000 年每周年初至今的销售额。
| 1. |
要提高查询的可读性并专注于分区外部联接,使用一个 WITH 子句开始查询。执行下面的 SQL*Plus 脚本: @pp pp.sql 脚本包含以下内容: WITH v AS
(SELECT
p.Prod_Name Product_Name,
t.Calendar_Year Year,
t.Calendar_Week_Number Week,
SUM(Amount_Sold) Sales
FROM Sales s, Times t, Products p
WHERE s.Time_id = t.Time_id AND
s.Prod_id = p.Prod_id AND
p.Prod_name in ('Y Box') AND
t.Calendar_Year in (2000,2001) AND
t.Calendar_Week_Number BETWEEN 30 AND 40
GROUP BY p.Prod_Name, t.Calendar_Year, t.Calendar_Week_Number
)
SELECT substr(Product_Name,1,12) Prod,
Year,
Week,
Sales,
Weekly_ytd_sales,
Weekly_ytd_sales_prior_year
FROM:
(SELECT --Start of year_over_year sales
Product_Name, Year, Week, Sales, Weekly_ytd_sales,
LAG(Weekly_ytd_sales, 1) OVER
(PARTITION BY Product_Name, Week ORDER BY Year)
Weekly_ytd_sales_prior_year
FROM:
(SELECT --Start of dense_sales
v.Product_Name Product_Name,
t.Year Year,
t.Week Week,
NVL(v.Sales,0) Sales,
SUM(NVL(v.Sales,0)) OVER
(PARTITION BY v.Product_Name, t.Year
ORDER BY t.week) weekly_ytd_sales
FROM v
PARTITION BY (v.Product_Name)
RIGHT OUTER JOIN
(SELECT DISTINCT
Calendar_Week_Number Week,
Calendar_Year Year
FROM Times
WHERE Calendar_Year IN (2000, 2001)
) t
ON (v.week = t.week AND v.Year = t.Year)
) dense_sales
) year_over_year_sales
WHERE Year = 2001 AND
Week BETWEEN 30 AND 40
ORDER BY 1, 2, 3;
在內联视图 DENSE_SALES 的 FROM 子句中,使用聚合视图 v 和时间视图 t 的分区外部联接沿时间维填补销售数据中的空隙。然后,由分析函数 SUM ...OVER 来处理分区外部联接的输出,以计算每周年初至今的销售额(“weekly_ytd_sales”列)。因此,视图 DENSE_SALES 将计算出每周的年初至今销售数据(包括在聚合视图 s 中缺失的数据)。 然后,内联视图 YEAR_OVER_YEAR_SALES 使用 LAG 函数计算一年前的每周年初至今销售额。标记为“weekly_ytd_sales_prior_year”的 LAG 函数将指定一个 PARTITION BY 子句,以将 2000 年和 2001 年同一周的行配对到单个分区中。并将偏移量 1 传递给 LAG 函数以获得前一年的每周年初至今销售额。 最外面的查询块用 yr = 2001 条件从 YEAR_OVER_YEAR_SALES 选择数据,因而查询返回每种产品在 2001 年和 2000 年指定周中的每周年初至今销售额。 |
尽管前面的示例演示了按单个时间标准创建比较的方法,但是,在单个查询中处理多时间级更为实用。例如,您可以按年、季度、月、天等相对于前一时段进行销售额比较。
下一个任务中,将要创建一个查询,按我们时间层次的所有级别对年初至今销售额执行年与年之间的对比。
执行此任务需要若干步骤。目标是构建单一查询,以按天、周、月、季度、年各级进行比较。您将用到物化视图 MV_PROD_TIME,该视图具有一个跨 TIMES 和 PRODUCTS 聚合的销售额层次立方。在这个物化视图之上将创建另一个视图。此外,还需创建一个时间维视图以用作立方的边。此时间边将成为到物化视图中稀疏数据的分区外部联接。
关于层次立方的更多信息,请参阅 数据仓库参考指南 中“在数据仓库中适用于聚合的 SQL”一章。
| 1. |
创建物化视图。请注意,查询仅限于两种产品以便缩短处理时间。执行下面的 SQL*Plus 脚本: @cm1 cm1.sql 脚本包含以下内容: CREATE MATERIALIZED VIEW mv_prod_time
REFRESH COMPLETE ON DEMAND
AS
SELECT
(CASE
WHEN ((GROUPING(calendar_year)=0 )
AND (GROUPING(calendar_quarter_desc)=1 ))
THEN (TO_CHAR(calendar_year) || '_0')
WHEN ((GROUPING(calendar_quarter_desc)=0 )
AND (GROUPING(calendar_month_desc)=1 ))
THEN (TO_CHAR(calendar_quarter_desc) || '_1')
WHEN ((GROUPING(calendar_month_desc)=0 )
AND (GROUPING(t.time_id)=1 ))
THEN (TO_CHAR(calendar_month_desc) || '_2')
ELSE (TO_CHAR(t.time_id) || '_3') END) Hierarchical_Time,
calendar_year year,
calendar_quarter_desc quarter,
calendar_month_desc month,
t.time_id day,
prod_category cat,
prod_subcategory subcat,
p.prod_id prod,
GROUPING_ID(prod_category, prod_subcategory, p.prod_id,
calendar_year, calendar_quarter_desc,
calendar_month_desc,t.time_id) gid,
GROUPING_ID(prod_category, prod_subcategory, p.prod_id) gid_p,
GROUPING_ID(calendar_year, calendar_quarter_desc,
calendar_month_desc, t.time_id) gid_t,
SUM(amount_sold) s_sold,
COUNT(amount_sold) c_sold,
COUNT(*) cnt
FROM SALES s, TIMES t, PRODUCTS p
WHERE s.time_id = t.time_id AND
p.prod_name in ('Bounce', 'Y Box') AND
s.prod_id = p.prod_id
GROUP BY
ROLLUP(calendar_year, calendar_quarter_desc,
calendar_month_desc, t.time_id),
ROLLUP(prod_category, prod_subcategory, p.prod_id);
由于将物化视图限制在两种产品上,因此只有 2200 多行。请注意,Hierarchical_Time 列包含时间层次的所有级别时间的字符串表示形式。用于 Hierarchical_Time 列的 CASE 表达式将标记 (_0, _1, ...) 追加到每个日期字符串以表示值的时间级。_0 表示年级,_1 表示季度,_2 是月,_3 是日。请注意,GROUP BY 子句是一个连接而成的 ROLLUP,指定时间和产品维的卷积层次。GROUP BY 子句确定层次立方内容。 |
| 2. |
创建与物化视图 MV_PROD_TIME 具有相同定义的视图 CUBE_PROD_TIME。执行下面的 SQL*Plus 脚本: @cv1 cv1.sql 脚本包含以下内容: CREATE OR REPLACE VIEW cube_prod_time
AS
SELECT
(CASE
WHEN ((GROUPING(calendar_year)=0 )
AND (GROUPING(calendar_quarter_desc)=1 ))
THEN (TO_CHAR(calendar_year) || '_0')
WHEN ((GROUPING(calendar_quarter_desc)=0 )
AND (GROUPING(calendar_month_desc)=1 ))
THEN (TO_CHAR(calendar_quarter_desc) || '_1')
WHEN ((GROUPING(calendar_month_desc)=0 )
AND (GROUPING(t.time_id)=1 ))
THEN (TO_CHAR(calendar_month_desc) || '_2')
ELSE (TO_CHAR(t.time_id) || '_3') END) Hierarchical_Time,
calendar_year year,
calendar_quarter_desc quarter,
calendar_month_desc month,
t.time_id day,
prod_category cat,
prod_subcategory subcat,
p.prod_id prod,
GROUPING_ID(prod_category, prod_subcategory, p.prod_id,
calendar_year, calendar_quarter_desc, calendar_month_desc,
t.time_id) gid,
GROUPING_ID(prod_category, prod_subcategory, p.prod_id) gid_p,
GROUPING_ID(calendar_year, calendar_quarter_desc,
calendar_month_desc, t.time_id) gid_t,
SUM(amount_sold) s_sold,
COUNT(amount_sold) c_sold,
COUNT(*) cnt
FROM SALES s, TIMES t, PRODUCTS p
WHERE s.time_id = t.time_id AND
p.prod_name IN ('Bounce', 'Y Box') AND
s.prod_id = p.prod_id
GROUP BY
ROLLUP(calendar_year, calendar_quarter_desc,
calendar_month_desc, t.time_id),
ROLLUP(prod_category, prod_subcategory, p.prod_id);
|
| 3. |
创建 EDGE_TIME 视图,它是完整的日期值集。EDGE_TIME 是用分区外部联接填补时间空隙所用的源。EDGE_TIME 中的 HIERARCHICAL_TIME 列将用在与 CUBE_PROD_TIME 视图的 HIERARCHICAL_TIME 列的分区连接中。执行下面的 SQL*Plus 脚本: @cv2 cv2.sql 脚本包含以下内容: CREATE OR REPLACE VIEW edge_time AS SELECT (CASE WHEN ((GROUPING(calendar_year)=0 ) AND (GROUPING(calendar_quarter_desc)=1 )) THEN (TO_CHAR(calendar_year) || '_0') WHEN ((GROUPING(calendar_quarter_desc)=0 ) AND (GROUPING(calendar_month_desc)=1 )) THEN (TO_CHAR(calendar_quarter_desc) || '_1') WHEN ((GROUPING(calendar_month_desc)=0 ) AND (GROUPING(time_id)=1 )) THEN (TO_CHAR(calendar_month_desc) || '_2') ELSE (TO_CHAR(time_id) || '_3') END) Hierarchical_Time, calendar_year yr, calendar_quarter_number qtr_num, calendar_quarter_desc qtr, calendar_month_number mon_num, calendar_month_desc mon, time_id - TRUNC(time_id, 'YEAR') + 1 day_num, time_id day, GROUPING_ID(calendar_year, calendar_quarter_desc, calendar_month_desc, time_id) gid_t FROM TIMES GROUP BY ROLLUP (calendar_year, (calendar_quarter_desc, calendar_quarter_number), (calendar_month_desc, calendar_month_number), time_id); |
| 4. |
现在,具备了进行比较查询所需的元素。可以按任一时间级进行时段与时段之间的比较计算。它需要将分析函数沿时间维应用到带有密集数据的层次立方。对于每个时间级,可执行的计算包括:
以下示例将执行上述四种计算。它使用 CUBE_PROD_TIME 和 EDGE_TIME 视图的分区外部联接创建名为 DENSE_CUBE_PROD_TIME 的密集数据的内嵌视图。然后,按与之前单一级别示例中相同的方法,查询并使用 LAG 函数。外部 WHERE 子句指定了 3 个级别的时间:2001 年 8 月中的某些天,整个 8 月,2001 年整个第三季度。请注意,结果的最后两行将包含月级和季度级总计。 执行下面的 SQL*Plus 脚本: @mt mt.sql 脚本包含以下内容: SELECT
substr(prod,1,4) prod, substr(Hierarchical_Time,1,12) ht,
sales,
sales_prior_period,
sales - sales_prior_period variance_prior_period,
sales_same_period_prior_year,
sales - sales_same_period_prior_year variance_same_period_p_year
FROM:
(SELECT cat, subcat, prod, gid_p, gid_t, Hierarchical_Time,
yr, qtr, mon, day, sales,
LAG(sales, 1) OVER (PARTITION BY gid_p, cat, subcat, prod,
gid_t ORDER BY yr, qtr, mon, day)
sales_prior_period,
LAG(sales, 1) OVER (PARTITION BY gid_p, cat, subcat, prod,
gid_t, qtr_num, mon_num, day_num ORDER BY yr)
sales_same_period_prior_year
FROM:
(SELECT c.gid, c.cat, c.subcat, c.prod, c.gid_p,
t.gid_t, t.yr, t.qtr, t.qtr_num, t.mon, t.mon_num,
t.day, t.day_num, t.Hierarchical_Time, NVL(s_sold,0) sales
FROM cube_prod_time c
PARTITION BY (gid_p, cat, subcat, prod)
RIGHT OUTER JOIN edge_time t
ON ( c.gid_t = t.gid_t AND c.Hierarchical_Time = t.Hierarchical_Time)
) dense_cube_prod_time
) -- side by side current,prior and prior year sales
WHERE prod IN (139) AND gid_p=0 AND -- 1 product and product level data
( (mon IN ('2001-08' ) AND gid_t IN (0, 1) ) OR -- day and month data
( qtr IN ('2001-03' ) AND gid_t IN (3) ) ) -- quarter level data
ORDER BY day;
|
对于很多 OLAP 任务而言,在维中定义自定义成员是有助益的。例如,您可以定义特定的时段以用于分析。可以使用分区外部联接将成员临时添加到维中。请注意,Oracle 数据库 10g 中引入的新 SQL MODEL 子句适用于创建涉及维中新成员的更复杂方案。关于此主题的详细信息,请参阅数据仓库参考指南中的教程“用于建模的 SQL”。
在本练习中,将为 TIME 维定义一个新成员。在 TIME 维中创建第 13 个月级成员。该第 13 个月被定义为 2001 年每季度头一个月每种产品销售额的总和。将使用前面示例中创建的视图和表构建这个解决方案。
| 1. |
创建一个有新成员添加到相应维的视图。该视图使用 UNION ALL 操作添加新成员。要使用自定义成员查询,使用 CASE 表达式和分区外部联接。执行下面的 SQL*Plus 脚本: @cv3 cv3.sql 脚本包含以下内容: CREATE OR REPLACE VIEW time_c AS (SELECT * FROM edge_time UNION ALL SELECT '2001-13_2', 2001, 5, '2001-05', 13, '2001-13', null, null, 8 -- 在显示的语句中,通过执行前例中定义的 EDGE_TIME 视图的 UNION ALL 和用户定义的第 13 个月,对 TIME_C 视图进行了定义。UNION ALL 通过从 DUAL 表进行 SELECT 指定用于第 13 个月的属性。请注意,分组 id(列 gid_t)设定为 8,季度数设定为 5。
|
| 2. |
查询的内联视图(如下所示)执行 CUBE_PROD_TIME 与 TIME_C 的分区外部联接。该步骤将在每个产品汇总级为第 13 个月创建销售数据。在主查询中,分析函数 SUM 与 CASE 表达式一起使用以计算第 13 个月的数据,即每个季度首月的销售总和。执行下面的 SQL*Plus 脚本: @cv4 cv4.sql 脚本包含以下内容: SELECT * from ( SELECT substr(cat,1,12) cat, substr(subcat,1,12) subcat, substr(prod,1,9) prod, mon, mon_num, SUM(CASE WHEN mon_num IN (1, 4, 7, 10) THEN s_sold ELSE NULL END) OVER (PARTITION BY gid_p, prod, subcat, cat, yr) sales_month_13 FROM: (SELECT c.gid, c.prod, c.subcat, c.cat, gid_p, t.gid_t , t.day, t.mon, t.mon_num, t.qtr, t.yr, NVL(s_sold,0) s_sold FROM cube_prod_time c PARTITION BY (gid_p, prod, subcat, cat) RIGHT OUTER JOIN time_c t ON (c.gid_t = t.gid_t AND c.Hierarchical_Time = t.Hierarchical_Time) ) ) WHERE mon_num=13; 用于生成这些结果的 SUM 函数有一个 CASE 语句,用来将日期限制为每年的 1、4、7、10 月。由于数据集很小,只有两个产品,结果的卷积值是较低级别聚合的必要重复。要获得更实际的卷积值集,您可以在底层物化视图中包含更多“Game Console”和“Y Box Games”子类别中的产品。 |
在本教程中,您学习了如何执行下列任务:
| 使用分析 SQL 函数填补数据中的空隙 | ||
| 用最近的非 NULL 值替换 NULL | ||
| 执行时段与时段的比较 | ||
| 在维中创建自定义成员 | ||