SQL:查找在给定日期范围内每个月都有数据的记录

时间:2014-10-25 12:00:41

标签: sql postgresql relational-division

考虑以下表格

 =# \d users
 Column |         Type
--------+-----------------------
 id     | integer
 name   | character varying(32)

=# \d profiles
 Column  |  Type
---------+---------
 id      | integer
 user_id | integer

=# \d views
   Column   |            Type
------------+-----------------------------
 id         | integer
 profile_id | integer
 time       | timestamp without time zone

我需要在给定日期范围的每个月找到具有关联视图的所有用户。目前我正在做以下事情:

with months as (
  select to_char(month, 'MM/YYYY') from generate_series('2014-07-01', '2014-09-01', INTERVAL '1 month') as month
)

select * from users
  join profiles on user_id = users.id
  join views on profile_id = profiles.id
    and to_char(views.time, 'MM/YYYY') in (select * from months)

我设置了一个小提琴here

目前,搜索结果包括8月和9月没有观看次数的用户 Kyle 。正确的结果应该只包括在给定范围内的所有3个月内都有视图的用户 Stan 。我们如何修改此查询以返回所需的结果?

4 个答案:

答案 0 :(得分:2)

也许这就足够了(我不知道Postgresql)

select u.id, u.name  from users u
  join profiles on user_id = users.id
  join views on profile_id = profiles.id
    and views.time between ? and ?
group by u.id, u.name
having count(distinct to_char(views.time, 'MM/YYYY')) = 3;

答案 1 :(得分:2)

您似乎有一个扩展的关系部门,即您正在寻找仅在给定范围内拥有观看次数的用户,尽管他们的观点也可能超出了感兴趣的范围。

GROUP BY一起,您可以通过EXCEPT构造进行检查。基本上,如果您使用给定范围内的所有视图减去范围内的所有月份,则不应接收任何行:

WITH months(month) AS (
  SELECT DATE '2014-07-01' + m*INTERVAL'1mon'
    FROM generate_series(0,2) m
)
SELECT *
  FROM users    u
  JOIN profiles p ON p.user_id=u.id
  JOIN views    v ON v.profile_id=p.id
 WHERE 0 = (SELECT count(*) FROM (
    SELECT month FROM months
    EXCEPT ALL
    SELECT date_trunc('mon',time) FROM views
     WHERE date_trunc('mon',time) IN (SELECT * FROM months)
       AND profile_id=p.id) minus);

您可以通过= ALL构造稍微简化此构造,因为在子查询没有返回任何行时它将返回true

WITH months(month) AS (
  SELECT DATE '2014-07-01' + m*INTERVAL'1mon'
    FROM generate_series(0,2) m
)
SELECT *
  FROM users    u
  JOIN profiles p ON p.user_id=u.id
  JOIN views    v ON v.profile_id=p.id
 WHERE date_trunc('mon',time) = ALL (
    SELECT month FROM months
    EXCEPT ALL
    SELECT date_trunc('mon',time) FROM views
     WHERE date_trunc('mon',time) IN (SELECT * FROM months)
       AND profile_id=p.id);

ALL手册中的引用:

  

ALL的结果是" true"如果所有行都产生真   (包括子查询不返回任何行的情况)

我的查询实际上都是一样的。第一个计算内侧的行数并将它们与零进行比较(我同意,这更明显)。第二个将当前views.time与子查询的所有结果进行比较。仅当子查询返回的所有条目等于views.time(当然,截断到月边界)时,此构造才会生成true。并且,如果引用,如果子查询不返回任何行,则此构造也会生成true。

根据意图,子查询不应该产生任何行,这表示所有视图都在期望的时间范围内发生。

Check on SQL Fiddle

答案 2 :(得分:1)

with months (month, month_count) as (
  select to_char(month, 'MM/YYYY'), 
         count(*) over ()
  from generate_series('2014-07-01', '2014-09-01', INTERVAL '1' month) as month
), counted as (
  select *, 
         count(*) over (partition by user_id) as cnt
  from users
    join profiles on user_id = users.id
    join views on profile_id = profiles.id
    join months on months.month = to_char(views.time, 'MM/YYYY')
) 
select * 
from counted
where cnt = month_count

第一个CTE现在包括所有月份的计数,第二个CTE然后计算每个用户的月份并且是必要的,因为窗口函数不能直接放入where子句。最后的选择然后消除了没有所有月份的用户。

答案 3 :(得分:0)

确保它不仅是匹配的计数,而且还有在转换月份系列后可以进行数组比较的内容,以及视图到数组。

  • 我们知道的第一个cte
  • 第二个cte创建了一组分组的用户,以及他们访问过的不同月份的数组
  • 最后一个查询比较两个数组

示例:

with months as (
    select array_agg(to_char(month, 'MM/YYYY')) m
    from generate_series('2014-07-01', '2014-09-01', INTERVAL '1 month') as month
), user_months as (
    select p.user_id, array_agg(distinct to_char(v.time, 'MM/YYYY')) m
    from views v
    inner join profiles p on p.id = v.profile_id
    group by p.user_id
)
select um.*
from user_months um
inner join months m on m.m = um.m;