「最新の対応する行」を効率的に取得するにはどうすればよいですか?


53

非常に一般的なクエリパターンがありますが、効率的なクエリを作成する方法がわかりません。別のテーブルの行の「後ではなく最新の日付」に対応するテーブルの行を検索したい。

inventoryたとえば、特定の日に保有する在庫を表すテーブルがあります。

date       | good | quantity
------------------------------
2013-08-09 | egg  | 5
2013-08-09 | pear | 7
2013-08-02 | egg  | 1
2013-08-02 | pear | 2

そして、「価格」と言うテーブルは、特定の日に財の価格を保持します

date       | good | price
--------------------------
2013-08-07 | egg  | 120
2013-08-06 | pear | 200
2013-08-01 | egg  | 110
2013-07-30 | pear | 220

在庫表の各行の「最新」価格を効率的に取得するにはどうすればよいですか?

date       | pricing date | good | quantity | price
----------------------------------------------------
2013-08-09 | 2013-08-07   | egg  | 5        | 120
2013-08-09 | 2013-08-06   | pear | 7        | 200
2013-08-02 | 2013-08-01   | egg  | 1        | 110
2013-08-02 | 2013-07-30   | pear | 2        | 220

私はこれを行う1つの方法を知っています:

select inventory.date, max(price.date) as pricing_date, good
from inventory, price
where inventory.date >= price.date
and inventory.good = price.good
group by inventory.date, good

次に、このクエリをインベントリに再度結合します。大きなテーブルの場合、最初のクエリを(インベントリに再度結合することなく)実行することでも非常に遅くなります。ただし、プログラミング言語を使用してインベントリテーブルからmax(price.date) ... where price.date <= date_of_interest ... order by price.date desc limit 1クエリを1つずつ発行するだけで、同じ問題がすぐに解決されるdate_of_interestため、計算上の障害がないことがわかります。ただし、1つのSQLクエリで問題全体を解決することをお勧めします。クエリの結果に対してさらにSQL処理を行うことができるからです。

これを効率的に行う標準的な方法はありますか?頻繁に登場する必要があり、それに対する高速クエリを作成する方法があるはずだと感じています。

私はPostgresを使用していますが、SQLの一般的な答えをいただければ幸いです。


3
効率性の問題であるため、DBA.SEへの移行に投票しました。クエリをいくつかの異なる方法で作成することもできますが、それではそれほど高速にはなりません。
ypercubeᵀᴹ

5
単一のクエリから、すべての日にすべての商品が実際に必要ですか?ありそうもない要件のように思えますか?より一般的には、特定の日付の価格または特定の商品の価格(特定の日付)を取得します。これらの代替クエリは、(適切な)インデックスからはるかに簡単に恩恵を受けることができます。次のことも知っておく必要があります:カーディナリティ(各テーブルの行数)、完全なテーブル定義を含む。データ型、制約、インデックス、...(\d tblpsqlで使用)、Postgresおよびminのバージョン。/最大 財ごとの価格の数。
アーウィンブランドステッター

@ErwinBrandstetter答えを受け入れるように頼んでいますか?あなたが最も賛成票を持っているので、私はそれを受け入れてうれしいです。
トム・エリス

それがあなたの質問に答えるか、あなたのために働く場合のみ受け入れてください。それが関連するケースを助けることができるならば、あなたはあなたがどのように進んだかコメントを残すかもしれません。質問に回答がないと思われる場合は、お知らせください。
アーウィンブランドステッター

1
私はそれから謝罪しなければなりません、私は優れた答えと思われるものを受け取ったが、私はもはや質問を引き起こした問題に取り組んでいないので、私はどちらが最良の答えであるか、実際にそれらのいずれかを判断する場所がない私のユースケースに本当に適しています(以前のように)。この場合に従う必要があるDBA.Stackexchange ettequetteがある場合はお知らせください。
トム・エリス

回答:


42

それは非常に多くの依存状況と正確な要件に。質問への私のコメントを検討しください。

シンプルなソリューション

DISTINCT ONPostgresの中:

SELECT DISTINCT ON (i.good, i.the_date)
       i.the_date, p.the_date AS pricing_date, i.good, p.price
