复制记录以填补日期之间的空白
Posted
技术标签:
【中文标题】复制记录以填补日期之间的空白【英文标题】:Duplicating records to fill gap between dates 【发布时间】:2012-04-20 23:22:24 【问题描述】:我需要做一些非常奇怪的事情,那就是创建虚假记录在视图中以填补产品价格发布日期之间的空白。
实际上,我的场景比这要复杂一些,但我已经简化为产品/日期/价格。
假设我们有这张桌子:
create table PRICES_TEST
(
PRICE_DATE date not null,
PRODUCT varchar2(13) not null,
PRICE number
);
alter table PRICES_TEST
add constraint PRICES_TEST_PK
primary key (PRICE_DATE, PRODUCT);
有了这些记录:
insert into PRICES_TEST values (date'2012-04-15', 'Screw Driver', 13);
insert into PRICES_TEST values (date'2012-04-18', 'Screw Driver', 15);
insert into PRICES_TEST values (date'2012-04-13', 'Hammer', 10);
insert into PRICES_TEST values (date'2012-04-16', 'Hammer', 15);
insert into PRICES_TEST values (date'2012-04-19', 'Hammer', 17);
选择记录将返回给我:
PRICE_DATE PRODUCT PRICE
------------------------- ------------- ----------------------
13-Apr-2012 00:00:00 Hammer 10
16-Apr-2012 00:00:00 Hammer 15
19-Apr-2012 00:00:00 Hammer 17
15-Apr-2012 00:00:00 Screw Driver 13
18-Apr-2012 00:00:00 Screw Driver 15
假设今天是 2012 年 4 月 21 日,我需要 一个视图,该视图将每天重复每个价格,直到发布新价格。像这样:
PRICE_DATE PRODUCT PRICE
------------------------- ------------- ----------------------
13-Apr-2012 00:00:00 Hammer 10
14-Apr-2012 00:00:00 Hammer 10
15-Apr-2012 00:00:00 Hammer 10
16-Apr-2012 00:00:00 Hammer 15
17-Apr-2012 00:00:00 Hammer 15
18-Apr-2012 00:00:00 Hammer 15
19-Apr-2012 00:00:00 Hammer 17
20-Apr-2012 00:00:00 Hammer 17
21-Apr-2012 00:00:00 Hammer 17
15-Apr-2012 00:00:00 Screw Driver 13
16-Apr-2012 00:00:00 Screw Driver 13
17-Apr-2012 00:00:00 Screw Driver 13
18-Apr-2012 00:00:00 Screw Driver 15
19-Apr-2012 00:00:00 Screw Driver 15
20-Apr-2012 00:00:00 Screw Driver 15
21-Apr-2012 00:00:00 Screw Driver 15
任何想法如何做到这一点?我不能真的使用其他辅助表、触发器或 PL/SQL 编程,我真的需要使用 视图。
我认为这可以使用 oracle 分析来完成,但我对此并不熟悉。我试图阅读这个http://www.club-oracle.com/articles/analytic-functions-i-introduction-164/,但我根本没有明白。
【问题讨论】:
NVM,我现在明白了 :) 可以创造性地使用Dual
表来生成数据。
我可以假设 oracle 分析有它自己的日期维度表来执行这样的功能。您可以创建自己的日期维度表吗?
这是一篇关于在 tsql 中创建动态日历视图的有趣文章(是的,您需要 oracle,但也许可以更改):sqlserverpedia.com/blog/sql-server-bloggers/…
【参考方案1】:
您可以使用CONNECT BY LEVEL
语法创建行生成器语句,与表中的不同产品交叉连接,然后将其外部连接到您的价格表。最后一点是使用LAST_VALUE
函数和IGNORE NULLS
重复价格,直到遇到新值,并且由于您想要查看,使用CREATE VIEW
语句:
create view dense_prices_test as
select
dp.price_date
, dp.product
, last_value(pt.price ignore nulls) over (order by dp.product, dp.price_date) price
from (
-- Cross join with the distinct product set in prices_test
select d.price_date, p.product
from (
-- Row generator to list all dates from first date in prices_test to today
with dates as (select min(price_date) beg_date, sysdate end_date from prices_test)
select dates.beg_date + level - 1 price_date
from dual
cross join dates
connect by level <= dates.end_date - dates.beg_date + 1
) d
cross join (select distinct product from prices_test) p
) dp
left outer join prices_test pt on pt.price_date = dp.price_date and pt.product = dp.product;
【讨论】:
伙计,你是大师!我从未听说过分析函数中的“with”、“connect by”或“ignore nulls”。非常感谢你。它与我更复杂的场景完美配合。 @LeoHolanda 太棒了!祝你好运。 嗨@Wolf,虽然你的答案是让我比其他人学到更多的答案,但 mellamokb 的答案是真正产生预期结果的答案,因此,他应该得到“接受的答案”标志。非常感谢所有帮助我的人。【参考方案2】:我认为我有一个解决方案,使用增量方法来获得 CTE 的最终结果:
with mindate as
(
select min(price_date) as mindate from PRICES_TEST
)
,dates as
(
select mindate.mindate + row_number() over (order by 1) - 1 as thedate from mindate,
dual d connect by level <= floor(SYSDATE - mindate.mindate) + 1
)
,productdates as
(
select p.product, d.thedate
from (select distinct product from PRICES_TEST) p, dates d
)
,ranges as
(
select
pd.product,
pd.thedate,
(select max(PRICE_DATE) from PRICES_TEST p2
where p2.product = pd.product and p2.PRICE_DATE <= pd.thedate) as mindate
from productdates pd
)
select
r.thedate,
r.product,
p.price
from ranges r
inner join PRICES_TEST p on r.mindate = p.price_date and r.product = p.product
order by r.product, r.thedate
mindate
检索数据集中可能的最早日期
dates
生成从最早可能日期到今天的日期日历。
productdates
交叉连接所有可能的产品和所有可能的日期
ranges
确定每个日期适用的价格日期
最终查询链接哪个价格日期适用于实际价格,并通过inner join
条件过滤掉没有相关价格日期的日期
演示:http://www.sqlfiddle.com/#!4/e528f/126
【讨论】:
我刚刚删除了第二个“with”选择中对双表和地板舍入的引用。它们并不是真正需要的。除此之外,它看起来很棒!我的真实场景在 PK 中包含另外 2 个列(例如产品、品牌、型号)和其他几个信息列(例如价格、净重、注释),并且在适当的更改下它仍然可以完美运行。非常感谢。【参考方案3】:我对 Wolf 的出色回答做了一些修改。
我用connect by
中的常规子查询替换了子查询分解 (WITH
)。这使代码更简单一些。 (尽管这种类型的代码一开始看起来很奇怪,所以这里可能不会有很大的收获。)
最重要的是,我使用了分区外连接,而不是交叉连接和外连接。分区外连接也有点奇怪,但它们正是针对这种情况的。这使代码更简单,并且应该会提高性能。
select
price_dates.price_date
,product
,last_value(price ignore nulls) over (order by product, price_dates.price_date) price
from
(
select trunc(sysdate) - level + 1 price_date
from dual
connect by level <= trunc(sysdate) -
(select min(trunc(price_date)) from prices_test) + 1
) price_dates
left outer join prices_test
partition by (prices_test.product)
on price_dates.price_date = prices_test.price_date;
【讨论】:
这是一个不错的改进。 WITH 子句是我拥有的其他一些 SQL 的产物,但我一直忘记分区外连接。干得好。【参考方案4】:我刚刚意识到 @Wolf 和 @jonearles 的改进不会返回我需要的确切结果,因为列出所有日期的行生成器不会按产品生成范围。如果产品 A 的第一个价格晚于产品 B 的任何价格,则产品 A 的第一个上市日期仍然必须相同。但他们确实帮助我进一步工作并获得了预期的结果:
我从更改 @wolf 的日期范围选择器开始:
select min(price_date) beg_date, sysdate end_date from prices_test
到这里:
select min(PRICE_DATE) START_DATE, sysdate as END_DATE, PRODUCT
from PRICES_TEST group by sysdate, PRODUCT
但是,不知何故,每个产品的行数在每个级别都呈指数级重复增长。我只是在外部查询中添加了一个 distinct。最后的选择是这样的:
select
DP.PRICE_DATE,
DP.PRODUCT,
LAST_VALUE(PT.PRICE ignore nulls) over (order by DP.PRODUCT, DP.PRICE_DATE) PRICE
from (
select distinct START_DATE + DAYS as PRICE_DATE, PRODUCT
from
(
-- Row generator to list all dates from first date of each product to today
with DATES as (select min(PRICE_DATE) START_DATE, sysdate as END_DATE, PRODUCT from PRICES_TEST group by sysdate, PRODUCT)
select START_DATE, level - 1 as DAYS, PRODUCT
from DATES
connect by level < END_DATE - START_DATE + 1
order by 3, 2
) d order by 2, 1
) DP
left outer join prices_test pt on pt.price_date = dp.price_date and pt.product = dp.product;
@Mellamokb 解决方案实际上是我真正需要的,而且肯定比我的菜鸟解决方案更好。
感谢大家不仅在这方面为我提供帮助,而且还向我展示了“with”和“connect by”等功能。
【讨论】:
以上是关于复制记录以填补日期之间的空白的主要内容,如果未能解决你的问题,请参考以下文章
复制记录组以填补 Google BigQuery 中的多个日期空白