我有不同公司的日常时间序列(实际上是工作日),我使用PostgreSQL。还有一个指标变量(称为标志),大部分时间取值为0,在某些罕见的事件日取值为1。如果指标变量的值为公司的值1,我想进一步调查相应公司的事件前两天到事件后一天的条目。让我将其称为[-2,1]窗口,事件日为第0天。
我使用以下查询
CREATE TABLE test AS
WITH cte AS (
SELECT *
, MAX(flag) OVER(PARTITION BY company ORDER BY day
ROWS BETWEEN 1 preceding AND 2 following) Lead1
FROM mytable)
SELECT *
FROM cte
WHERE Lead1 = 1
ORDER BY day,company
对于遇到该事件的公司,查询将从事件发生前2天到事件发生后一天的条目。 该查询为所有事件执行此操作。
这是结果表的一小部分。
day company flag
2012-01-23 A 0
2012-01-24 A 0
2012-01-25 A 1
2012-01-25 B 0
2012-01-26 A 0
2012-01-26 B 0
2012-01-27 B 1
2012-01-30 B 0
2013-01-10 A 0
2013-01-11 A 0
2013-01-14 A 1
现在我想分别对每个[-2,1]窗口进行进一步的计算。所以我需要一个允许我识别每个[-2,1]窗口的变量。我的想法是,我计算每个变量“出现”的公司的窗口数,以便在进一步的计算中我可以使用该子句
GROUP BY company, occur
因此我希望的输出看起来像这样:
day company flag occur
2012-01-23 A 0 1
2012-01-24 A 0 1
2012-01-25 A 1 1
2012-01-25 B 0 1
2012-01-26 A 0 1
2012-01-26 B 0 1
2012-01-27 B 1 1
2012-01-30 B 0 1
2013-01-10 A 0 2
2013-01-11 A 0 2
2013-01-14 A 1 2
在该示例中,公司B仅出现一次(出现= 1)。但公司A出现了两次。这是2012-01-23至2012-01-26的第一次。并且是2013-01-10至2013-01-14的第二次。公司A的第二个时间范围不包括事件日(-2,-1,0,1)周围的所有四天,因为公司在该时间范围结束之前离开数据集。
正如我所说,我在工作日工作。我不关心假期,我有星期一到星期五的数据。之前我写过以下函数:
CREATE OR REPLACE FUNCTION addbusinessdays(date, integer)
RETURNS date AS
$BODY$
WITH alldates AS (
SELECT i,
$1 + (i * CASE WHEN $2 < 0 THEN -1 ELSE 1 END) AS date
FROM generate_series(0,(ABS($2) + 5)*2) i
),
days AS (
SELECT i, date, EXTRACT('dow' FROM date) AS dow
FROM alldates
),
businessdays AS (
SELECT i, date, d.dow FROM days d
WHERE d.dow BETWEEN 1 AND 5
ORDER BY i
)
-- adding business days to a date --
SELECT date FROM businessdays WHERE
CASE WHEN $2 > 0 THEN date >=$1 WHEN $2 < 0
THEN date <=$1 ELSE date =$1 END
LIMIT 1
offset ABS($2)
$BODY$
LANGUAGE 'sql' VOLATILE;
它可以从给定日期添加/减去工作日,并按以下方式工作:
select * from addbusinessdays('2013-01-14',-2)
提供结果2013-01-10。因此,在Jakub的方法中,我们可以将第二行和第三行更改为
w.day BETWEEN addbusinessdays(t1.day, -2) AND addbusinessdays(t1.day, 1)
并且可以处理工作日。
答案 0 :(得分:1)
基本上,策略是首先将旗帜日纳入其中,然后与其他人一起加入:
WITH windows AS(
SELECT t1.day
,t1.company
,rank() OVER (PARTITION BY company ORDER BY day) as rank
FROM table1 t1
WHERE flag =1)
SELECT t1.day
,t1.company
,t1.flag
,w.rank
FROM table1 AS t1
JOIN windows AS w
ON
t1.company = w.company
AND
w.day BETWEEN
t1.day - interval '2 day' AND t1.day + interval '1 day'
ORDER BY t1.day, t1.company;
然而,工作日存在问题,因为这些可能意味着什么(节假日计数?)。
答案 1 :(得分:1)
在使用函数addbusinessdays()
时,请考虑以下内容:
CREATE OR REPLACE FUNCTION addbusinessdays(date, integer)
RETURNS date AS
$func$
SELECT day
FROM (
SELECT i, $1 + i * sign($2)::int AS day
FROM generate_series(0, ((abs($2) * 7) / 5) + 3) i
) sub
WHERE EXTRACT(ISODOW FROM day) < 6 -- truncate weekend
ORDER BY i
OFFSET abs($2)
LIMIT 1
$func$ LANGUAGE sql IMMUTABLE;
永远不要引用语言名称sql
。它是一个标识符,而不是字符串。
为什么函数VOLATILE
?将其设为IMMUTABLE
以获得更好的重复使用性能和更多选项(例如在功能索引中使用它)。
(ABS($2) + 5)*2)
填充太多了。替换为((abs($2) * 7) / 5) + 3)
。
多级CTE是无用的。
ORDER BY
也没用。
正如我之前的回答所提到的,extract(
ISODOWFROM ...)
更容易截断周末。
那就是说,我根本不会对这个查询使用上面的函数。建立一个完整的相关日期网格,而不是计算每一行的天数范围。
基于评论中的这个断言(应该在问题中,真的!):
同一公司的两个后续窗口永远不会重叠。
WITH range AS ( -- only with flag
SELECT company
, min(day) - 2 AS r_start
, max(day) + 1 AS r_stop
FROM tbl t
WHERE flag <> 0
GROUP BY 1
)
, grid AS (
SELECT company, day::date
FROM range r
,generate_series(r.r_start, r.r_stop, interval '1d') d(day)
WHERE extract('ISODOW' FROM d.day) < 6
)
SELECT *, sum(flag) OVER(PARTITION BY company ORDER BY day
ROWS BETWEEN UNBOUNDED PRECEDING
AND 2 following) AS window_nr
FROM (
SELECT t.*, max(t.flag) OVER(PARTITION BY g.company ORDER BY g.day
ROWS BETWEEN 1 preceding
AND 2 following) in_window
FROM grid g
LEFT JOIN tbl t USING (company, day)
) sub
WHERE in_window > 0 -- only rows in [-2,1] window
AND day IS NOT NULL -- exclude missing days in [-2,1] window
ORDER BY company, day;
构建所有工作日的网格:CTE grid
。
要使网格尽可能小,请按公司提取最小和最大(加缓冲)日:CTE range
。
LEFT JOIN
实际行。现在,随后窗口函数的框架与静态数字一起使用。
要获得每个标记和公司(window_nr
)的不同数字,只需从网格开头计算标记(考虑缓冲区)。
仅在[-2,1]窗口(in_window > 0
)内保留数天。
仅保留表中实际行的天数。
VOILÀ。