FROM   inventory  i
LEFT   JOIN price p ON i.good = p.good AND i.the_date >= p.the_date
ORDER  BY i.good, i.the_date, p.the_date DESC;

順序付けられた結果。

またはNOT EXISTS、標準SQLで(私が知っているすべてのRDBMSで動作します):

SELECT i.the_date, p.the_date AS pricing_date, i.good, i.quantity, p.price
FROM   inventory  i
LEFT   JOIN price p ON p.good = i.good AND p.the_date <= i.the_date
WHERE  NOT EXISTS (
   SELECT 1 FROM price p1
   WHERE  p1.good = p.good
   AND p1.the_date <= i.the_date
   AND p1.the_date >  p.the_date
   );

同じ結果ですが、任意のソート順で-を追加しない限りORDER BY
データの分布、正確な要件およびインデックスに応じて、これらのいずれかが高速になる場合があります。
一般にDISTINCT ON、勝者はその上にソートされた結果を取得します。しかし、場合によっては、他のクエリ手法は(はるかに)高速です。下記参照。

最大/最小値を計算するためのサブクエリを使用するソリューションは、一般に低速です。CTEを使用したバリアントは一般に低速です。

(別の回答で提案されたような)単純なビューは、Postgresでのパフォーマンスにはまったく役立ちません。

SQLフィドル。


適切なソリューション

文字列と照合

まず、テーブルのレイアウトが最適ではないことに悩まされます。些細なことのように思えるかもしれませんが、スキーマを正規化することは大いに役立ちます。

ソート文字タイプ(textvarchar、...) -ロケールに応じて行う必要がありますCOLLATION特にインチ ほとんどの場合、DBはローカルのルールセットを使用します(私の場合:などde_AT.UTF-8)。で調べる:

SHOW lc_collate;

これにより、ソートとインデックスの検索が遅くなります。文字列(商品の名前)が長いほど悪化します。出力の照合ルール(またはソート順)を実際に気にしない場合は、以下を追加するとより高速になりますCOLLATE "C"

SELECT DISTINCT ON (i.good COLLATE "C", i.the_date)
       i.the_date, p.the_date AS pricing_date, i.good, p.price
FROM   inventory  i
LEFT   JOIN price p ON i.good = p.good AND i.the_date >= p.the_date
ORDER  BY i.good COLLATE "C", i.the_date, p.the_date DESC;

照合を2つの場所に追加したことに注意してください。
私のテストでは、それぞれ2万行と非常に基本的な名前(「good123」)で2倍の速さです。

インデックス

クエリがインデックスを使用することになっている場合、文字データを含む列は一致する照合を使用する必要があります(good例)。

CREATE INDEX inventory_good_date_desc_collate_c_idx
ON price(good COLLATE "C", the_date DESC);

SOに関するこの関連する回答の最後の2つの章を必ずお読みください。

同じ列に異なる照合を持つ複数のインデックスを持つこともできます-他のクエリの別の(またはデフォルトの)照合に従ってソートされた商品も必要な場合。

ノーマライズ

冗長な文字列(良い名前)は、テーブルとインデックスを膨張させ、すべてをさらに遅くします。適切なテーブルレイアウトを使用すると、最初の問題のほとんどを回避できます。次のようになります。

CREATE TABLE good (
  good_id serial PRIMARY KEY
, good    text   NOT NULL
);

CREATE TABLE inventory (
  good_id  int  REFERENCES good (good_id)
, the_date date NOT NULL
, quantity int  NOT NULL
, PRIMARY KEY(good_id, the_date)
);

CREATE TABLE price (
  good_id  int     REFERENCES good (good_id)
, the_date date    NOT NULL
, price    numeric NOT NULL
, PRIMARY KEY(good_id, the_date));

主キーは、必要なすべてのインデックスを(ほぼ)自動的に提供します。
欠落している詳細に応じて、2列目の降順で複数列インデックスprice使用すると、パフォーマンスが向上する場合があります。

CREATE INDEX price_good_date_desc_idx ON price(good, the_date DESC);

この場合も、照合はクエリと一致する必要があります(上記を参照)。

Postgres 9.2以降では、特にテーブルに追加の列があり、テーブルがカバーインデックスよりも大幅に大きくなる場合、インデックスオンリースキャンの「インデックスのカバー」がさらに役立ちます。

これらの結果のクエリははるかに高速です。

存在しない

SELECT i.the_date, p.the_date AS pricing_date, g.good, i.quantity, p.price
FROM   inventory  i
JOIN   good       g USING (good_id)
LEFT   JOIN price p ON p.good_id = i.good_id AND p.the_date <= i.the_date
AND    NOT EXISTS (
   SELECT 1 FROM price p1
   WHERE  p1.good_id = p.good_id
   AND    p1.the_date <= i.the_date
   AND    p1.the_date >  p.the_date
   );

区別する

SELECT DISTINCT ON (i.the_date)
       i.the_date, p.the_date AS pricing_date, g.good, i.quantity, p.price
FROM   inventory  i
JOIN   good       g USING (good_id)
LEFT   JOIN price p ON p.good_id = i.good_id AND p.the_date <= i.the_date
ORDER  BY i.the_date, p.the_date DESC;

SQLフィドル。


より高速なソリューション

それでも十分に高速でない場合は、より高速なソリューションがある可能性があります。

再帰CTE / JOIN LATERAL/相関サブクエリ

特に、商品あたりの価格多いデータ分布の場合:

マテリアライズドビュー

これを頻繁かつ高速に実行する必要がある場合は、マテリアライズドビューを作成することをお勧めします。過去の日付の価格と在庫はめったに変わらないと仮定するのは安全だと思います。結果を一度計算し、スナップショットをマテリアライズドビューとして保存します。

Postgres 9.3+は、マテリアライズドビューの自動サポートを備えています。古いバージョンの基本バージョンを簡単に実装できます。


3
price_good_date_desc_idxあなたが劇的にお勧めのインデックスは私の類似したクエリのパフォーマンスを向上させました。私のクエリプランは、コスト削減42374.01..42374.86から0.00..37.12
cimmanon

@cimmanon:いいね!コアクエリ機能は何ですか?存在しない?DISTINCT ON?GROUP BY?
アーウィンブランドステッター

DISTINCT ONの使用
cimmanon

6

参考までに、mssql 2008を使用したため、Postgresには「include」インデックスがありません。ただし、以下に示す基本的なインデックス作成を使用すると、Postgresのハッシュ結合からマージ結合に変更されます。http//explain.depesz.com/s/eF6(インデックスなし) http://explain.depesz.com/s/j9x(結合基準のインデックス付き)

クエリを2つの部分に分けることをお勧めします。まず、在庫日付と価格設定日付の関係を表す他のさまざまなコンテキストで使用できるビュー(パフォーマンスの向上を目的としない)

create view mostrecent_pricing_dates_per_good as
select i.good,i.date i_date,max(p.date)p_date
  from inventory i
  join price p on i.good = p.good and i.date >= p.date
 group by i.good,i.date;

次に、照会(最近の価格設定日のない在庫を見つけるために左結合を使用するなど)の場合、クエリは他の種類の操作をより簡単かつ容易にすることができます。

select i.good
       ,i.date inventory_date
       ,i.quantity
       ,p.date pricing_date
       ,p.price       
  from inventory i
  join price p on i.good = p.good
  join mostrecent_pricing_dates_per_good x 
    on i.good = x.good 
   and p.date = x.p_date
   and i.date = x.i_date

これにより、次の実行計画が生成されます。http//sqlfiddle.com/#!3 / 24f23 / 1 インデックスなし

...すべてのスキャンを完全に並べ替えます。ハッシュ一致のパフォーマンスコストが総コストの大部分を占めることに注意してください。テーブルスキャンと並べ替えが遅いことがわかっています(目標と比較して:インデックスシーク)。

次に、基本的なインデックスを追加して、結合で使用される基準を支援します(これらは最適なインデックスであると主張しませんが、ポイントを示しています):http : //sqlfiddle.com/#!3 / 5ec75/1 基本的なインデックス付け

これは改善を示しています。ネストされたループ(内部結合)操作は、クエリに関連する総コストをもう消費しません。残りのコストは現在、インデックスシーク(すべてのインベントリ行​​をプルしているため、インベントリのスキャン)に分散されています。ただし、クエリは数量と価格を取得するため、さらに改善できます。そのデータを取得するには、結合基準を評価した後、検索を実行する必要があります。

最後の反復では、インデックスで「include」を使用して、計画が簡単にスライドし、追加要求されたデータをインデックス自体から簡単に取得できるようにします。したがって、ルックアップはなくなりました:http : //sqlfiddle.com/#!3 / 5f143/1 ここに画像の説明を入力してください

これで、クエリの合計コストが非常に高速なインデックスシーク操作に均等に分散されるクエリプランが作成されました。これは、get-as-it-getsに近くなります。確かに他の専門家はこれをさらに改善することができますが、このソリューションはいくつかの大きな懸念を解消します。

  1. データベース内にわかりやすいデータ構造を作成し、アプリケーションの他の領域で構成および再利用しやすくします。
  2. 最もコストのかかるクエリ演算子はすべて、いくつかの基本的なインデックス作成を使用してクエリプランから除外されています。

3
これは(SQL-Serverの場合)問題ありませんが、類似点はあるものの、異なるDBMS向けに最適化されているため、深刻な違いもあります。
ypercubeᵀᴹ

@ypercubeそれは本当です。Postgresに関する資格をいくつか追加しました。私の意図は、ここで説明した思考プロセスのほとんどが、DBMS固有の機能に関係なく適用されることでした。
ココゴリラ

答えは非常に深いので、試してみるには時間がかかります。私がどうやって乗るのか教えてあげましょう。
トム・エリス

5

PostgreSQL 9.3(本日リリース)をお持ちの場合は、LATERAL JOINを使用できます。

私はこれをテストする方法がなく、以前に使用したことはありませんが、ドキュメントからわかることから、構文は次のようになります:

SELECT  Inventory.Date,
        Inventory.Good,
        Inventory.Quantity,
        Price.Date,
        Price.Price
FROM    Inventory
        LATERAL
        (   SELECT  Date, Price
            FROM    Price
            WHERE   Price.Good = Inventory.Good
            AND     Price.Date <= Inventory.Date
            ORDER BY Price.Date DESC
            LIMIT 1
        ) p;

これは基本的にSQL-ServerのAPPLYと同等であり、デモ用にSQL-Fiddleでこれの実用例があります。


5

Erwinと他の人が述べたように、効率的なクエリは多くの変数に依存し、PostgreSQLはこれらの変数に基づいてクエリの実行を最適化しようと非常に努力します。一般的には、明確にするために最初に記述し、次にボトルネックを特定した後にパフォーマンスを修正する必要があります。

さらに、PostgreSQLには、物事をかなり効率的にするために使用できる多くのトリックがあります(1つの部分インデックス)。読み取り/書き込みの負荷に応じて、慎重にインデックスを作成することでこれを最適化できる場合があります。

最初に試すことは、ビューを作成して結合することです。

CREATE VIEW most_recent_rows AS
SELECT good, max(date) as max_date
FROM inventory
GROUP BY good;

これは、次のようなことを行うときにうまく機能するはずです。

SELECT price 
  FROM inventory i
  JOIN goods g ON i.goods = g.description
  JOIN most_recent_rows r ON i.goods = r.goods
 WHERE g.id = 123;

その後、それに参加できます。クエリは最終的に基になるテーブルに対してビューを結合しますが、一意のインデックスが(date、good in order)であると仮定すると、行っておくべきです(これは単純なキャッシュルックアップであるため)。これは、検索された数行で非常にうまく機能しますが、何百万もの商品の価格を消化しようとしている場合は非常に非効率的です。

2番目にできることは、インベントリテーブルにmost_recent bool列を追加し、

create unique index on inventory (good) where most_recent;

その後、トリガーを使用して、商品の新しい行が挿入されたときにmost_recentをfalseに設定します。これにより、複雑さが増し、バグが発生する可能性が高くなりますが、役立ちます。

