3

次のようなアカウント テーブルを想像してください。

   Column   |            Type             | Modifiers 
------------+-----------------------------+-----------
 id         | bigint                      | not null
 signupdate | timestamp without time zone | not null
 canceldate | timestamp without time zone | 

申し込み数と解約数を月ごとにレポートしたい。

1 つは月ごとのサインアップ用、もう 1 つは月ごとのキャンセル用の 2 つのクエリで行うのは非常に簡単です。単一のクエリでそれを行う効率的な方法はありますか? サインアップとキャンセルがゼロの月もあり、結果にはゼロが表示されるはずです。

次のようなソース データを使用します。

id    signupDate     cancelDate
 1    2012-01-13     
 2    2012-01-15     2012-02-05
 3    2012-03-01     2012-03-20

次の結果が得られるはずです。

Date      signups    cancellations    
2012-01         2                0
2012-02         0                1
2012-03         1                1

私はpostgresql 9.0を使用しています

最初の回答後に更新します。

Craig Ringer は、以下の素晴らしい回答を提供してくれました。約 75,000 レコードのデータ セットでは、最初と 3 番目の例は同様に機能しました。2 番目の例はどこかにエラーがあるようで、間違った結果を返しました。

Explain Analyst の結果 (および私のテーブルには signup_date のインデックスがあります) を見ると、最初のクエリは次のように返されます。

Sort  (cost=2086062.39..2086062.89 rows=200 width=24) (actual time=863.831..863.833 rows=20 loops=1)
  Sort Key: m.m
  Sort Method:  quicksort  Memory: 26kB
  InitPlan 2 (returns $1)
    ->  Result  (cost=0.12..0.13 rows=1 width=0) (actual time=0.063..0.064 rows=1 loops=1)
          InitPlan 1 (returns $0)
            ->  Limit  (cost=0.00..0.12 rows=1 width=8) (actual time=0.040..0.040 rows=1 loops=1)
                  ->  Index Scan using account_created_idx on account  (cost=0.00..8986.92 rows=75759 width=8) (actual time=0.039..0.039 rows=1 loops=1)
                        Index Cond: (created IS NOT NULL)
  InitPlan 3 (returns $2)
    ->  Aggregate  (cost=2991.39..2991.40 rows=1 width=16) (actual time=37.108..37.108 rows=1 loops=1)
          ->  Seq Scan on account  (cost=0.00..2612.59 rows=75759 width=16) (actual time=0.008..14.102 rows=75759 loops=1)
  ->  HashAggregate  (cost=2083057.21..2083063.21 rows=200 width=24) (actual time=863.801..863.806 rows=20 loops=1)
        ->  Nested Loop  (cost=0.00..2077389.49 rows=755696 width=24) (actual time=37.238..805.333 rows=94685 loops=1)
              Join Filter: ((date_trunc('month'::text, a.created) = m.m) OR (date_trunc('month'::text, a.terminateddate) = m.m))
              ->  Function Scan on generate_series m  (cost=0.00..10.00 rows=1000 width=8) (actual time=37.193..37.197 rows=20 loops=1)
              ->  Materialize  (cost=0.00..3361.39 rows=75759 width=16) (actual time=0.004..11.916 rows=75759 loops=20)
                    ->  Seq Scan on account a  (cost=0.00..2612.59 rows=75759 width=16) (actual time=0.003..24.019 rows=75759 loops=1)
Total runtime: 872.183 ms

3 番目のクエリは次を返します。

Sort  (cost=1199951.68..1199952.18 rows=200 width=8) (actual time=732.354..732.355 rows=20 loops=1)
  Sort Key: m.m
  Sort Method:  quicksort  Memory: 26kB
  InitPlan 4 (returns $2)
    ->  Result  (cost=0.12..0.13 rows=1 width=0) (actual time=0.030..0.030 rows=1 loops=1)
          InitPlan 3 (returns $1)
            ->  Limit  (cost=0.00..0.12 rows=1 width=8) (actual time=0.022..0.022 rows=1 loops=1)
                  ->  Index Scan using account_created_idx on account  (cost=0.00..8986.92 rows=75759 width=8) (actual time=0.022..0.022 rows=1 loops=1)
                        Index Cond: (created IS NOT NULL)
  InitPlan 5 (returns $3)
    ->  Aggregate  (cost=2991.39..2991.40 rows=1 width=16) (actual time=30.212..30.212 rows=1 loops=1)
          ->  Seq Scan on account  (cost=0.00..2612.59 rows=75759 width=16) (actual time=0.004..8.276 rows=75759 loops=1)
  ->  HashAggregate  (cost=12.50..1196952.50 rows=200 width=8) (actual time=65.226..732.321 rows=20 loops=1)
        ->  Function Scan on generate_series m  (cost=0.00..10.00 rows=1000 width=8) (actual time=30.262..30.264 rows=20 loops=1)
        SubPlan 1
          ->  Aggregate  (cost=2992.34..2992.35 rows=1 width=8) (actual time=21.098..21.098 rows=1 loops=20)
                ->  Seq Scan on account  (cost=0.00..2991.39 rows=379 width=8) (actual time=0.265..20.720 rows=3788 loops=20)
                      Filter: (date_trunc('month'::text, created) = $0)
        SubPlan 2
          ->  Aggregate  (cost=2992.34..2992.35 rows=1 width=8) (actual time=13.994..13.994 rows=1 loops=20)
                ->  Seq Scan on account  (cost=0.00..2991.39 rows=379 width=8) (actual time=2.363..13.887 rows=998 loops=20)
                      Filter: (date_trunc('month'::text, terminateddate) = $0)
