按特定日期间隔对分组进行计数

时间:2019-03-09 14:52:30

标签: sql postgresql

我想计算自合同开始之日起30天周期组中客户拥有的“服务”数量。因此,我必须从他的开始日期开始,每月计算一次服务时间。简化表格是这样的:

services  
------------------
id serial   
id_customer bigint  
service_date date  

让成像只有一种服务。我这样解决:

SELECT 
  DATE_PART('year',service_date)||'-'|| CASE WHEN DATE_PART('day',service_date) >= 15 THEN 
    DATE_PART('month',service_date)
  ELSE
    CASE WHEN DATE_PART('month',service_date) = 1 THEN 
        12 
    ELSE 
        DATE_PART('month',service_date)-1 
    END
  END bill, count(id) 
FROM services
WHERE id_customer = 1
GROUP BY bill

结果应为

bill    | count
-------------------
2019-02 | 2455333

在此示例中,id_customer 1的开始日期为2019-02-15,但对于2019-02期间,我将一直计算服务至2019-03-14。

我想知道的是,有一个更好/更有效的解决方案吗?

我看到了解决方法here,但是它暗示了一个带有GROUP BY的INNER JOIN和同一张表,我认为这样做会比较慢,因为我的表有很多记录。

1 个答案:

答案 0 :(得分:0)

您不必担心一个月中的实际天数,也不必担心月,年或月中的某天。

只需使用客户的开始日期,让PostgreSQL为您生成正确的计费周期即可。

要对所有客户运行单个查询,我使用了一个单独的表,其中配置了客户id并配置了一个billing_start日期,然后我们可以针对该日期运行查询,如下所示:

WITH
    periods (id, period_start, period_end) AS (
        SELECT
            id,
            generate_series(billing_start, current_date, '1 month'::interval)::date,
            (generate_series(billing_start, current_date, '1 month'::interval) + '1 month'::interval)::date
        FROM test_customers
    ),
    data AS (
        SELECT
            periods.id AS customer,
            period_start,
            count(test_services.*) AS service_calls
        FROM periods INNER JOIN test_services ON (test_services.id_customer = periods.id)
        WHERE test_services.service_date >= periods.period_start AND test_services.service_date < periods.period_end
        GROUP BY 1, 2
    )
SELECT customer, to_char(period_start, 'YYYY-MM') AS bill, service_calls
FROM data
ORDER BY 1, 2
;

...产生如下输出:

 customer |  bill   | service_calls 
----------+---------+---------------
        1 | 2018-12 |        382736
        1 | 2019-01 |        382735
        1 | 2019-02 |        345696
        2 | 2018-12 |        382736
        2 | 2019-01 |        382734
        2 | 2019-02 |        234580
        3 | 2018-12 |        382734
        3 | 2019-01 |        382736
        3 | 2019-02 |        123463
        4 | 2018-12 |        382734
        4 | 2019-01 |        382736
        4 | 2019-02 |         12346
        5 | 2019-01 |        382735
        5 | 2019-02 |        283965
        6 | 2019-01 |        382735
        6 | 2019-02 |        172848
        7 | 2019-01 |        382734
        7 | 2019-02 |         61732
        8 | 2019-02 |        333351
        9 | 2019-02 |        222234
       10 | 2019-02 |        111117
(21 rows)

完整的在线示例:https://rextester.com/IHLJ95398

要快速注意的重要一点是id_customerservice_date上的多列索引,因为这是进行计数的地方,然后可以不进行排序就完成:

CREATE INDEX idx_svc_customer_date ON test_services (id_customer, service_date);

(否则,排序很可能会在磁盘上完成,而不是在大型数据集的内存中进行)

如果您只想要单个客户的周期,请像这样使用它:

WITH
    periods (id, period_start, period_end) AS (
        SELECT
            id,
            generate_series(billing_start, current_date, '1 month'::interval)::date,
            (generate_series(billing_start, current_date, '1 month'::interval) + '1 month'::interval)::date
        FROM test_customers WHERE id = 4
    ),
    data AS (
        SELECT
            periods.id AS customer,
            period_start,
            count(test_services.*) AS service_calls
        FROM periods INNER JOIN test_services ON (test_services.id_customer = periods.id)
        WHERE test_services.service_date >= periods.period_start AND test_services.service_date < periods.period_end
        GROUP BY 1, 2
    )
SELECT customer, to_char(period_start, 'YYYY-MM') AS bill, service_calls
FROM data
ORDER BY 1, 2
;

...给予:

  bill   | service_calls 
---------+---------------
 2018-12 |        382734
 2019-01 |        382736
 2019-02 |         12346
(3 rows)