最大数量 每天的并发用户会话数

Mik*_*ke 5 sql postgresql aggregate-functions overlap window-functions

情况

我们有一个PostgreSQL 8.4数据库,其中包含用户会话,其中包含登录日期/时间和每行注销日期/时间.我们的Web应用程序会记录此次并处理用户未明确注销的情况(会话超时).因此,每种情况下都会给出登录日期/时间和注销日期/时间.

目标

我需要每天最大并发会话数的用户统计信息.所以,我可以说以下内容:"在2015年3月16日同时在线用户峰值登录为6."

类似的问题

这里回答了一个类似的问题:每天每小时SQL最大并发会话 但是,我无法使解决方案适应我的情况,我希望有一个结果表,显示最大值.每天并发用户会话数,而不是每小时.表格方案也略有不同,因为我的案例中的一行包含登录和注销日期/时间,而在示例中,每行代表登录或注销.此外,该问题基于MS SQL数据库环境而不是PostgreSQL.

注意事项

  • 不同用户的会话可以重叠
  • 用户可能有重复会话,只应计数一次(按用户名分组)
  • 会话表的表格方案如下所示

表格方案:

user_id     |  login_date  |  login_time  |  logout_date  |  logout_time
------------+--------------+--------------+---------------+-------------
USER32      |  2014-03-03  |    08:23:00  |   2014-03-03  |     14:44:00
USER82      |  2014-03-03  |    08:49:00  |   2014-03-03  |     17:18:00
USER83      |  2014-03-03  |    09:40:00  |   2014-03-03  |     17:31:00
USER36      |  2014-03-03  |    09:50:00  |   2014-03-03  |     16:10:00
USER37      |  2014-03-03  |    11:44:00  |   2014-03-03  |     15:21:00
USER72      |  2014-03-03  |    12:52:00  |   2014-03-03  |     12:55:00
Run Code Online (Sandbox Code Playgroud)

以下示例通过Google Charts API作为时间线说明应有助于了解问题:http://i.imgur.com/ZOjnLll.png

鉴于2015-03-03的这一天的例子,除了USER78(6个用户)之外的所有用户都在当天的12:52和12:55之间登录.这是同时登录用户的最大数量,我需要在给定时间范围内每天进行这样的统计.

Day         |   MaxNumberOfConcurrentSessions 
------------+--------------------------------
2015-03-01  |                 2 
2015-03-02  |                 3
2015-03-03  |                 6
...
Run Code Online (Sandbox Code Playgroud)

上面的时间轴截图示例为Google Charts API.

google.setOnLoadCallback(drawChart);
function drawChart() {

  var container = document.getElementById('example5.1');
  var chart = new google.visualization.Timeline(container);
  var dataTable = new google.visualization.DataTable();
  dataTable.addColumn({ type: 'string', id: 'Room' });
  dataTable.addColumn({ type: 'string', id: 'Name' });
  dataTable.addColumn({ type: 'date', id: 'Start' });
  dataTable.addColumn({ type: 'date', id: 'End' });
  dataTable.addRows([
["USER78", '', new Date(2014,03,03,20,38), new Date(2014,03,03,21,14)],
["USER83", '', new Date(2014,03,03,09,40), new Date(2014,03,03,17,31)],
["USER72", '', new Date(2014,03,03,08,43), new Date(2014,03,03,08,43)],
["USER72", '', new Date(2014,03,03,09,40), new Date(2014,03,03,09,40)],
["USER72", '', new Date(2014,03,03,10,03), new Date(2014,03,03,10,06)],
["USER72", '', new Date(2014,03,03,12,52), new Date(2014,03,03,12,55)],
["USER72", '', new Date(2014,03,03,21,13), new Date(2014,03,03,21,13)],
["USER72", '', new Date(2014,03,03,21,37), new Date(2014,03,03,21,38)],
["USER72", '', new Date(2014,03,03,23,14), new Date(2014,03,03,23,15)],
["USER72", '', new Date(2014,03,03,23,27), new Date(2014,03,03,23,28)],
["USER36", '', new Date(2014,03,03,08,05), new Date(2014,03,03,09,17)],
["USER36", '', new Date(2014,03,03,09,50), new Date(2014,03,03,16,10)],
["USER36", '', new Date(2014,03,03,16,12), new Date(2014,03,03,20,29)],
["USER32", '', new Date(2014,03,03,08,23), new Date(2014,03,03,14,44)],
["USER82", '', new Date(2014,03,03,08,49), new Date(2014,03,03,17,18)],
["USER37", '', new Date(2014,03,03,08,04), new Date(2014,03,03,08,06)],
["USER37", '', new Date(2014,03,03,11,44), new Date(2014,03,03,15,21)],
["USER37", '', new Date(2014,03,03,15,34), new Date(2014,03,03,15,51)],
["USER37", '', new Date(2014,03,03,16,12), new Date(2014,03,03,16,14)],
["USER37", '', new Date(2014,03,03,16,52), new Date(2014,03,03,16,54)],
["USER37", '', new Date(2014,03,03,17,07), new Date(2014,03,03,17,08)],
["USER37", '', new Date(2014,03,03,20,20), new Date(2014,03,03,20,24)],
["USER37", '', new Date(2014,03,03,21,03), new Date(2014,03,03,21,20)],
["USER37", '', new Date(2014,03,03,22,42), new Date(2014,03,03,23,05)],
["USER37", '', new Date(2014,03,03,23,51), new Date(2014,03,03,23,56)],
["USER01", '', new Date(2014,03,03,16,11), new Date(2014,03,03,16,12)]
]);

  var options = {
    timeline: { colorByRowLabel: true }
  };

  chart.draw(dataTable, options);
}
Run Code Online (Sandbox Code Playgroud)
<script type="text/javascript" src="https://www.google.com/jsapi?autoload={'modules':[{'name':'visualization',
       'version':'1','packages':['timeline']}]}"></script>
