web-dev-qa-db-ja.com

特定の時点でのn個の燃料ステーションの現在の価格を見つける

価格情報が格納されているテーブルがあり、PostgreSQL9.5データベースに約1,300万行が格納されています。

_CREATE TABLE public.de_tt_priceinfo (
  id integer NOT NULL DEFAULT nextval('priceinfo_id_seq'::regclass),
  station_id character varying(60),
  recieved timestamp with time zone NOT NULL DEFAULT now(),
  e5 numeric(4,3),
  e10 numeric(4,3),
  diesel numeric(4,3),
  CONSTRAINT de_tt_priceinfo_id_pkey PRIMARY KEY (id)
);

CREATE INDEX de_tt_priceinfo_recieved_station_id_idx
  ON public.de_tt_priceinfo (recieved, station_id COLLATE pg_catalog."default");

CREATE INDEX index_station_id
  ON public.de_tt_priceinfo (station_id COLLATE pg_catalog."default");
_

このテーブルから、このテーブルをクエリする3,200万人の通勤者をシミュレートする必要があるため、特定の時点での最新の価格を最大のパフォーマンスで抽出する必要があります(一度にではなく、それでも)。

動作するクエリがあります!

_SELECT station_id, e5, e10, diesel, recieved FROM de_tt_priceinfo a 
WHERE a.recieved = (SELECT MAX(recieved) FROM de_tt_priceinfo b 
  WHERE a.station_id = b.station_id
  AND  recieved <= '2014-09-25 08:45:12'::TIMESTAMPTZ)
AND station_id IN('0C91A93A-a-b-c-d', '578C44BB-a-b-c-d', '6F2F48A8-a-b-c-d'
                , '9982BE74-a-b-c-d', 'A24C612B-a-b-c-d', 'BEC3EF55-a-b-c-d'
                , 'F5137488-a-b-c-d')
_

このクエリのパフォーマンスは使用できません。実行時間は約900ms変動します。結果は次のようになります

_0C91A93A-a-b-c-d, 1.xxx, 1.xxx, 1.xxx, "2014-09-25 08:17:50.000000"
578C44BB-a-b-c-d, 1.xxx, 1.xxx, 1.xxx, "2014-09-25 08:00:09.000000"
6F2F48A8-a-b-c-d, 1.xxx, 1.xxx, 1.xxx, "2014-09-25 07:08:57.000000"
9982BE74-a-b-c-d, 1.xxx, 1.xxx, 1.xxx, "2014-09-25 08:29:55.000000"
A24C612B-a-b-c-d, 1.xxx, 1.xxx, 1.xxx, "2014-09-25 08:00:09.000000"
BEC3EF55-a-b-c-d, 1.xxx, 1.xxx, 1.xxx, "2014-09-25 06:53:49.000000"
F5137488-a-b-c-d, 1.xxx, 1.xxx, 1.xxx, "2014-09-25 07:44:55.000000"
_

そこで、少し調べてみると、再帰的CTE、ルーズインデックススキャン、DBAに関するいくつかの回答など、非常に近いと思われる流行語が見つかりましたが、ニーズに合わせて変更することはできませんでした。

私が正しく理解していれば、再帰CTEは、必要なデータをクエリする最も簡単な方法です。

私がこれまでに得たものはこれです:

_WITH RECURSIVE cte AS (
   (
   SELECT station_id, e5, e10, diesel, recieved
   FROM   de_tt_priceinfo
   WHERE  recieved <= '2014-09-25 08:45:00'::TIMESTAMPTZ
     AND station_id IN('0C91A93A-a-b-c-d', '578C44BB-a-b-c-d', '6F2F48A8-a-b-c-d'
                     , '9982BE74-a-b-c-d', 'A24C612B-a-b-c-d', 'BEC3EF55-a-b-c-d'
                     , 'F5137488-a-b-c-d')
   ORDER  BY station_id, recieved DESC NULLS LAST
   LIMIT 1  
   )
   UNION ALL
   (
   SELECT u.station_id, u.e5, u.e10, u.diesel, u.recieved
   FROM   cte c
   JOIN de_tt_priceinfo u ON u.recieved > c.recieved   
   WHERE  u.recieved <= '2014-09-25 08:45:00'::TIMESTAMPTZ  -- repeat condition!
     AND u.station_id IN('0C91A93A-a-b-c-d', '578C44BB-a-b-c-d', '6F2F48A8-a-b-c-d'
                       , '9982BE74-a-b-c-d', 'A24C612B-a-b-c-d', 'BEC3EF55-a-b-c-d'
                       , 'F5137488-a-b-c-d')
   ORDER BY u.station_id, u.recieved DESC NULLS LAST LIMIT 1
   )
   )
SELECT * FROM cte;
_

しかし、これは次の2行を返すだけです。

_0C91A93A-a-b-c-d, 1.xxx, 1.xxx, 1.xxx, "2014-09-25 08:17:50.000000"
9982BE74-a-b-c-d, 1.xxx, 1.xxx, 1.xxx, "2014-09-25 08:29:55.000000"
_

更新:

  • SELECT Version(); PostgreSQL 9.5devel on x86_64-pc-linux-gnu、コンパイル済みx86_64-pc-linux-gnu-gcc(Gentoo 4.8.3 p1.1、pie-0.5.9)4.8.3、64 -ビット
  • 説明分析: http://explain.depesz.com/s/clrZ
  • XEON 1231v3、16 GB RAM、Samsung 840 PRO SSD
  • デフォルトのpostgresql.confへの変更
#接続
 listen_addresses = '*' 
 max_connections = 16 
 
#ロギング
 log_destination = 'csvlog' 
 log_directory = 'pg_log' 
 logging_collector = on 
 log_filename = 'postgres-%Y-%m-%d_%H%M%S.log' 
 log_rotation_age = 1d 
 log_rotation_size = 1GB 
 log_min_duration_statement = 500ms 
#log_checkpoints = on 
#log_connections = on 
#log_disconnections = on 
 log_lock_wait on 
#log_temp_files = 0 
 
#メモリ
 shared_buffers = 1GB 
 temp_buffers = 32MB 
 work_mem = 256MB 
 maintenance_work_mem = 1GB 
 effective_cache_size = 8GB 
 
#チェックポイント(ディスクに書き込むタイミング)
 wal_buffers = 16MB 
 checkpoint_completion_target = 0.9 
 checkpoint_timeout = 30min 
 checkpoint_segments = 32 
 
 random_page_cost = 1.1 
 
#インポートのみ!
#autovacuum = off 
 fsync = off 
 synchronous_commit = of f 
 full_page_writes = off 
1
Benjamin

注:交換しました recieved どこでもreceivedで。

インデックス

何よりもまず、クエリのタイプにとって、これははるかに優れたインデックスです。

CREATE INDEX de_tt_priceinfo_received_station_id_idx
  ON public.de_tt_priceinfo (station_id, received);  -- note the reversed order

組み合わせは一意であると想定されているため(私は推測します)、代わりに(station_id, receved)UNIQUE制約を提案します。

ALTER TABLE de_tt_priceinfo ADD CONSTRAINT de_tt_priceinfo_station_id_received
UNIQUE (station_id, received);

インデックスindex_station_idはほとんど置き換えられており、おそらく今は削除できます。
インデックスde_tt_priceinfo_received_station_id_idxはまだ使用されている可能性があります。

このすべての背後にあるロジックを必ず理解してください。

クエリ

基本的なDISTINCT ONクエリも検討します。

SELECT DISTINCT ON (station_id)
       station_id, e5, e10, diesel, received
FROM   de_tt_priceinfo
WHERE  received <= '2014-09-25 08:45:12'::TIMESTAMPTZ
AND    station_id = ANY ('{0C91A93A-a-b-c-d, 578C44BB-a-b-c-d, 6F2F48A8-a-b-c-d
                         , 9982BE74-a-b-c-d, A24C612B-a-b-c-d, BEC3EF55-a-b-c-d
                         , F5137488-a-b-c-d}'::varchar[])
ORDER BY station_id, received DESC;

しかし、ステーションごとにたくさんの行があるように見えるので、それは輝いていません。代わりに:

SELECT *
FROM  (
   VALUES
     ('0C91A93A-a-b-c-d'::varchar)
    , ('578C44BB-a-b-c-d')
    , ('6F2F48A8-a-b-c-d')
    , ('9982BE74-a-b-c-d')
    , ('A24C612B-a-b-c-d')
    , ('BEC3EF55-a-b-c-d')
    , ('F5137488-a-b-c-d')
   ) s(station_id)
LEFT JOIN LATERAL (
    SELECT e5, e10, diesel, received
    FROM   de_tt_priceinfo
    WHERE  station_id = s.station_id
    AND    received <= '2014-09-25 08:45:12'::TIMESTAMPTZ
    ORDER  BY received DESC
    LIMIT  1
   )  p ON TRUE

これは、上記のUNIQUE制約(または同等のインデックス)と組み合わせてダイナマイトにする必要があります。

詳細な説明:

テーブル定義

数百万行のテーブルの場合、簡単に可能な限りストレージを最適化するのにお金がかかります。すべてをより小さく、より速くします。

それが私がそれを設計する方法です:

CREATE TABLE station (
   station_id serial PRIMARY KEY
 , station    text
 , CHECK (length(station) < 61) -- ?? optional, you decide 
);

CREATE TABLE priceinfo (
   priceinfo_id serial PRIMARY KEY
 , station_id   integer NOT NULL REFERENCES station ON UPDATE CASCADE
 , received     timestamptz NOT NULL DEFAULT now(),
 , e5           integer  -- price in 0.1 Cent
 , e10          integer  -- price in 0.1 Cent
 , diesel       integer  -- price in 0.1 Cent
 , CONSTRAINT priceinfo_station_id_received UNIQUE (station_id, received)
);

CREATE INDEX priceinfo_received_idx ON public.priceinfo (received);

priceinfoの行サイズは60バイト(24ヒープタプルヘッダー+ nullビットマップ; 32バイトデータ; 4バイトアイテムポインタ)になります。94バイト(24 + 66 + 4)元のテーブル。これは、例のようにassuming16文字の文字列です。すべてが約36%小さく(またはそれ以上?)、かなり速くなります。

(station_id, received)の重要なインデックスは、32バイトではなく、インデックスタプルあたりのデータの8バイトになります。 /またはさらに多く(!)-それぞれプラスオーバーヘッド。さらに、station_idinteger番号の処理は、通常、その上にCOLLATIONが付いたテキストよりも高速です。

詳細:

クエリは最初にstationテーブルからstation_idをフェッチします。これは安価です。

価格は、0.1セントを意味するintegerの数値として保存されます。 (4バイト10バイトの代わりに元のnumeric(4,3) 0.1を掛けてセントを取得しますまたは0.001で表示用の€を取得します。非常にシンプルで高速です。

UUID

エラーメッセージの文字列はかなり長く見え、実際には通常のように見えます UUID 数値:

871828b4-37e5-419c-b7a5-cdbe1e1c0148

その場合は、uuidデータ型を使用してください。古いままにするという私のデザインを採用するかどうか。少なくともuuidデータ型に切り替えると、あらゆる面で全体的に大きな利益が得られます。

1