Total runtime: 732.487 ms

これにより、確かに 3 番目のクエリの方が高速であるように見えますが、「time」コマンドを使用してコマンド ラインからクエリを実行すると、最初のクエリは一貫して高速ですが、わずか数ミリ秒です。

驚いたことに、2 つの別々のクエリ (1 つはサインアップをカウントするクエリ、もう 1 つはキャンセルをカウントするクエリ) を実行すると、大幅に高速になります。実行にかかった時間は、約 300 ミリ秒と約 730 ミリ秒の半分以下でした。もちろん、外部で行う作業はさらに多くなりますが、私の目的にとっては、それでも最善の解決策かもしれません。単一のクエリは次のとおりです。

select 
    m,
    count(a.id) as "signups"
from
    generate_series(
        (SELECT date_trunc('month',min(signup_date)) FROM accounts), 
        (SELECT date_trunc('month',greatest(max(signup_date),max(cancel_date))) FROM accounts), 
        interval '1 month') as m
INNER JOIN accounts a ON (date_trunc('month',a.signup_date) = m)
group by m
order by m 
;

select 
    m,
    count(a.id) as "cancellations"
from
    generate_series(
        (SELECT date_trunc('month',min(signup_date)) FROM accounts), 
        (SELECT date_trunc('month',greatest(max(signup_date),max(cancel_date))) FROM accounts), 
        interval '1 month') as m
INNER JOIN accounts a ON (date_trunc('month',a.cancel_date) = m)
group by m
order by m 
;

私はクレイグの答えを正しいとマークしましたが、もしあなたがそれをより速くすることができれば、私はそれについて聞きたいです.

4

2 に答える 2

3

これを行うには、3 つの異なる方法があります。すべては、時系列を生成してからスキャンすることに依存しています。サブクエリを使用して、各月のデータを集計します。1 つは、異なる基準を持つシリーズに対してテーブルを 2 回結合します。別の形式では、時系列に対して 1 つの結合を実行し、開始日または終了日のいずれかに一致する行を保持してから、カウント内の述語を使用して結果をさらにフィルター処理します。

EXPLAIN ANALYZEデータに最適なアプローチを選択するのに役立ちます。

http://sqlfiddle.com/#!12/99c2a/9

テスト設定:

CREATE TABLE accounts
    ("id" int, "signup_date" timestamp, "cancel_date" timestamp);

INSERT INTO accounts
    ("id", "signup_date", "cancel_date")
VALUES
    (1, '2012-01-13 00:00:00', NULL),
    (2, '2012-01-15 00:00:00', '2012-02-05'),
    (3, '2012-03-01 00:00:00', '2012-03-20')
;

単一の結合とカウントのフィルター:

SELECT m, 
  count(nullif(date_trunc('month',a.signup_date) = m,'f')), 
  count(nullif(date_trunc('month',a.cancel_date) = m,'f'))
FROM generate_series(
  (SELECT date_trunc('month',min(signup_date)) FROM accounts),
  (SELECT date_trunc('month',greatest(max(signup_date),max(cancel_date))) FROM accounts),
  INTERVAL '1' MONTH
) AS m
INNER JOIN accounts a ON (date_trunc('month',a.signup_date) = m OR date_trunc('month',a.cancel_date) = m)
GROUP BY m
ORDER BY m;

accountsテーブルを 2 回結合することにより、次のようになります。

SELECT m, count(s.signup_date) AS n_signups, count(c.cancel_date) AS n_cancels 
FROM generate_series( 
  (SELECT date_trunc('month',min(signup_date)) FROM accounts),
  (SELECT date_trunc('month',greatest(max(signup_date),max(cancel_date))) FROM accounts),
  INTERVAL '1' MONTH
) AS m LEFT OUTER JOIN accounts s ON (date_trunc('month',s.signup_date) = m) LEFT OUTER JOIN accounts c ON (date_trunc('month',c.cancel_date) = m)
GROUP BY m
ORDER BY m;

