一次通话中多个日期范围的总和?

时间:2014-01-29 04:16:23

标签: sql ruby-on-rails postgresql sum query-optimization

我有以下查询:

SELECT 
   SUM("balance_transactions"."fee") AS sum_id 
   FROM "balance_transactions" 
   JOIN charges ON balance_transactions.source = charges.balance_id 
   WHERE "balance_transactions"."account_id" = 6 
      AND (balance_transactions.type = 'charge' 
      AND charges.refunded = false 
      AND charges.invoice IS NOT NULL) 
      AND ("balance_transactions"."created" BETWEEN '2013-12-20' AND '2014-01-19');

这样做会增加这两个日期之间发生的所有“费用”。大。工作正常。

问题是我几乎总是一次需要数百个日期范围的费用,这相当于我运行相同的查询数百次。效率不高。

但是有没有办法将其压缩为所有日期范围的单个查询?

例如,我会为SUM调用一系列范围,如下所示:

2013-12-20 to 2014-01-19
2013-12-21 to 2014-01-20
2013-12-22 to 2014-01-21
2013-12-23 to 2014-01-22
2013-12-24 to 2014-01-23
...so on and so on

我需要输出在每个日期范围内收集的费用总和(最终需要在数组中)。

那么,有关处理这种情况和减少数据库事务的方法的任何想法吗?

FWIW,这是在Rails应用程序内的Postgres上。

10 个答案:

答案 0 :(得分:8)

假设我正确理解了您的要求,我认为您需要的是这样的:

SELECT "periods"."start_date", 
       "periods"."end_date", 
       SUM(CASE WHEN "balance_transactions"."created" BETWEEN "periods"."start_date" AND "periods"."end_date" THEN "balance_transactions"."fee" ELSE 0.00 END) AS period_sum
  FROM "balance_transactions" 
  JOIN charges ON balance_transactions.source = charges.balance_id 
  JOIN ( SELECT '2013-12-20'::date as start_date, '2014-01-19'::date as end_date UNION ALL
         SELECT '2013-12-21'::date as start_date, '2014-01-20'::date as end_date UNION ALL
         SELECT '2013-12-22'::date as start_date, '2014-01-21'::date as end_date UNION ALL
         SELECT '2013-12-23'::date as start_date, '2014-01-22'::date as end_date UNION ALL
         SELECT '2013-12-24'::date as start_date, '2014-01-23'::date as end_date
         ) as periods
    ON "balance_transactions"."created" BETWEEN "periods"."start_date" AND "periods"."end_date"
 WHERE "balance_transactions"."account_id" = 6 
   AND "balance_transactions"."type" = 'charge' 
   AND "charges"."refunded" = false 
   AND "charges"."invoice" IS NOT NULL
 GROUP BY "periods"."start_date", "periods"."end_date"

这应该会在一个结果集中返回您感兴趣的所有句点。 由于查询是在前端即时“生成”的,因此您可以根据需要向句点部分添加任意数量的行。

编辑:经过一些试验和错误,我设法让它在[sqlFiddle] [1]中运行并相应地更新了上面的语法。

答案 1 :(得分:0)

从SQL Server背景来看,我会将你的where子句更改为

...
AND (
      "balance_transactions"."created" BETWEEN '2013-12-20' AND '2014-01-19'
      OR
      "balance_transactions"."created" BETWEEN '2013-12-21' AND '2014-01-20'
      OR
      "balance_transactions"."created" BETWEEN '2013-12-23' AND '2014-01-22'
      OR
      "balance_transactions"."created" BETWEEN '2013-12-24' AND '2014-01-23'
    );

请确保您在这些日期有一个很好的索引! :)

答案 2 :(得分:0)

这是您可以使用的未经测试的程序。

CREATE OR REPLACE PROCEDURE sum_fees(v_start IN Date, v_end in Date) IS

BEGIN
  SELECT 
   SUM("balance_transactions"."fee") AS sum_id 
   FROM "balance_transactions" 
       JOIN charges ON balance_transactions.source = charges.balance_id 
   WHERE "balance_transactions"."account_id" = 6 
      AND (balance_transactions.type = 'charge' 
      AND charges.refunded = false 
      AND charges.invoice IS NOT NULL) 
      AND ("balance_transactions"."created" BETWEEN v_start AND v_end);
END;

然后使用您的范围日期调用该程序。

答案 3 :(得分:0)

它尚未被提及,并且不如单个选择BUT那样有效,因为您可以使用UNION和您自己的静态标识符(日期范围组)组合,例如:

SELECT 
   'a' AS DateRangeIdentifier,
   SUM("balance_transactions"."fee") AS sum_id 
   FROM "balance_transactions" 
   JOIN charges ON balance_transactions.source = charges.balance_id 
   WHERE "balance_transactions"."account_id" = 6 
      AND (balance_transactions.type = 'charge' 
      AND charges.refunded = false 
      AND charges.invoice IS NOT NULL) 
      AND ("balance_transactions"."created" BETWEEN '2013-01-01' AND '2013-01-31')

UNION 

SELECT 
   'b' AS DateRangeIdentifier,
   SUM("balance_transactions"."fee") AS sum_id 
   FROM "balance_transactions" 
   JOIN charges ON balance_transactions.source = charges.balance_id 
   WHERE "balance_transactions"."account_id" = 6 
      AND (balance_transactions.type = 'charge' 
      AND charges.refunded = false 
      AND charges.invoice IS NOT NULL) 
      AND ("balance_transactions"."created" BETWEEN '2013-07-01' AND '2013-07-31')

UNION 

SELECT 
   'c' AS DateRangeIdentifier,
   SUM("balance_transactions"."fee") AS sum_id 
   FROM "balance_transactions" 
   JOIN charges ON balance_transactions.source = charges.balance_id 
   WHERE "balance_transactions"."account_id" = 6 
      AND (balance_transactions.type = 'charge' 
      AND charges.refunded = false 
      AND charges.invoice IS NOT NULL) 
      AND ("balance_transactions"."created" BETWEEN '2013-12-20' AND '2014-01-19')

GROUP BY DateRangeIdentifier, sum_id

这种方式至少会产生一个数据库请求,而不是代码中的循环。

请在此处查看更新的小提琴:http://sqlfiddle.com/#!15/9ce0f/5

答案 4 :(得分:0)

如果我理解得很好,你想重新使用日期查询。为此,可以重用的查询部分是每日部分。我的意思是:

SELECT 
   SUM("balance_transactions"."fee") AS sum_id 
   FROM "balance_transactions" 
   JOIN charges ON balance_transactions.source = charges.balance_id 
   WHERE "balance_transactions"."account_id" = 6 
      AND (balance_transactions.type = 'charge' 
      AND charges.refunded = false 
      AND charges.invoice IS NOT NULL) 
      AND ("balance_transactions"."created" = 'yyyy-mm-dd');

假设您的“已创建”字段只是日期而非时间戳,并且如果过去几天的数据没有更改,您可以将此查询转储到表中:

insert into sum_table
SELECT 
   "balance_transactions"."created" balance_created
   SUM("balance_transactions"."fee") AS balance_fee 
   FROM "balance_transactions" 
   JOIN charges ON balance_transactions.source = charges.balance_id 
   WHERE "balance_transactions"."account_id" = 6 
      AND (balance_transactions.type = 'charge' 
      AND charges.refunded = false 
      AND charges.invoice IS NOT NULL) 
   group by "balance_transactions"."created"
;

然后将您的主查询更改为:

SELECT 
   SUM(balance_fee) AS sum_id 
   FROM sum_table where balance_created between ('2013-12-20' AND '2014-01-19');

另一个优化是消除介于两者之间,因为通常它不使用索引,如果你有很多不同的日期,它可能会很慢。

更好的方式:

SELECT 
   SUM(balance_fee) AS sum_id 
   FROM sum_table where balance_created in ('2013-12-20', '2013-12-21', '2013-12-22' ... '2014-01-19');

但为此,您必须直接在客户端应用程序(ej.DAO)中创建SQL

希望这有帮助。

答案 5 :(得分:0)

这将返回给定日期和今天之间所有月份范围的总和。

元组输出 - SQL Fiddle

select distinct on (s.d)
    s.d as "date",
    sum(bt.fee) over (
        partition by daterange(s.d, (s.d + interval '1 month')::date, '[]')
    ) as sum_id
from
    balance_transactions bt
    inner join
    charges on bt.source = charges.balance_id
    right join
    (
        select d::date as d
        from generate_series (
            '2013-12-20'::date,
            current_date,
            interval '1 day'
        ) s(d)
    ) s on s.d = bt.created
where
    bt.account_id = 6
    and bt.type = 'charge'
    and charges.refunded = false
    and charges.invoice is not null
order by s.d

数组输出。不适用于SQL Fiddle但适用于我的桌面。

select array_agg(("date", sum_id)) as arr_sum_id
from (
    select distinct on (s.d)
        s.d as "date",
        sum(bt.fee) over (
            partition by daterange(s.d, (s.d + interval '1 month')::date, '[]')
        ) as sum_id
    from
        balance_transactions bt
        inner join
        charges on bt.source = charges.balance_id
        right join
        (
            select d::date as d
            from generate_series (
                '2013-12-20'::date,
                current_date,
                interval '1 day'
            ) s(d)
        ) s on s.d = bt.created
    where
        bt.account_id = 6
        and bt.type = 'charge'
        and charges.refunded = false
        and charges.invoice is not null
    order by s.d
) s

