高效比较不同货币的价格


10

我想让用户可以在一个价格范围内搜索产品。无论产品设置哪种货币,用户都应该能够使用任何货币(美元,欧元,英镑,日元,...)。因此,产品价格为200美元,并且,如果用户搜索价格在100欧元至200欧元之间的产品,他仍然可以找到它。如何使其快速有效?

这是我到目前为止所做的。我保存pricecurrency codecalculated_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';

如果用户使用其他货币搜索,例如说美元,我们以欧元计算价格并搜索calculated_price列。

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

这样,我们可以非常快速地比较价格,因为我们不需要计算每一行的实际价格,因为它只计算一次。

坏事是,至少每天我们都必须重新计算default_price所有行的,因为货币汇率已更改。

有没有更好的方法来解决这个问题?

还没有其他聪明的解决方案吗?也许一些数学公式?我的想法calculated_price是相对于某个变量的比率,X并且当货币变化时,我们仅更新该变量X,而不更新calculated_price,因此我们甚至不需要更新任何内容(行)...也许某些数学家可以解决它像这样?

Answers:


4

这是一种不同的方法,对于它来说,重新计算calculated_price只是一种优化,而不是绝对必要的。

假设在currencies表中添加了另一列,last_rate其中包含了calculated_price上次更新时的汇率,无论何时发生。

要快速检索价格点在50美元到100美元之间的包含所需结果的一组产品,您可以执行以下操作:

  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_price摆脱中的calculated_price专栏products
  • 写产生你想要的输出视图products_summary通过SELECT从荷兰国际集团productsJOIN荷兰国际集团上currencies。我会打电话products_summary_dynamic给您,但命名取决于您。如果需要,可以使用函数而不是视图。
  • 定期刷新物化视图表products_summaryproducts_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(通过SELECT从ING的products_summary_dynamic视图),并更新它们当产品的细节的变化。

products_summaryTRUNCATE ..; INSERT ...;更新摘要表的事务期间,此方法将排他锁定。如果由于花费太长时间而导致应用程序停顿,则可以保留该products_summary表的两个版本。更新未使用的,然后在事务中更新ALTER TABLE products_summary RENAME TO products_summary_old; ALTER TABLE products_summary_new RENAME TO products_summary;


另一种可替代但非常狡猾的方法是使用表达式索引。因为用这种方法更新货币表可能不可避免地需要在a期间加锁,DROP INDEXCREATE 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(假设今天使用的恒定汇率),并始终以此为基础进行计算!当然,以欧元显示价格时,请根据实际汇率计算。我对吗?还是有我看不到的问题?
塔艾

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美元的产品的方法,以及如何calculated_price计算收益的方法:从美元到最新的欧元汇率,再到固定(旧)汇率

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;

结论。

这个想法是在几个小时前出现的,目前,我正在请您检查我是否对这种解决方案正确。你怎么看?这行得通吗?还是我弄错了?

我希望你能理解所有这些。我不是英语母语人士,也来晚了,我很累。:)

更新

好吧,看来它解决了一个问题,但又引入了另一个问题。太糟糕了。:)


问题是rate_newest / rate_fixed每种货币的货币是不同的,并且此解决方案仅考虑一种供用户选择的搜索货币。任何以其他货币表示的价格都不会与最新汇率进行比较。我以某种方式提交的答案存在类似的问题,但我认为我已在更新的版本中对其进行了修复。
DanielVérité2012年

我用这种方法看到的主要问题是它没有利用价格上的数据库索引(ORDER BY created_price子句)。
罗森菲尔德
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.