異なる通貨での価格の効率的な比較


10

ユーザーが価格帯で商品を検索できるようにしたい。ユーザーは、製品で設定されている通貨に関係なく、任意の通貨(USD、EUR、GBP、JPYなど)を使用できる必要があります。したがって、製品価格は200USDであり、ユーザーが100EUR-200EURの価格の製品を検索した場合でも、ユーザーはそれを見つける可能性があります。それを速く効果的にするには?

これが私が今までやってきたことです。私は保存pricecurrency codeおよびcalculated_priceデフォルトの通貨であるユーロ(EUR)での価格です。

CREATE TABLE "products" (
  "id" serial,
  "price" numeric NOT NULL,
  "currency" char(3),
  "calculated_price" numeric NOT NULL,
  CONSTRAINT "products_id_pkey" PRIMARY KEY ("id")
);

CREATE TABLE "currencies" (
  "id" char(3) NOT NULL,
  "modified" timestamp NOT NULL,
  "is_default" boolean NOT NULL DEFAULT 'f',
  "value" numeric NOT NULL,       -- ratio additional to the default currency
  CONSTRAINT "currencies_id_pkey" PRIMARY KEY ("id")
);

INSERT INTO "currencies" (id, modified, is_default, value)
  VALUES
  ('EUR', '2012-05-17 11:38:45', 't', 1.0),
  ('USD', '2012-05-17 11:38:45', 'f', '1.2724'),
  ('GBP', '2012-05-17 11:38:45', 'f', '0.8005');

INSERT INTO "products" (price, currency, calculated_price)
  SELECT 200.0 AS price, 'USD' AS currency, (200.0 / value) AS calculated_price
    FROM "currencies" WHERE id = 'USD';

ユーザーが他の通貨(USDなど)で検索している場合、価格をEURで計算し、calculated_price列を検索します。

SELECT * FROM "products" WHERE calculated_price > 100.0 AND calculated_price < 200.0;

この方法では、価格を1回だけ計算するため、すべての行の実際の価格を計算する必要がないため、価格を非常に高速に比較できます。

悪いことにdefault_price、通貨レートが変更されているため、少なくとも毎日、すべての行のを再計算する必要があります。

これに対処するより良い方法はありますか?

他に賢い解決策はありませんか?たぶんいくつかの数式ですか?私はという考え持っているcalculated_priceいくつかの変数に対する比率であるXと、とき通貨の変更、我々は変数ことだけ更新しX、ではないcalculated_price私たちも、アップデートは何も(行)には必要ありませんので、...たぶんいくつかの数学者はそれを解決することができますがこのような?

回答:


4

calculated_price厳密な必要性とは対照的に、の再計算が最適化である別のアプローチがあります。

であるとしcurrenciesたテーブル、あなたが別の列、追加last_rate時の為替レートが含まれ、calculated_price最後に更新されましたが、これは関係なく起こったとき。

たとえば、50 USDから100 USDまでの価格で、目的の結果を含む一連の製品をすばやく取得するに、次のようにします。

  SELECT * FROM products
   WHERE calculated_price > 50.0/(:last_rate*
    (SELECT coalesce(max(value/last_rate),1) FROM currencies
      WHERE value>last_rate))
   AND calculated_price < 100.0/ (:last_rate*
    (SELECT coalesce(min(value/last_rate),1) FROM currencies
      WHERE value<last_rate))

:last_rateは、最終更新時のEUR / USD為替レートが含まれています。アイデアは、すべての通貨の最大変動を考慮に入れるために間隔を増やすことです。間隔の両端の増加係数は、レートの更新間で一定であるため、事前に計算できます。

レートは短期間にわずかしか変化しないため、上記のクエリは最終結果の近似値を提供する可能性があります。最終結果を取得するために、の最後の更新以降のレートの変化により、価格が範囲外になっている製品を除外しますcalculated_price

  WITH p AS (
   SELECT * FROM products
   WHERE calculated_price > 50.0/(:last_rate*
    (SELECT coalesce(max(value/last_rate),1) FROM currencies
      WHERE value>last_rate))
   AND calculated_price < 100.0/ (:last_rate*
    (SELECT coalesce(min(value/last_rate),1) FROM currencies
      WHERE value<last_rate))
  )
  SELECT price,c.value FROM p join currencies c on (p.currency=c.id)
     WHERE price/c.value>50/:current_rate
       AND price/c.value<100/:current_rate;

