Membandingkan harga dalam mata uang yang berbeda secara efisien

10

Saya ingin memungkinkan pengguna untuk mencari produk dalam kisaran harga. Pengguna harus dapat menggunakan mata uang apa pun (USD, EUR, GBP, JPY, ...), apa pun mata uang yang ditentukan oleh produk. Jadi, harga produk adalah 200 USD dan, jika pengguna mencari produk yang harganya 100EUR - 200EUR, ia masih dapat menemukannya. Bagaimana cara membuatnya cepat dan efektif?

Inilah yang telah saya lakukan sampai sekarang. Saya menyimpan price, currency codedan calculated_priceitu adalah harga dalam Euro (EUR) yang merupakan mata uang default.

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';

Jika pengguna mencari dengan mata uang lain, katakanlah USD, kami menghitung harga dalam EUR dan mencari calculated_pricekolom.

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

Dengan cara ini kita dapat membandingkan harga dengan sangat cepat, karena kita tidak perlu menghitung harga aktual untuk setiap baris, karena dihitung sekali.

Yang buruk adalah bahwa setidaknya setiap hari kita harus menghitung ulang default_priceuntuk semua baris, karena nilai tukar mata uang telah berubah.

Apakah ada cara yang lebih baik untuk melakukan ini?

Apakah tidak ada solusi pintar lainnya? Mungkin beberapa rumus matematika? Saya punya ide bahwa calculated_priceini adalah rasio terhadap beberapa variabel Xdan, ketika mata uang berubah, kami hanya memperbarui variabel itu X, bukan calculated_price, jadi kami bahkan tidak perlu memperbarui apa pun (baris) ... Mungkin beberapa ahli matematika dapat menyelesaikannya seperti ini?

Taai
sumber

Jawaban:

4

Berikut adalah pendekatan berbeda yang mengkomputasi ulang calculated_pricehanya merupakan pengoptimalan, dan bukan sangat diperlukan.

Misalkan dalam currenciestabel, Anda menambahkan kolom lain last_rate,, yang berisi nilai tukar pada saat calculated_priceterakhir diperbarui, tidak peduli kapan ini terjadi.

Untuk cepat mengambil satu set produk dengan titik harga antara, katakanlah, 50 USD dan 100 USD yang mencakup hasil yang diinginkan, Anda dapat melakukan sesuatu seperti itu:

  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))

di mana :last_rateberisi nilai tukar EUR / USD pada saat pembaruan terakhir. Idenya adalah untuk meningkatkan interval dengan memperhitungkan variasi maksimum setiap mata uang. Faktor peningkatan untuk kedua ujung interval adalah konstan di antara pembaruan laju, sehingga keduanya bisa dihitung sebelumnya.

Karena nilai tukar hanya berubah sedikit selama periode waktu yang singkat, permintaan di atas kemungkinan akan memberikan perkiraan hasil akhir yang dekat. Untuk mendapatkan hasil akhir, mari saring produk-produk yang harganya telah keluar dari batasan karena perubahan nilai sejak pembaruan terakhir 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;

di mana :current_ratetingkat yang lebih baru dengan EUR untuk uang yang dipilih oleh pengguna.

Efisiensi berasal dari kenyataan bahwa kisaran tarif seharusnya kecil, nilainya berdekatan.

Daniel Vérité
sumber
2

Ini terdengar seperti pekerjaan untuk tampilan terwujud. Meskipun PostgreSQL tidak mendukungnya secara eksplisit, Anda dapat membuat dan mempertahankan tampilan yang terwujud menggunakan fungsi dan pemicu pada tabel normal.

Saya akan:

  • Buat tabel baru, katakanlah products_summary, dengan skema productstabel Anda saat ini;
  • ALTER TABLE products DROP COLUMN calculated_priceuntuk menyingkirkan calculated_pricekolom diproducts
  • Tulis tampilan yang menghasilkan output yang Anda inginkan products_summarydengan menggunakan SELECTdari productsdan JOINterus currencies. Saya akan menelepon itu products_summary_dynamictetapi penamaannya terserah Anda. Anda bisa menggunakan fungsi alih-alih tampilan jika Anda mau.
  • Menyegarkan secara berkala tabel tampilan terwujud products_summarydari products_summary_dynamicdengan BEGIN; TRUNCATE products_summary; INSERT INTO products_summary SELECT * FROM products_summary_dynamic; COMMIT;.
  • Buat AFTER INSERT OR UPDATE OR DELETE ON productspemicu yang menjalankan prosedur pemicu untuk mempertahankan products_summarytabel, menghapus baris saat dihapus products, menambahkannya saat ditambahkan ke products(dengan melihat SELECTdari products_summary_dynamictampilan), dan memutakhirkannya ketika detail produk berubah.

Pendekatan ini akan mengambil kunci eksklusif products_summaryselama TRUNCATE ..; INSERT ...;transaksi yang memperbarui tabel ringkasan. Jika itu menyebabkan warung di aplikasi Anda karena butuh waktu lama, Anda bisa menyimpan dua versi products_summarytabel. Perbarui yang tidak digunakan, lalu dalam transaksiALTER TABLE products_summary RENAME TO products_summary_old; ALTER TABLE products_summary_new RENAME TO products_summary;


Alternatif tetapi pendekatan yang sangat cerdik adalah dengan menggunakan indeks ekspresi. Karena memperbarui tabel mata uang dengan pendekatan ini mungkin tidak terhindarkan memerlukan kunci selama a DROP INDEXdan CREATE INDEXsaya tidak akan melakukannya terlalu sering - tetapi mungkin cocok untuk beberapa situasi.

Idenya adalah untuk membungkus konversi mata uang Anda dalam suatu IMMUTABLEfungsi. Karena IMMUTABLEAnda menjamin ke mesin database bahwa nilai kembali untuk setiap argumen yang diberikan akan selalu sama, dan bahwa bebas untuk melakukan segala macam hal gila jika nilai kembali berbeda. Panggil fungsi, katakan to_euros(amount numeric, currency char(3)) returns numeric,. Implementasikan sesuai keinginan Anda; CASEpernyataan besar berdasarkan mata uang, tabel pencarian, apa pun. Jika Anda menggunakan tabel pencarian Anda tidak boleh mengubah tabel pencarian kecuali seperti yang dijelaskan di bawah ini .

Buat indeks ekspresi pada products, seperti:

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

Anda sekarang dapat mencari produk dengan cepat berdasarkan harga yang dihitung, misalnya:

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

Masalahnya sekarang menjadi bagaimana memperbarui tabel mata uang. Kuncinya di sini adalah bahwa Anda dapat mengubah tabel mata uang, Anda hanya perlu menjatuhkan dan membuat kembali indeks untuk melakukannya.

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;

Saya menjatuhkan dan mendefinisikan ulang fungsi di atas hanya untuk menekankan bahwa Anda harus membuang semua yang menggunakan fungsi jika Anda mendefinisikan kembali fungsi yang tidak dapat diubah. Menggunakan CASCADEsetetes adalah cara terbaik untuk melakukannya.

Saya sangat curiga bahwa pandangan terwujud adalah pendekatan yang lebih baik. Ini tentu lebih aman. Saya termasuk yang ini kebanyakan untuk iseng.

Craig Ringer
sumber
Saat ini saya sedang memikirkan hal ini - mengapa saya harus memperbarui calculated_pricesama sekali? Saya hanya bisa menyimpan initial_currency_value(kurs mata uang konstan yang diambil, katakanlah, hari ini) dan selalu menghitung terhadap itu! Dan saat menampilkan harga dalam Euro, hitung terhadap nilai tukar mata uang aktual, tentu saja. Apakah saya benar? Atau ada masalah yang tidak saya lihat?
Taai
1

Saya datang dengan ide saya sendiri. Katakan padaku apakah itu benar-benar akan berhasil!

Masalah.

Ketika produk ditambahkan dalam productstabel, harga akan dikonversi ke mata uang default (EUR) dan disimpan dalam calculated_pricekolom.

Kami ingin pengguna dapat mencari (memfilter) harga mata uang apa pun. Ini dilakukan dengan mengubah harga input ke mata uang default (EUR) dan membandingkannya dengan calculated_pricekolom.

Kami perlu memperbarui nilai mata uang, sehingga pengguna dapat mencari berdasarkan nilai mata uang baru. Tetapi masalahnya adalah - bagaimana memperbarui secara calculated_priceefisien.

Solusinya (semoga).

Cara memperbarui secara calculated_priceefisien.

Jangan! :)

Idenya adalah bahwa kita mengambil harga mata uang kemarin ( semua tanggal yang sama ) dalam calculated_pricepenggunaan hanya orang-orang. Seperti ... selamanya! Tidak ada pembaruan harian. Satu-satunya hal yang kita butuhkan sebelum membandingkan / memfilter / mencari harga adalah dengan mengambil nilai tukar hari ini seperti kemarin.

Jadi, di calculated_pricekami hanya akan menggunakan kurs mata uang pada tanggal yang sudah ditentukan (kami telah memilih, katakanlah, kemarin). Yang kita perlukan adalah mengubah harga hari ini menjadi harga kemarin. Dengan kata lain, ambil kurs hari ini dan konversikan ke kurs kemarin:

cash_in_euros * ( rate_newest / rate_fixed )

Dan ini adalah tabel mata uang:

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")
);

Ini adalah cara menambahkan produk yang harganya 200 USD dan bagaimana calculated_pricepenghitungannya: dari USD ke kurs EUR terbaru dan ke kurs tetap (lama)

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';

Ini juga bisa pra-dihitung di sisi klien dan itulah yang akan saya lakukan - menghitung harga input pengguna ke nilai yang calculated_pricekompatibel sebelum kami membuat kueri, sehingga akan ada yang digunakan lama yang baikSELECT * FROM products WHERE calculated_price > 100.0 AND calculated_price < 200.0;

Kesimpulan.

Ide ini datang kepada saya hanya beberapa jam yang lalu dan saat ini saya meminta Anda untuk memeriksa apakah saya benar tentang solusi ini. Bagaimana menurut anda? Apakah ini akan berhasil? Atau saya salah?

Saya harap Anda mengerti semua ini. Saya bukan penutur asli bahasa Inggris, juga sudah terlambat dan saya lelah. :)

MEMPERBARUI

Yah, sepertinya itu menyelesaikan satu masalah, tetapi memperkenalkan yang lain. Sangat buruk. :)

Taai
sumber
Masalahnya adalah rate_newest / rate_fixedperbedaan per mata uang, dan solusi ini hanya mempertimbangkan uang yang dipilih pengguna dalam pencarian. Harga apa pun dalam mata uang yang berbeda tidak akan dibandingkan dengan tarif terbaru. Jawaban yang saya kirimkan entah bagaimana memiliki masalah yang sama tetapi saya pikir saya telah memperbaikinya dalam versi yang diperbarui.
Daniel Vérité
Masalah utama yang saya lihat dengan pendekatan itu adalah tidak mengambil keuntungan dari indeks basis data pada harga (ORDER DENGAN klausa dihitung_harga).
rosenfeld