<div id="example5.1" style="width:5000px;height: 600px;"></div>
   
Run Code Online (Sandbox Code Playgroud)

Erw*_*ter 5

我会序列化登录和注销UNION ALL,“in”计为 1,“out”计为 -1。然后使用简单的窗口函数计算运行计数并获得每天的最大值。

由于尚未指定,假设:

  • “并发”是指在同一时间点(不仅仅是同一天)。
  • 会话可以跨越任何时间范围(也就是多天)。
  • 每个用户在一个时间点只能在线一次。所以在我的解决方案中不需要对每个用户进行分组。
  • 注销胜过登录。如果两者同时发生,则首先计算注销(导致在极端情况下并发数较低)。
WITH range AS (SELECT '2014-03-01'::date AS start_date  -- time range
                    , '2014-03-31'::date AS end_date)   -- inclusive bounds
, cte AS (
   SELECT *
   FROM   tbl, range r
   WHERE  login_date  <= r.end_date
   AND    logout_date >= r.start_date
   )
, ct AS (
   SELECT log_date, sum(ct) OVER (ORDER BY log_date, log_time, ct) AS session_ct
   FROM  (
      SELECT logout_date AS log_date, logout_time AS log_time, -1 AS ct FROM cte
      UNION ALL
      SELECT login_date, login_time, 1 FROM cte
      ) sub
   )
SELECT log_date, max(session_ct) AS max_sessions
FROM   ct, range r
WHERE  log_date BETWEEN r.start_date AND r.end_date  -- crop actual time range
GROUP  BY 1
ORDER  BY 1;
Run Code Online (Sandbox Code Playgroud)

可以在 中使用OVERLAPS运算符cte

AND   (login_date, logout_date) OVERLAPS (r.start_date, r.end_date)
Run Code Online (Sandbox Code Playgroud)

细节:

但这可能不是一个好主意,因为(根据文档):

每个时间段都被视为代表半开区间start <= time < end,除非startend相等,在这种情况下它代表单个时间瞬间。这意味着例如只有一个共同端点的两个时间段不重叠

大胆强调我的。您的范围的上限必须是您想要的时间范围之后的第二天。

解释

  • CTE 从 Postgres 8.4 开始可用

  • 第1 CTErange只是提供时间范围的便利一次

  • 第二个 CTEcte只选择相关的行:那些......

    • 在范围之前或范围内开始
    • 并在范围内或之后结束
  • 第三个 CTEct序列化“in”和“out”点,值为 +/-1,并sum()使用用作窗口函数的聚合函数计算运行计数。这些从 Postgres 8.4 开始可用

  • 在最后SELECT修剪前导和尾随天数,并汇总每天的最大值。瞧。

用于 Postgres 9.6 的SQL 小提琴
Postgres 8.4 太旧了,不再可用,但应该可以正常工作。我在测试用例中添加了一行 - 一个跨越多天。应该让它更有用。

笔记

我通常会使用timestamp代替datetime。同样的尺寸,更容易处理。或者timestamptz如果可以涉及多个时区。

索引(login_date, logout_date DESC)作为最低限度的性能有帮助。