web-dev-qa-db-ja.com

1日中すべての時間を実行するクエリを最適化する

クエリの最適化についてサポートが必要です。現在PostgreSQL 9.3.4を使用していますが、必要に応じて9.4にアップグレードできます。

次のようなレコードが6,000万以上あるテーブルがあります。

                                                          Table "public.snapshots"
   Column   |           Type           |                         Modifiers                          | Storage  | Stats target | Description 
------------+--------------------------+------------------------------------------------------------+----------+--------------+-------------
 id         | integer                  | not null default nextval('snapshots_new_id_seq'::regclass) | plain    |              | 
 camera_id  | integer                  | not null                                                   | plain    |              | 
 created_at | timestamp with time zone | not null                                                   | plain    |              | 
 notes      | text                     |                                                            | extended |              | 
 data       | bytea                    | not null                                                   | extended |              | 
 is_public  | boolean                  | not null default false                                     | plain    |              | 
Indexes:
    "snapshots_new_created_at_camera_id_index" UNIQUE, btree (created_at, camera_id)
Foreign-key constraints:
    "snapshots_new_camera_id_fkey" FOREIGN KEY (camera_id) REFERENCES cameras(id) ON DELETE CASCADE
Has OIDs: no

1時間に0〜3600のスナップショットレコードが存在する可能性があります。このクエリでは、特定のcamera_idの特定の日のどの時間に1つ以上のスナップショットレコードがあるかを知ることにのみ興味があります(実際の数は重要ではありません)。

現在、アプリケーションは次のように1日の1時間ごとに1つのクエリを実行するように実装されています。

SELECT count(*) AS "count" FROM "snapshots" WHERE (("snapshots"."camera_id" = 4809) AND ("created_at" >= '2015-05-24 23:00:00 UTC') AND ("created_at" <= '2015-05-24 23:59:59 UTC'));
SELECT count(*) AS "count" FROM "snapshots" WHERE (("snapshots"."camera_id" = 4809) AND ("created_at" >= '2015-05-25 00:00:00 UTC') AND ("created_at" <= '2015-05-25 00:59:59 UTC'));
...
SELECT count(*) AS "count" FROM "snapshots" WHERE (("snapshots"."camera_id" = 4809) AND ("created_at" >= '2015-05-25 22:00:00 UTC') AND ("created_at" <= '2015-05-25 22:59:59 UTC'));

1つのクエリに対する分析の説明: http://explain.depesz.com/s/9tbP

私はこれを最適化してみましたが、私が必要としているクエリのようになりました。

SELECT count(*) AS "count" FROM "snapshots" WHERE (("created_at" > '2015-05-06 23:00:00.000000+0000') AND ("created_at" < '2015-05-08 23:00:00.000000+0000')) GROUP BY date_trunc('hour', created_at);

このクエリの分析の説明: http://explain.depesz.com/s/cVUK

しかし、これは実際には、上記の24個のクエリを組み合わせた場合よりも10倍遅いです。

何が悪いのですか?実際のカウントに関心がないので、COUNTを忘れる必要がありますか?クエリはどのように見えますか?

編集:すべてのコメントと回答をありがとう、私はあなたから複数のことを学びました! 3つの答えすべてを受け入れることができるといいのですが、ypercubeを選択したのは、それが最も効率的で柔軟なためです。

3
Milos

@ Akashの回答 のバリエーション。これはLATERAL構文を使用し、より良い実行計画をもたらします(Index Only Scan using idx2_snapshots on snapshots以下のプランで):

SELECT 
      hour AS start_hour,
      hour + interval '1 hour' AS end_hour
FROM 
      generate_series('2015-01-01'::timestamp, 
                      '2015-01-02 23:00:00'::timestamp, 
                      '1 hour') AS hour 
    , LATERAL
        ( SELECT 1 
          FROM snapshots 
          WHERE camera_id = 3 
            AND created_at >= hour 
            AND created_at < hour + interval '1 hour' 
          LIMIT 1 
        ) AS x ;

SQLfiddle (クエリ2)でテストされています。計画:

Nested Loop (cost=0.43..4083.77 rows=1000 width=8)
-> Function Scan on generate_series hour (cost=0.00..10.00 rows=1000 width=8)
-> Limit (cost=0.43..4.05 rows=1 width=0)
-> Index Only Scan using idx2_snapshots on snapshots (cost=0.43..7740.98 rows=2139 width=0)
Index Cond: ((camera_id = 3) AND (created_at >= hour.hour) AND (created_at < (hour.hour + '01:00:00'::interval)))
1
ypercubeᵀᴹ

ただのアイデア:1日のすべての時間(0〜23)を含むテーブルを作成します。

create table hours(
  hr integer
);

次に、指定されたcamera_idと日付のスナップショットがあるすべての時間を検索します(もちろん、クエリに独自のcamera_idと日付を代入する必要があります)。

select h.hr, 1 as camera_id
from hours h
where exists (
    select 1 
    from snapshots s
    where s.camera_id = 1
    and s.created_at between to_timestamp ('2015-05-01 ' || to_char(h.hr, '00') || ':00:00', 'YYYY-MM-DD HH24:MI:SS')
                             and to_timestamp ('2015-05-01 ' || to_char(h.hr, '00') || ':59:59', 'YYYY-MM-DD HH24:MI:SS')
)

SQLFiddle でこれを参照してください。テーブルからcount(*)を返すには、データベースは条件を満たす行をすべて調べてそれらをカウントする必要があります。 exists()はこれを、条件を満たす最初の行の検索に制限する必要があります。

3
zgguy

データベースの小さな sample を作成してみましたが、おそらくプランナーにsemi-joinネストされたループを強制的に使用して、処理を停止する必要がありますzugguyが提案したように、特定のhour, camera_idの最初の行が見つかるとすぐに、さらに行を追加します。

generate_seriesを使用して一時テーブルを回避し、特定の日時の時間を取得できます。

SELECT 
  start_hour,
  end_hour
FROM
  (SELECT 
      hour as start_hour,
      hour + interval '1 hour' as end_hour
   FROM 
      generate_series('2015-01-01'::timestamp, '2015-01-02 23:59:59'::timestamp, '1 hour') hour 
  ) as hours
WHERE
  EXISTS( SELECT 
            1 
          FROM 
          snapshots 
          WHERE
            camera_id = 3 
            AND created_at >= start_hour and created_at < end_hour )

Plan 。サンプルデータベースでは20 msを使用します。 通知index内の列のシーケンスが主要な要素です。私は最初にcamera_idを配置し、次にcreated_atを合計なしと仮定して配置しました。 create_atの個別の値の数は、合計値よりはるかに大きくなります。 of camera_idsおよび特定のカメラのすべての時間をインデックス内で互いに近くにしたいのは、それが制限要因であるためです。

3
Akash