または、サブクエリを使用して:

SELECT m, (
  SELECT count(signup_date) 
  FROM accounts 
  WHERE date_trunc('month',signup_date) = m
) AS n_signups, (
  SELECT count(signup_date)
  FROM accounts
  WHERE date_trunc('month',cancel_date) = m
)AS n_cancels 
FROM generate_series( 
  (SELECT date_trunc('month',min(signup_date)) FROM accounts),
  (SELECT date_trunc('month',greatest(max(signup_date),max(cancel_date))) FROM accounts),
  INTERVAL '1' MONTH
) AS m
GROUP BY m
ORDER BY m;
于 2012-11-20T00:54:49.660 に答える
1

更新後の新しい回答。

2 つの単純なクエリでより良い結果が得られることに、私は驚きません。場合によっては、そのようにする方が単純に効率的です。ただし、私の元の回答には、パフォーマンスに大きな影響を与える問題がありました。

Erwin は別の回答で、Pg は の日付で単純な b ツリー インデックスを使用できないため、範囲を使用する方がよいと正確に指摘しましたdate_trunc。式で作成されたインデックスを使用できますdate_trunc('month',colname)が、別の不要なインデックスの作成は避けたほうがよいでしょう。

範囲を使用するように単一のスキャンとフィルターのクエリを言い換えると、次のようになります。

SELECT m, 
  count(nullif(date_trunc('month',a.signup_date) = m,'f')), 
  count(nullif(date_trunc('month',a.cancel_date) = m,'f'))
FROM generate_series(
  (SELECT date_trunc('month',min(signup_date)) FROM accounts),
  (SELECT date_trunc('month',greatest(max(signup_date),max(cancel_date))) FROM accounts),
  INTERVAL '1' MONTH
) AS m
INNER JOIN accounts a ON (
  (a.signup_date >= m AND a.signup_date < m + INTERVAL '1' MONTH) 
  OR (a.cancel_date >= m AND a.cancel_date < m + INTERVAL '1' MONTH))
GROUP BY m
ORDER BY m;

インデックス化できない条件では避ける必要がないdate_truncので、結合条件で間隔範囲を使用するように変更しただけです。

元のクエリでは seq スキャンとマテリアライズが使用されていましたが、 と にインデックスがある場合、これはビットマップ インデックス スキャンを使用するようにsignup_dateなりcancel_dateました。

PostgreSQL 9.2 では、以下を追加することでパフォーマンスが向上する可能性があります。

CREATE INDEX account_signup_or_cancel ON accounts(signup_date,cancel_date);

そしておそらく:

CREATE INDEX account_signup_date_nonnull 
ON accounts(signup_date) WHERE (signup_date IS NOT NULL);

CREATE INDEX account_cancel_date_desc_nonnull 
ON accounts(cancel_date DESC) WHERE (cancel_date IS NOT NULL);

インデックスのみのスキャンを許可します。テストする実際のデータがなければ、堅実なインデックスの推奨を行うことは困難です。

または、インデックス可能なフィルター条件が改善されたサブクエリ ベースのアプローチ:

SELECT m, (
  SELECT count(signup_date) 
  FROM accounts 
  WHERE signup_date >= m AND signup_date < m + INTERVAL '1' MONTH
) AS n_signups, (
  SELECT count(cancel_date)
  FROM accounts
  WHERE cancel_date >= m AND cancel_date < m + INTERVAL '1' MONTH
) AS n_cancels 
FROM generate_series( 
  (SELECT date_trunc('month',min(signup_date)) FROM accounts),
  (SELECT date_trunc('month',greatest(max(signup_date),max(cancel_date))) FROM accounts),
  INTERVAL '1' MONTH
) AS m
GROUP BY m
ORDER BY m;

signup_datecancel_date、または以下の通常の b ツリー インデックスの恩恵を受けます。

CREATE INDEX account_signup_date_nonnull 
ON accounts(signup_date) WHERE (signup_date IS NOT NULL);

CREATE INDEX account_cancel_date_nonnull 
ON accounts(cancel_date) WHERE (cancel_date IS NOT NULL);

作成するすべてのインデックスはパフォーマンスに悪影響を及ぼしINSERTUPDATE他のインデックスやヘルプ データと競合してキャッシュ領域を奪うことに注意してください。大きな違いがあり、他のクエリに役立つインデックスのみを作成するようにしてください。

于 2012-11-21T00:03:37.800 に答える