计算日期之间差异的最有效方法
Most efficient way to compute difference between dates
我有一个很大的table,按日期分区,结构如下:
InTable:
Key
Date
Field1
Value
key1
09/02/2021
ABC
10
key2
09/02/2021
DEF
-4
key3
09/02/2021
GHI
9
key1
10/02/2021
ABC
30
key2
10/02/2021
DEFG
6
key4
10/02/2021
LMN
2
我想输出一个table这样结构的
OutTable:
Key
Date
Field1
Value T
Value T-1
Daily Diff
key1
10/02/2021
ABC
30
10
20
key2
10/02/2021
DEFG
6
-4
10
key3
10/02/2021
GHI
0
9
-9
key4
10/02/2021
LMN
2
0
2
我在某个日期折叠了当前值、前一天的值以及具有相同键的两者之间的差异,同时保留了任何一个日期中缺少键的所有情况(对于例如,10 月 2 日缺少 key3,9 月 2 日缺少 key4。
如果key相同,Field1通常是相同的。如果它们不同,则必须保留最近的值(如示例中的 key2)。
据我所知,可以这样做:
- 通过在 in1.key = in2.key 和 in1.day = in2.day-1
上自行加入 InTable
- 通过在两个日期之间生成一个具有并集的稀疏矩阵,然后在关键字段上执行分组
- 通过使用枢轴结构
在 Oracle 中完成这项工作最有效、最快捷的方法是什么?
非常感谢。
我认为您需要 window 函数。如果你有所有天的数据或者想要数据中前一天的数据:
select t.*,
lag(value, 1, 0) over (partition by key, field order by date) as prev_value,
(value - lag(value, 1, 0) over (partition by key, field order by date)) as diff
from t;
如果值可以跳过日期,而您确实想要以前的日期,则可以使用带有 range
window 框架的 window 函数:
select t.*,
max(value) over (partition by key, field
order by date
range between '1' day preceding and '1' day preceding
) as prev_value,
(value -
max(value) over (partition by key, field
order by date
range between '1' day preceding and '1' day preceding
)
) as diff
from t;
这会将缺失值保留为 NULL
。您可以使用 COALESCE()
为它们赋值。
如果要按天过滤,需要在外部查询中进行:
select t.*
from (select t.*,
max(value) over (partition by key, field1
order by day
range between interval '1' day preceding and interval '1' day preceding
) as prev_value,
(value -
max(value) over (partition by key, field1
order by day
range between interval '1' day preceding and interval '1' day preceding
)
) as diff
from InTable t
) t
where day = date '2021-02-10'
order by day asc;
Here 是一个 db<>fiddle.
如果要修剪分区,则需要在子查询中包含滞后行。你可以起诉:
where day in (date '2021-02-10', date '2021-02-09')
在子查询中。
这是你可以做的(代码后的解释):
alter session set nls_date_format = 'dd/mm/yyyy';
with
intable (key, date_, field1, value) as (
select 'key1', to_date('09/02/2021'), 'ABC' , 10 from dual union all
select 'key2', to_date('09/02/2021'), 'DEF' , -4 from dual union all
select 'key3', to_date('09/02/2021'), 'GHI' , 9 from dual union all
select 'key1', to_date('10/02/2021'), 'ABC' , 30 from dual union all
select 'key2', to_date('10/02/2021'), 'DEFG', 6 from dual union all
select 'key4', to_date('10/02/2021'), 'LMN' , 2 from dual
)
, ref_date (dt) as (select to_date('10/02/2021') from dual)
, prep (key, field1, new_val, old_val) as (
select i.key,
min(field1) keep (dense_rank last order by date_),
nvl(min(case i.date_ when r.dt then value end), 0),
nvl(min(case i.date_ when r.dt - 1 then value end), 0)
from intable i cross join ref_date r
where date_ in (r.dt - 1, r.dt)
group by i.key, r.dt
)
select p.key, r.dt as date_, p.field1,
p.new_val, p.old_val, p.new_val - p.old_val as daily_diff
from prep p cross join ref_date r
order by key -- If needed
;
KEY DATE_ FIELD1 NEW_VAL OLD_VAL DAILY_DIFF
---- ---------- ------ ------- ------- ----------
key1 10/02/2021 ABC 30 10 20
key2 10/02/2021 DEFG 6 -4 10
key3 10/02/2021 GHI 0 9 -9
key4 10/02/2021 LMN 2 0 2
解释:
WITH
子句中的 INTABLE
子查询只是模拟输入数据的一种简单方法(而不是创建 table 并插入其中,然后记得删除table 来自我的系统)。在实际使用中,从查询中删除 INTABLE
,而是使用您实际的 table 和列名称。
请注意我首先 运行 的 ALTER SESSION
声明 - 因此我不需要为 TO_DATE
提供格式模型。另请注意,我将列名从 DATE
更改为因为 DATE
是 Oracle 中的保留关键字。 (不要在列名称周围使用双引号来解决该约束;只需使用不同的名称,就像我所做的那样。)
您还会看到我在输出中使用了不同的列名。如果需要,请更改它们,但不要创建包含空格、破折号等的列名。使用标准列名。
REF_DATE
也可能不需要;我用它来给出“报告日期”——但如果它是一个绑定变量就更好了。或者,使用绑定变量而不是我给出的硬编码值,就在该子查询中(和 keep REF_DATE
WITH
子句)。
主要工作在PREP
子查询中完成。事实上,优化器会将这个子查询与外部查询(在代码的末尾)结合起来;我按照我的方式编写它只是为了节省一些输入 - 这样我就不需要重复 OLD_VAL
和 NEW_VAL
的定义来计算差异。优化查询(内部 Oracle 优化的结果)将只执行一个 SELECT
,并且它将使用“sort group by”(使用结果排序 ORDER BY
的外部查询,单次通过)。
查询(PREP
子查询)仅针对报告所需的两个日期进行过滤 - 索引 对 [=27= 列的帮助极大].然后查询使用标准聚合(不是分析函数或任何其他功能)——这很可能使其比替代方法更快。
不清楚为什么需要输出中的 DATE_
列。它不基于数据中的任何内容 - 它只是报告您作为输入提供的“参考日期”。我把它包括在内,以防你真的需要它(它非常便宜 - 我再次交叉连接到标量子查询 REF_DATE
),但如果你不需要它,你可以从主(外部)中删除它查询,您不需要交叉连接,也不需要 PREP
的别名和其他列的 P
限定符。
我有一个很大的table,按日期分区,结构如下:
InTable:
Key | Date | Field1 | Value |
---|---|---|---|
key1 | 09/02/2021 | ABC | 10 |
key2 | 09/02/2021 | DEF | -4 |
key3 | 09/02/2021 | GHI | 9 |
key1 | 10/02/2021 | ABC | 30 |
key2 | 10/02/2021 | DEFG | 6 |
key4 | 10/02/2021 | LMN | 2 |
我想输出一个table这样结构的
OutTable:
Key | Date | Field1 | Value T | Value T-1 | Daily Diff |
---|---|---|---|---|---|
key1 | 10/02/2021 | ABC | 30 | 10 | 20 |
key2 | 10/02/2021 | DEFG | 6 | -4 | 10 |
key3 | 10/02/2021 | GHI | 0 | 9 | -9 |
key4 | 10/02/2021 | LMN | 2 | 0 | 2 |
我在某个日期折叠了当前值、前一天的值以及具有相同键的两者之间的差异,同时保留了任何一个日期中缺少键的所有情况(对于例如,10 月 2 日缺少 key3,9 月 2 日缺少 key4。
如果key相同,Field1通常是相同的。如果它们不同,则必须保留最近的值(如示例中的 key2)。
据我所知,可以这样做:
- 通过在 in1.key = in2.key 和 in1.day = in2.day-1 上自行加入 InTable
- 通过在两个日期之间生成一个具有并集的稀疏矩阵,然后在关键字段上执行分组
- 通过使用枢轴结构
在 Oracle 中完成这项工作最有效、最快捷的方法是什么?
非常感谢。
我认为您需要 window 函数。如果你有所有天的数据或者想要数据中前一天的数据:
select t.*,
lag(value, 1, 0) over (partition by key, field order by date) as prev_value,
(value - lag(value, 1, 0) over (partition by key, field order by date)) as diff
from t;
如果值可以跳过日期,而您确实想要以前的日期,则可以使用带有 range
window 框架的 window 函数:
select t.*,
max(value) over (partition by key, field
order by date
range between '1' day preceding and '1' day preceding
) as prev_value,
(value -
max(value) over (partition by key, field
order by date
range between '1' day preceding and '1' day preceding
)
) as diff
from t;
这会将缺失值保留为 NULL
。您可以使用 COALESCE()
为它们赋值。
如果要按天过滤,需要在外部查询中进行:
select t.*
from (select t.*,
max(value) over (partition by key, field1
order by day
range between interval '1' day preceding and interval '1' day preceding
) as prev_value,
(value -
max(value) over (partition by key, field1
order by day
range between interval '1' day preceding and interval '1' day preceding
)
) as diff
from InTable t
) t
where day = date '2021-02-10'
order by day asc;
Here 是一个 db<>fiddle.
如果要修剪分区,则需要在子查询中包含滞后行。你可以起诉:
where day in (date '2021-02-10', date '2021-02-09')
在子查询中。
这是你可以做的(代码后的解释):
alter session set nls_date_format = 'dd/mm/yyyy';
with
intable (key, date_, field1, value) as (
select 'key1', to_date('09/02/2021'), 'ABC' , 10 from dual union all
select 'key2', to_date('09/02/2021'), 'DEF' , -4 from dual union all
select 'key3', to_date('09/02/2021'), 'GHI' , 9 from dual union all
select 'key1', to_date('10/02/2021'), 'ABC' , 30 from dual union all
select 'key2', to_date('10/02/2021'), 'DEFG', 6 from dual union all
select 'key4', to_date('10/02/2021'), 'LMN' , 2 from dual
)
, ref_date (dt) as (select to_date('10/02/2021') from dual)
, prep (key, field1, new_val, old_val) as (
select i.key,
min(field1) keep (dense_rank last order by date_),
nvl(min(case i.date_ when r.dt then value end), 0),
nvl(min(case i.date_ when r.dt - 1 then value end), 0)
from intable i cross join ref_date r
where date_ in (r.dt - 1, r.dt)
group by i.key, r.dt
)
select p.key, r.dt as date_, p.field1,
p.new_val, p.old_val, p.new_val - p.old_val as daily_diff
from prep p cross join ref_date r
order by key -- If needed
;
KEY DATE_ FIELD1 NEW_VAL OLD_VAL DAILY_DIFF
---- ---------- ------ ------- ------- ----------
key1 10/02/2021 ABC 30 10 20
key2 10/02/2021 DEFG 6 -4 10
key3 10/02/2021 GHI 0 9 -9
key4 10/02/2021 LMN 2 0 2
解释:
WITH
子句中的 INTABLE
子查询只是模拟输入数据的一种简单方法(而不是创建 table 并插入其中,然后记得删除table 来自我的系统)。在实际使用中,从查询中删除 INTABLE
,而是使用您实际的 table 和列名称。
请注意我首先 运行 的 ALTER SESSION
声明 - 因此我不需要为 TO_DATE
提供格式模型。另请注意,我将列名从 DATE
更改为因为 DATE
是 Oracle 中的保留关键字。 (不要在列名称周围使用双引号来解决该约束;只需使用不同的名称,就像我所做的那样。)
您还会看到我在输出中使用了不同的列名。如果需要,请更改它们,但不要创建包含空格、破折号等的列名。使用标准列名。
REF_DATE
也可能不需要;我用它来给出“报告日期”——但如果它是一个绑定变量就更好了。或者,使用绑定变量而不是我给出的硬编码值,就在该子查询中(和 keep REF_DATE
WITH
子句)。
主要工作在PREP
子查询中完成。事实上,优化器会将这个子查询与外部查询(在代码的末尾)结合起来;我按照我的方式编写它只是为了节省一些输入 - 这样我就不需要重复 OLD_VAL
和 NEW_VAL
的定义来计算差异。优化查询(内部 Oracle 优化的结果)将只执行一个 SELECT
,并且它将使用“sort group by”(使用结果排序 ORDER BY
的外部查询,单次通过)。
查询(PREP
子查询)仅针对报告所需的两个日期进行过滤 - 索引 对 [=27= 列的帮助极大].然后查询使用标准聚合(不是分析函数或任何其他功能)——这很可能使其比替代方法更快。
不清楚为什么需要输出中的 DATE_
列。它不基于数据中的任何内容 - 它只是报告您作为输入提供的“参考日期”。我把它包括在内,以防你真的需要它(它非常便宜 - 我再次交叉连接到标量子查询 REF_DATE
),但如果你不需要它,你可以从主(外部)中删除它查询,您不需要交叉连接,也不需要 PREP
的别名和其他列的 P
限定符。