Ефективне порівняння цін у різних валютах


10

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

Ось що я робив дотепер. Я зберігаю price, currency codeі calculated_priceце ціна в євро (євро), яка є валютою за замовчуванням.

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, тож нам навіть не потрібно нічого оновлювати (рядки) ... Можливо, якийсь математик може це вирішити подобається це?

Відповіді:


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найсучасніша ставка з євро на гроші, обрані користувачем.

Ефективність випливає з того, що діапазон норм повинен бути невеликим, значення знаходяться близько.


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;


Альтернативним, але дуже хитрим підходом було б використання індексу вираження. Оскільки оновлення валютної таблиці при такому підході, ймовірно, неминуче вимагатиме блокування під час 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;

Висновок.

Ця ідея прийшла до мене всього кілька годин тому, і зараз я прошу вас перевірити, чи не прав я я щодо цього рішення. Як ти гадаєш? Це буде працювати? Або я помилився?

Сподіваюся, ви все це розумієте. Я не є носієм англійської мови, також пізно і я втомився. :)

ОНОВЛЕННЯ

Що ж, схоже, вона вирішує одну проблему, а вводить іншу. Дуже погано. :)


Проблема полягає в rate_newest / rate_fixedтому, що значення валюти різне для кожної валюти, і це рішення розглядає лише те, що вибрано користувачем гроші на пошук. Будь-яка ціна в іншій валюті не буде порівнюватися з сучасними курсами. Відповідь, яку я подав якось, мала подібну проблему, але, думаю, я її виправила в оновленій версії.
Даніель Веріте

Основна проблема, яку я бачу при такому підході, полягає в тому, що він не використовує переваги індексів бази даних щодо ціни (ЗАМОВЛЕННЯ ПО ОБРАЗУВАННІ_Цінові пропозиції).
rosenfeld
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.