答案 6 :(得分:0)

试试这个:

create table timeframes (
    start_dt date,
    end_dt date
);

insert into timeframes values ('2013-12-20', '2014-01-19');
insert into timeframes values ('2013-12-21', '2014-01-20');
insert into timeframes values ('2013-12-22', '2014-01-21');
insert into timeframes values ('2013-12-23', '2014-01-22');
insert into timeframes values ('2013-12-24', '2014-01-23');

SELECT 
    tf.start_date, 
    tf.end_date, 
    SUM(CASE 
        WHEN t.created BETWEEN tf.start_date AND tf.end_date THEN t.fee
        ELSE 0.00 
    END) as transaction_sum
FROM 
    balance_transactions t
INNER JOIN 
    charges c
ON 
    t.source = c.balance_id 
INNER JOIN 
    timeframes tf
ON 
    t.created BETWEEN tf.start_date AND tf.end_date
WHERE 
    t.account_id = 6
AND 
    (
    t.type = 'charge' 
        AND 
    c.refunded = false 
        AND 
    c.invoice IS NOT NULL
    ) 
GROUP BY
    tf.start_date, 
    tf.end_date

答案 7 :(得分:0)

SELECT periods.start_date, 
     periods.end_date, 
     SUM(fee) AS Period_Sum
FROM "balance_transactions" 
JOIN charges ON balance_transactions.source = charges.balance_id 
JOIN
(SELECT CAST('2013-12-20' AS DATE) AS start_date, CAST('2014-01-19' AS DATE) AS end_date UNION     ALL
 SELECT  CAST('2013-12-21' AS DATE),CAST('2014-01-20'  AS DATE) UNION ALL
 SELECT  CAST('2013-12-22' AS DATE),  CAST('2014-01-21' AS DATE) UNION ALL
 SELECT CAST('2013-12-23' AS DATE),  CAST('2014-01-22' AS DATE) UNION ALL
 SELECT  CAST('2013-12-24' AS DATE), CAST('2014-01-23' AS DATE)) as periods
ON "balance_transactions"."created" BETWEEN periods.start_date AND periods.end_date
WHERE "balance_transactions"."account_id" = 6 
AND (balance_transactions.type = 'charge' 
AND charges.refunded = false 
AND charges.invoice IS NOT NULL) 
GROUP BY periods.start_date, periods.end_date

以下链接到SQL Fiddle我测试过的地方: http://sqlfiddle.com/#!10/535ac/11/0

答案 8 :(得分:0)

我参与了以下代码。它使用XML文件。该文件或字符串包含您需要sumarize的日期范围。存储过程将返回一个表格,其中包含每个特定范围的总计。

/*****************CREATES/POPULATES FAKE TABLES WITH A SIMILAR STRUCTURE TO THE ONE YOU ARE USING************/
DECLARE @balance_transactions TABLE(fee FLOAT,
                                    source INT,
                                    account_id INT,
                                    [type] VARCHAR(25),
                                    created DATETIME)

INSERT INTO @balance_transactions
SELECT 12.5, 1, 6, 'charge', '01/15/2012'
UNION
SELECT 70, 2, 6, 'charge', '01/16/2012'
UNION
SELECT 136.89, 3, 6, 'charge', '01/17/2012'
UNION
SELECT 29.16, 4, 6, 'charge', '01/18/2012'
UNION
SELECT 1369.54, 5, 6, 'charge', '02/21/2012'
UNION
SELECT 468.85, 6, 6, 'charge', '02/22/2012'
UNION
SELECT 65.8, 7, 6, 'charge', '02/22/2012'
UNION
SELECT 1236.87, 8, 6, 'charge', '02/22/2012'

DECLARE @charges TABLE(balance_id INT,
                       refunded BIT,
                       invoice INT)

INSERT INTO @charges
SELECT 1, 0, 7
UNION
SELECT 2, 0, 8
UNION
SELECT 3, 0, 9
UNION
SELECT 4, 0, 10
UNION
SELECT 5, 0, 11
UNION
SELECT 6, 0, 12
UNION
SELECT 7, 0, null
UNION
SELECT 8, 0, null
/*******************************************************/

/*
You can use the code below for creating an Stored Procedure.
The SP will return a table with the SUM of all those values indicating the Date Range.

spGetTotalsPerDateRange 'your xml goes here'

results:

fromDate                |toDate                     |total
2012-01-15 00:00:00.000 |2012-01-30 00:00:00.000    |248.55
2012-02-15 00:00:00.000 |2012-02-28 00:00:00.000    |3141.06
*/
SET DATEFORMAT MDY

DECLARE @XmlDocumentHandle int
DECLARE @XmlDocument nvarchar(4000)
SET @XmlDocument = 
N'<dates>
    <range>
       <fromDate>01/15/2012</fromDate>
       <toDate>01/30/2012</toDate>
    </range>
    <range>   
       <fromDate>02/15/2012</fromDate>
       <toDate>02/28/2012</toDate>
    </range>
</dates>'

EXEC sp_xml_preparedocument @XmlDocumentHandle OUTPUT, @XmlDocument

DECLARE @feeTotal TABLE(fromDate DATETIME,
                        toDate DATETIME,
                        total FLOAT)


DECLARE @fromDate DATETIME
DECLARE @toDate DATETIME
DECLARE ranges_cur CURSOR FOR

SELECT fromDate, toDate
FROM OPENXML (@XmlDocumentHandle, '/dates/range',2)
     WITH (fromDate  DATETIME,
           toDate DATETIME);

OPEN ranges_cur;
FETCH NEXT FROM ranges_cur INTO @fromDate, @toDate;
WHILE @@FETCH_STATUS = 0
   BEGIN
      INSERT INTO @feeTotal
      SELECT @fromDate, 
             @toDate,
             SUM(bt.fee)
      FROM @balance_transactions bt
      INNER JOIN @charges c ON bt.source = c.balance_id 
      WHERE bt.account_id = 6 
      AND (bt.type = 'charge' 
      AND c.refunded = 0 
      AND c.invoice IS NOT NULL)
      AND (bt.created >= @fromDate AND bt.created <= @toDate);

      FETCH NEXT FROM ranges_cur INTO @fromDate, @toDate;
   END;
CLOSE ranges_cur;
DEALLOCATE ranges_cur;

SELECT fromDate,
       toDate,
       total
FROM @feeTotal

EXEC sp_xml_removedocument @XmlDocumentHandle

GO

参数化查询将如下所示。

CREATE PROCEDURE spGetTotalsPerDateRange(@XmlDocument NVARCHAR(4000),
                                         @type VARCHAR(50) = 'charge',
                                         @refunded BIT = 0)
AS
BEGIN
    SET DATEFORMAT MDY

    DECLARE @XmlDocumentHandle INT
    EXEC sp_xml_preparedocument @XmlDocumentHandle OUTPUT, @XmlDocument

    DECLARE @feeTotal TABLE(fromDate DATETIME,
                            toDate DATETIME,
                            total FLOAT)


    DECLARE @fromDate DATETIME
    DECLARE @toDate DATETIME
    DECLARE ranges_cur CURSOR FOR

    SELECT fromDate, toDate
    FROM OPENXML (@XmlDocumentHandle, '/dates/range',2)
         WITH (fromDate  DATETIME,
               toDate DATETIME);

    OPEN ranges_cur;
    FETCH NEXT FROM ranges_cur INTO @fromDate, @toDate;
    WHILE @@FETCH_STATUS = 0
       BEGIN
          INSERT INTO @feeTotal
          SELECT @fromDate, 
                 @toDate,
                 SUM(bt.fee)
          FROM balance_transactions bt
          INNER JOIN charges c ON bt.source = c.balance_id 
          WHERE bt.account_id = 6 
          AND (bt.type = 'charge' 
          AND c.refunded = 0 
          AND c.invoice IS NOT NULL)
          AND (bt.created >= @fromDate AND bt.created <= @toDate);

          FETCH NEXT FROM ranges_cur INTO @fromDate, @toDate;
       END;
    CLOSE ranges_cur;
    DEALLOCATE ranges_cur;

    SELECT fromDate,
           toDate,
           total
    FROM @feeTotal

    EXEC sp_xml_removedocument @XmlDocumentHandle

END
GO

第一个代码是使用虚假数据测试的,它运行正常。您需要根据表定义对SP列和变量类型进行必要的类型和名称调整。

这种方法的想法是能够以您需要的方式报告任何信息。您还可以通过XML属性传递其他参数。在MSDN website

上查看更多OPEN XML信息

希望有所帮助

答案 9 :(得分:0)

我来自oracle PL / SQL背景。在我看来,您可以使用以下查询来使用它。我的理解是,如果任何交易发生在19-NOV-2013上午1点,总是会在11月19日到11月20日,所以我们不应该担心我在下面查询创建的范围。我希望它会帮助你。

SELECT DISTINCT account_id,
  TRUNC(created)
  ||' to '
  || (TRUNC(created)+1) period,
  SUM(FEE) over (partition BY account_id,TRUNC(created) ) sumid
FROM balance_transactions a,
  charges b
WHERE a.source =balance_id
AND b.refunded =0
AND b.invoice IS NOT NULL
AND a.type     ='charge'
ORDER BY TRUNC(created)
  ||' to '
  || (TRUNC(created)+1);