web-dev-qa-db-ja.com

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

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

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
_

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

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

私はPostgresを使用していますが、SQL汎用の回答をいただければ幸いです。

59
Tom Ellis

それは状況と正確な要件に大きく依存します質問に対する私のコメント を検討してください。

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

Postgresで DISTINCT ON を使用する場合:

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;

注文結果。

または、標準SQLで NOT EXISTS を使用します(私が知っているすべての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、...) によるソートは、ロケールに従って実行する必要があります- [〜#〜]照合[〜#〜] 特に。ほとんどの場合、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倍の速さで、それぞれ20k行と非常に基本的な名前( 'good123')を使用しています。

インデックス

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

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

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

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

ノーマライズ

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

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);

繰り返しますが、collat​​ionはクエリと一致する必要があります(上記を参照)。

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 /相関サブクエリ

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

マテリアライズドビュー

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

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

50

ちなみに、私は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 no indexing

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

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

これは改善を示しています。ネストされたループ(内部結合)操作は、クエリに関連する総コストを消費しなくなりました。残りのコストは、インデックスシークに分散されます(すべての在庫行を取得しているため、在庫のスキャン)。しかし、クエリは数量と価格を取得するので、私たちはより良いことができます。そのデータを取得するには、結合基準を評価した後、ルックアップを実行する必要があります。

最後のイテレーションでは、インデックスに "include"を使用して、プランが簡単にスライドし、追加で要求されたデータをインデックス自体からすぐに取得できるようにします。したがって、ルックアップはなくなりました: http://sqlfiddle.com/#!3/5f143/1 enter image description here

これで、クエリの総コストが非常に高速なインデックスシーク操作に均等に分散されるクエリプランが作成されました。これは、すぐに使えるものに近いでしょう。確かに他の専門家はこれをさらに改善できますが、解決策はいくつかの主要な懸念を解消します:

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

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
GarethD

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

さらに、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 この順序で)に一意のインデックスがあるとすると、これで十分です(これは簡単なのでキャッシュルックアップ)。これは、いくつかの行をルックアップしても非常にうまく機能しますが、何百万もの商品の価格を消化しようとすると非常に非効率になります。

2番目にできることは、在庫テーブルにmost_recent bool列を追加し、

create unique index on inventory (good) where most_recent;

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

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

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

提供されているデータベース設計には、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;

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

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

5
Chris Travers

もう1つの方法は、ウィンドウ関数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

3
Tomas Greif

在庫から価格への結合を使用して、価格表からのレコードを在庫日以前のレコードのみに制限し、最大日付を抽出し、日付がそのサブセットからの最も高い日付である結合条件を使用します

したがって、あなたの在庫価格について:

 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つだけを選択する必要がある場合があります。

1
Charles Bretana