Эффективно сравнивая цены в разных валютах

10

Я хочу, чтобы пользователь мог искать товары в ценовом диапазоне. Пользователь должен иметь возможность использовать любую валюту (USD, EUR, GBP, JPY, ...) независимо от того, какая валюта установлена ​​продуктом. Таким образом, цена продукта составляет 200 долларов США, и, если пользователь ищет продукты стоимостью 100 - 200 евро, он все равно может их найти. Как сделать это быстро и эффективно?

Вот что я сделал до сих пор. Я храню price, currency 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, мы рассчитываем цену в евро и ищем calculated_priceстолбец.

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

Таким образом, мы можем сравнивать цены очень быстро, потому что нам не нужно вычислять фактическую цену для каждой строки, потому что она рассчитывается один раз.

Плохо то, что, по крайней мере, каждый день мы должны пересчитывать default_priceдля всех строк, потому что курсы валют были изменены.

Есть ли лучший способ справиться с этим?

Нет ли другого умного решения? Может быть, какая-то математическая формула? У меня есть идея, что calculated_priceэто отношение к некоторой переменной, Xи, когда валюта меняется, мы обновляем только эту переменную X, а не calculated_price, поэтому нам даже не нужно ничего обновлять (строки) ... Может быть, какой-то математик может решить это как это?

Taai
источник

Ответы:

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содержит курс евро к доллару США на момент последнего обновления. Идея состоит в том, чтобы увеличить интервал, чтобы учесть максимальное изменение каждой валюты. Коэффициенты увеличения для обоих концов интервала постоянны между обновлениями скоростей, поэтому их можно предварительно рассчитать.

Поскольку ставки меняются незначительно за короткие промежутки времени, приведенный выше запрос, вероятно, даст приблизительное приближение к конечному результату. Чтобы получить окончательный результат, давайте отфильтруем продукты, цены на которые вышли за пределы границ из-за изменений в ставках с момента последнего обновления 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- более актуальная ставка в евро на деньги, выбранные пользователем.

Эффективность обусловлена ​​тем фактом, что диапазон скоростей должен быть небольшим, а значения близки друг к другу.

Даниэль Верите
источник
2

Это звучит как работа для материализованного представления. Хотя PostgreSQL не поддерживает их явно, вы можете создавать и поддерживать материализованные представления, используя функции и триггеры для обычных таблиц.

Я бы:

  • Создайте новую таблицу, скажем products_summary, со схемой вашей текущей productsтаблицы;
  • ALTER TABLE products DROP COLUMN calculated_priceизбавиться от calculated_priceколонны вproducts
  • Написать вид , который производит вывод , который вы хотите для products_summaryпо SELECTING от productsи JOINING на 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( SELECTиз products_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) );

Теперь вы можете быстро искать товары по расчетной цене, например:

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;

Заключение.

Эта идея пришла ко мне всего несколько часов назад, и в настоящее время я прошу вас проверить, прав ли я насчет этого решения. Как вы думаете? Это сработает? Или я ошибся?

Я надеюсь, вы понимаете все это. Я не являюсь носителем английского языка, также уже поздно, и я устал. :)

ОБНОВИТЬ

Ну, похоже, это решает одну проблему, но представляет другую. Жаль. :)

Taai
источник
Проблема в том, что они rate_newest / rate_fixedразные для каждой валюты, и это решение учитывает только одно для выбранных пользователем денег в поиске. Любая цена в другой валюте не будет сравниваться с современными курсами. Ответ, который я представил, каким-то образом имел похожую проблему, но я думаю, что я исправил ее в обновленной версии.
Даниэль Верите
Основная проблема, с которой я сталкиваюсь при таком подходе, заключается в том, что он не использует индексы базы данных по цене (предложения ORDER BY computing_price).
Розенфельд