ここで:current_rate、ユーザーが選択した金額に対するEURを使用した最新のレートです。

効率は、レートの範囲が小さく、値が互いに近いと想定されているという事実から生じます。


2

これはマテリアライズドビューの仕事のように聞こえます。PostgreSQLはそれらを明示的にサポートしていませんが、通常のテーブルの関数とトリガーを使用して、マテリアライズドビューを作成および維持できます。

私は...するだろう:

  • products_summary現在のproductsテーブルのスキーマを使用して、新しいテーブルを作成します。
  • ALTER TABLE products DROP COLUMN calculated_pricecalculated_price列を取り除くためにproducts
  • あなたがしたい出力を生成ビュー書くproducts_summaryことでSELECTからINGのproductsJOIN上のINGのをcurrencies。私はそれを呼び出すと思いますproducts_summary_dynamicが、命名はあなた次第です。必要に応じて、ビューの代わりに関数を使用できます。
  • 定期的にマテリアライズド・ビュー・テーブルを更新するproducts_summaryからproducts_summary_dynamicBEGIN; TRUNCATE products_summary; INSERT INTO products_summary SELECT * FROM products_summary_dynamic; COMMIT;
  • 作成しAFTER INSERT OR UPDATE OR DELETE ON products維持するために、トリガプロシージャを実行するトリガーproducts_summaryから削除されたときに行を削除、テーブルproductsに追加するときにそれらを添加すること、products(BY SELECTからINGのproducts_summary_dynamic図)、及び生成物が変化を詳細ときにそれらを更新します。

このアプローチでは、サマリーテーブルを更新するトランザクションproducts_summary中に排他ロックがかかりTRUNCATE ..; INSERT ...;ます。時間がかかるためにアプリケーションが停止する場合は、代わりに2つのバージョンのproducts_summaryテーブルを保持できます。使用されていないものを更新してから、トランザクションでALTER TABLE products_summary RENAME TO products_summary_old; ALTER TABLE products_summary_new RENAME TO products_summary;


別のしかし非常に危険なアプローチは、式インデックスを使用することです。このアプローチに通貨テーブルを更新する可能性があるため避けられない時にロックを必要とDROP INDEXし、CREATE INDEX私があまりにも頻繁にそれをしないだろう-それはいくつかの状況に適しているかもしれません。

アイデアは、通貨換算をIMMUTABLE関数にまとめることです。それはですので、IMMUTABLEあなたが任意の引数の戻り値は常に同じであることをデータベースエンジンに保証し、そしてそれは非常識なものであれば、戻り値が異なるのすべてのソートを行うには自由だということです。たとえば、関数を呼び出しto_euros(amount numeric, currency char(3)) returns numericます。好きなように実装してください。CASE通貨ごとの大きな声明、ルックアップテーブルなど。ルックアップテーブルを使用する場合、以下で説明する場合を除いて、ルックアップテーブルを変更してはなりません

次のproductsように、式インデックスをに作成します。

CREATE INDEX products_calculated_price_idx
ON products( to_euros(price,currency) );

計算された価格で製品をすばやく検索できるようになりました。例:

SELECT *
FROM products
WHERE to_euros(price,currency) BETWEEN $1 and $2;

ここで問題は、通貨テー​​ブルを更新する方法になります。ここでの秘訣は、通貨テー​​ブルを変更できることです。それを行うには、インデックスを削除して再作成するだけです。

BEGIN;

-- An exclusive lock will be held from here until commit:
DROP INDEX products_calculated_price_idx;
DROP FUNCTION to_euros(amount numeric, currency char(3)) CASCADE;

-- It's probably better to use a big CASE statement here
-- rather than selecting from the `currencies` table as shown.
-- You could dynamically regenerate the function with PL/PgSQL
-- `EXECUTE` if you really wanted.
--
CREATE FUNCTION to_euros(amount numeric, currency char(3))
RETURNS numeric LANGUAGE sql AS $$
SELECT $1 / value FROM currencies WHERE id = $2;
$$ IMMUTABLE;

-- This may take some time and will run with the exclusive lock
-- held.
CREATE INDEX products_calculated_price_idx
ON products( to_euros(price,currency) );

COMMIT;

上記の関数を削除して再定義するのは、不変の関数を再定義する場合は、その関数を使用するすべてのものを削除する必要があることを強調するためです。これを行うには、ドロップを使用するのが最良の方法です。CASCADE

マテリアライズドビューの方が良いアプローチだと私は強く思います。それは確かに安全です。これは主にキック用に含めています。


現在、私はこれについて考えています-なぜ更新する必要があるのcalculated_priceですか?私はinitial_currency_value(実際に使用されている一定の通貨レート)を保存し、それに対して常に計算することができます!もちろん、ユーロで価格を表示する場合は、実際の通貨レートに対して計算してください。私は正しいですか?または、私が見ない問題がありますか?
Taai

1

私は自分のアイデアを思いつきました。実際に機能するかどうか教えてください!

問題。

製品がproductsテーブルに追加されると、価格はデフォルトの通貨(EUR)に変換され、calculated_price列に格納されます。

ユーザーが任意の通貨の価格を検索(フィルター)できるようにする必要があります。これは、入力価格をデフォルトの通貨(EUR)に変換し、それをcalculated_price列と比較することによって行われます。

ユーザーが新しい通貨レートで検索できるように、通貨レートを更新する必要があります。しかし問題は- calculated_price効率的にアップデートする方法です。

ソリューション(うまくいけば)。

calculated_price効率的に更新する方法。

やめて!:)

考え方は、昨日の通貨レート(すべて同じ日付)をcalculated_price使用しそれらのみを使用することです。のような...永遠に!毎日の更新はありません。価格を比較/フィルタリング/検索する前に必要なのは、昨日のように今日の通貨レートを取得することだけです。

したがって、ここcalculated_priceでは固定日の通貨レートのみを使用します(昨日としましょう)。今日の価格を昨日の価格に変換する必要があります。言い換えれば、今日のレートを取り、それを昨日のレートに変換します。

cash_in_euros * ( rate_newest / rate_fixed )

そしてこれは通貨の表です:

CREATE TABLE "currencies" (
  "id" char(3) NOT NULL, -- currency code (EUR, USD, GBP, ...)
  "is_default" boolean NOT NULL DEFAULT 'f',

  -- Set once. If you update, update all database fields that depends on this.
  "rate_fixed" numeric NOT NULL, -- Currency rate against default currency
  "rate_fixed_updated" timestamp NOT NULL,

  -- Update as frequently as needed.
  "rate_newest" numeric NOT NULL, -- Currency rate against default currency
  "rate_newest_updated" timestamp NOT NULL,

  CONSTRAINT "currencies_id_pkey" PRIMARY KEY ("id")
);

これは、200 USDのコストの製品を追加する方法と、calculated_price取得の計算方法です。USDから最新のEURレートおよび固定(古い)レートへ

INSERT INTO "products" (price, currency, calculated_price)
  SELECT
  200.0 AS price,
  'USD' AS currency,

  ((200.0 / rate_newest) * (rate_newest / rate_fixed)) AS calculated_price

    FROM "currencies" WHERE id = 'USD';

これはクライアント側でも事前に計算できcalculated_priceます。それを行うのは、クエリを実行する前にユーザー入力価格を互換性のある値に計算するため、古き良きものが使用されますSELECT * FROM products WHERE calculated_price > 100.0 AND calculated_price < 200.0;

結論。

このアイデアはほんの数時間前に思いついたのですが、現在、この解決策について私が正しいかどうかを確認するようにお願いしています。どう思いますか?これはうまくいくのでしょうか?または私は間違っていますか?

ご理解いただければ幸いです。私は英語を母国語とする人ではありません。また、遅くて疲れています。:)

更新

まあ、それは1つの問題を解決するように見えますが、別の問題をもたらします。残念な。:)


問題は、rate_newest / rate_fixed通貨によって通貨が異なることであり、このソリューションでは、ユーザーが検索で選択した金額の通貨のみが考慮されます。別の通貨での価格は、最新のレートと比較されません。どういうわけか私が提出した答えは同様の問題がありましたが、私は更新バージョンでそれを修正したと思います。
DanielVérité12年

そのアプローチで私が目にする主な問題は、価格のデータベースインデックスを利用しないことです(ORDER BYで計算された価格句)。
ローゼンフェルド2016年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.