繰り返しますが、これの多くは、適切なインデックスが存在することに依存しています。最新の日付クエリでは、おそらく日付にインデックスがあり、可能であれば日付で始まり、結合基準を含む複数列のインデックスが必要です。

以下のPer Erwinのコメントを更新してください、私はこれを誤解したようです。質問を読み直す私は、何が尋ねられているのか全くわかりません。アップデートで、私が目にする潜在的な問題は何か、なぜこれが不明確になるのかについて言及したいと思います。

提供されるデータベース設計には、ERPおよび会計システムでのIMEの実際の使用はありません。これは、特定の製品の特定の日に販売されたすべてのものが同じ価格であるという仮想の完全な価格設定モデルで機能します。ただし、常にそうであるとは限りません。通貨交換のようなものでさえありません(ただし、一部のモデルはそうするふりをしています)。これが人為的な例である場合、それは不明確です。それが実際の例である場合、データレベルでの設計に大きな問題があります。ここでは、これが実際の例であると仮定します。

日付だけで特定の商品の価格が指定されていると想定することはできません。すべてのビジネスの価格は、取引相手ごと、場合によっては取引ごとに交渉できます。このため、実際に在庫を実際に処理するテーブル(在庫テーブル)に価格保存する必要があります。このような場合、日付/商品/価格表は、交渉に基づいて変更される可能性のある基本価格を指定するだけです。そのような場合、この問題はレポートの問題から、トランザクションであり、一度に各テーブルの1つの行で動作する問題になります。たとえば、特定の日の特定の製品のデフォルト価格を次のように検索できます。

 SELECT price 
   FROM prices p
   JOIN goods g ON p.good = g.good
  WHERE g.id = 123 AND p."date" >= '2013-03-01'
  ORDER BY p."date" ASC LIMIT 1;

価格のインデックス(良い、日付)でこれはうまく機能します。

これは人為的な例であり、おそらくあなたが取り組んでいることに近い何かが役立つでしょう。


most_recentアプローチは、最も最近の価格のために働く必要があります絶対に。ただし、OPは各在庫日に対して最新の価格を必要とするようです。
アーウィンブランドステッター

いい視点ね。読み直しましたが、提案されたデータの実際の実用上の欠陥をいくつか見つけましたが、それが単なる不自然な例かどうかはわかりません。不自然な例として、何が欠けているのかわかりません。これを指摘するためのアップデートも適切でしょう。
クリストラヴァーズ

@ChrisTravers:これは不自然な例ですが、実際に作業しているスキーマを投稿することはできません。おそらく、あなたが発見した実際的な欠陥について少し言えるかもしれません。
トム・エリス

私はそれが正確である必要があるとは思わないが、problem話で失われている問題を心配している。少し近いものが役立つでしょう。問題は、価格設定では、特定の日の価格がデフォルトになる可能性が高いため、トランザクションエントリのデフォルトとしてのみレポートに使用することはないため、興味深いクエリは通常、時間。
クリストラバーズ

3

別の方法は、ウィンドウ関数を使用lead()してテーブル価格のすべての行の日付範囲を取得betweenし、在庫を結合するときに使用することです。実際にこれを実際に使用しましたが、これは主にこれがこれを解決するための最初のアイデアであったためです。

with cte as (
  select
    good,
    price,
    date,
    coalesce(lead(date) over(partition by good order by date) - 1
            ,Now()::date) as ndate
  from
    price
)

select * from inventory i join cte on
  (i.good = cte.good and i.date between cte.date and cte.ndate)

SqlFiddle


1

在庫から価格への結合を使用して、価格タブルプからの在庫を在庫日以前の在庫のみに制限し、最大日付を抽出し、日付がそのサブセットの最高日付である結合条件で

在庫価格について:

 Select i.date, p.Date pricingDate,
    i.good, quantity, price        
 from inventory I join price p 
    on p.good = i.good
        And p.Date = 
           (Select Max(Date from price
            where good = i.good
               and date <= i.Date)

特定の商品の価格が同じ日に複数回変更され、実際にこれらの列に日付のみがあり、時間がない場合、価格変更レコードの1つのみを選択するために、結合にさらに制限を適用する必要があります。


残念ながら、物事をスピードアップしていないようです。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.