从存储的活动开始和结束时间中获取空余时间

时间:2016-06-12 20:41:28

标签: postgresql function range plpgsql gaps-and-islands

我正在尝试实现一个计算存储活动开始和结束时间的空闲时间的函数。我在PostgreSQL 9.5.3上实现了我的数据库。这就是活动表的样子

activity_id | user_id   | activity_title                     | starts_at                     | ends_at 

(serial)    | (integer) | (text)                             | (timestamp without time zone) |(timestamp without time zone)
---------------------------------------------------------------------------------------------------------------------------
1           | 1         | Go to school                       | 2016-06-12 08:00:00           | 2016-06-12 14:00:00
2           | 1         | Visit my uncle                     | 2016-06-12 16:00:00           | 2016-06-12 17:30:00
3           | 1         | Go shopping                        | 2016-06-12 18:00:00           | 2016-06-12 21:15:00
4           | 1         | Go to Library                      | 2016-06-13 10:00:00           | 2016-06-13 12:00:00
5           | 1         | Install some programs on my laptop | 2016-06-13 18:00:00           | 2016-06-13 19:00:00

我真实桌子的实际表格定义:

CREATE TABLE public.activity (
  activity_id serial,
  user_id integer NOT NULL,
  activity_title text,
  starts_at timestamp without time zone NOT NULL,
  start_tz text NOT NULL,
  ends_at timestamp without time zone NOT NULL,
  end_tz text NOT NULL,
  recurrence text NOT NULL DEFAULT 'none'::text,
  lat numeric NOT NULL,
  lon numeric NOT NULL,
  CONSTRAINT pk_activity PRIMARY KEY (activity_id),
  CONSTRAINT fk_user_id FOREIGN KEY (user_id)
      REFERENCES public.users (user_id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
)

我想使用以(user_id INTEGER, range_start TIMESTAMP, range_end TIMESTAMP)为参数的PL / pgSQL函数计算该用户每天的业余时间。我想要这个SQL语句的输出:

SELECT * from calculate_spare_time(1, '2016-06-12', '2016-06-13');

是这样的:

spare_time_id | user_id   | starts_at                     | ends_at 

(serial)      | (integer) | (timestamp without time zone) |(timestamp without time zone)
----------------------------------------------------------------------------------------
1             | 1         | 2016-06-12 00:00:00           | 2016-06-12 08:00:00
2             | 1         | 2016-06-12 12:00:00           | 2016-06-12 16:00:00
3             | 1         | 2016-06-12 17:30:00           | 2016-06-12 18:00:00
4             | 1         | 2016-06-12 21:15:00           | 2016-06-13 00:00:00
5             | 1         | 2016-06-13 00:00:00           | 2016-06-13 10:00:00
6             | 1         | 2016-06-13 12:00:00           | 2016-06-13 18:00:00
7             | 1         | 2016-06-13 19:00:00           | 2016-06-14 00:00:00

我有想法从同一天发生的下一个活动的开始时间减去一个活动的结束时间,但是我坚持用PL / pgSQL实现它,特别是如何处理2行中的同一时间。

1 个答案:

答案 0 :(得分:1)

为了简化操作,我建议创建一个视图 - 或者更好的是:MATERIALZED VIEW在每个用户的活动中显示间隙

CREATE MATERIALIZED VIEW mv_gap AS
SELECT user_id, tsrange(a, z) AS gap
FROM  (
   SELECT user_id, ends_at AS a
        , lead(starts_at) OVER (PARTITION BY user_id ORDER BY starts_at) AS z
   FROM   activity
   ) sub
WHERE  z > a;  -- weed out simple overlaps and the dangling "gap" till infinity

请注意range type tsrange

注意:您提到了可能的重叠,这使事情变得复杂。如果单个用户的一个时间范围可以包含在另一个时间,则需要执行更多操作!合并时间范围以识别每个块的最早开始和最后结束。

请记住在需要时刷新MV。

然后您的功能可以简单地为:

CREATE OR REPLACE FUNCTION f_freetime(_user_id int, _from timestamp, _to timestamp)
  RETURNS TABLE (rn int, gap tsrange) AS
$func$
   SELECT row_number() OVER (ORDER BY g.gap)::int AS rn
        , g.gap * tsrange(_from, _to) AS gap
   FROM   mv_gap g
   WHERE  g.user_id = _user_id
   AND    g.gap && tsrange(_from, _to)
   ORDER  BY g.gap;
$func$  LANGUAGE sql STABLE;

呼叫:

SELECT * FROM f_freetime(1, '2016-06-12 0:0', '2016-06-13 0:0');

请注意range operators * and && 还要注意我使用了一个简单的SQL函数,问题已经足够简化了。如果您需要添加更多内容,可能需要切换回plpgsql并使用RETURN QUERY ...

或者只使用不带函数包装的查询。

性能

如果每个用户有多个行,为了优化查询时间,请添加SP-GiST索引(使用MV的一个原因):

CREATE INDEX activity_gap_spgist_idx on mv_gap USING spgist (gap);

除了(user_id)的索引外 这个相关答案的细节: