有效地比较不同货币的价格

Taa*_*aai 10 postgresql money

我想让用户可以在价格范围内搜索产品。用户应该能够使用任何货币(美元、欧元、英镑、日元、...),无论产品设置什么货币。因此,产品价格为 200USD,如果用户搜索价格为 100EUR - 200EUR 的产品,他仍然可能找到它。如何让它快速有效?

这是我到目前为止所做的。我保存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';
Run Code Online (Sandbox Code Playgroud)

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

SELECT * FROM "products" WHERE calculated_price > 100.0 AND calculated_price < 200.0;
Run Code Online (Sandbox Code Playgroud)

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

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

有没有更好的方法来处理这个问题?

就没有其他聪明的解决办法吗?也许一些数学公式?我有一个想法,即calculated_price是与某个变量的比率X,当货币发生变化时,我们只更新那个变量X,而不是calculated_price,所以我们甚至不需要更新任何东西(行)......也许一些数学家可以解决它像这样?

Cra*_*ger 3

这听起来像是物化视图的工作。虽然 PostgreSQL 没有明确支持它们,但您可以使用普通表上的函数和触发器来创建和维护物化视图。

我会:

  • products_summary使用当前表的架构创建一个新表,例如products
  • ALTER TABLE products DROP COLUMN calculated_price删除calculated_price其中的列products
  • products_summary编写一个视图,通过SELECTing fromproductsJOINing on生成您想要的输出currencies。我会这样称呼它products_summary_dynamic,但命名由你决定。如果需要,您可以使用函数而不是视图。
  • products_summary定期products_summary_dynamic刷新物化视图表BEGIN; 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(通过SELECTproducts_summary_dynamic视图中添加),并在产品详细信息更改时更新它们。

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


另一种但非常狡猾的方法是使用表达式索引。因为用这种方法更新货币表可能不可避免地需要在 a 期间锁定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) );
Run Code Online (Sandbox Code Playgroud)

您现在可以通过计算的价格快速搜索产品,例如:

SELECT *
FROM products
WHERE to_euros(price,currency) BETWEEN $1 and $2;
Run Code Online (Sandbox Code Playgroud)

现在的问题是如何更新货币表。这里的技巧是,您可以更改货币表,只需删除并重新创建索引即可。

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;
Run Code Online (Sandbox Code Playgroud)

我删除并重新定义上面的函数只是为了强调,如果您重新定义不可变函数,则必须删除使用该函数的所有内容。使用CASCADE水滴是最好的方法。

我强烈怀疑物化视图是更好的方法。这当然更安全。我加入这个主要是为了好玩。