Farklı para birimlerindeki fiyatları etkili bir şekilde karşılaştırma


10

Kullanıcının fiyat aralığında ürün aramasını mümkün kılmak istiyorum. Ürün tarafından ayarlanan para birimi ne olursa olsun, kullanıcı herhangi bir para birimini (USD, EUR, GBP, JPY, ...) kullanabilmelidir. Yani, ürün fiyatı 200USD'dir ve kullanıcı 100EUR - 200EUR'a mal olan ürünleri ararsa, yine de bulabilir. Hızlı ve etkili nasıl yapılır?

İşte şimdiye kadar yaptığım şey. Ben depolamak price, currency codeve calculated_pricevarsayılan para birimi olan Euro (EUR) 'fiyatını olduğunu.

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

Kullanıcı başka bir para birimiyle arama yapıyorsa, diyelim ki USD, fiyatı EUR cinsinden hesaplıyoruz ve calculated_pricesütunda arama yapıyoruz .

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

Bu şekilde fiyatları çok hızlı bir şekilde karşılaştırabiliriz, çünkü her satır için gerçek fiyatı hesaplamamız gerekmez, çünkü bir kez hesaplanır.

Kötü olan şey, en azından her gün default_pricetüm satırlar için yeniden hesaplamamız gerektiğidir , çünkü döviz kurları değiştirilmiştir.

Bununla başa çıkmanın daha iyi bir yolu var mı?

Başka akıllı bir çözüm yok mu? Belki matematiksel bir formül? calculated_priceBazı değişkenlere karşı bir oran olduğunu bir fikrim var ve Xpara birimi değiştiğinde, sadece bu değişkeni güncelliyoruz X, değil calculated_price, bu yüzden bir şey (satırlar) güncellememiz bile gerekmiyor ... Belki bazı matematikçiler bunu çözebilir bunun gibi?

Yanıtlar:


4

Burada, yeniden hesaplamanın calculated_pricekesinlikle gerekli olmanın aksine, sadece bir optimizasyon olduğu farklı bir yaklaşım var .

currenciesTablolarda, ne zaman olursa olsun last_rate, calculated_priceen son güncellenme anındaki döviz kurunu içeren başka bir sütun eklediğinizi varsayalım .

İstenen sonuçları içeren 50 USD ile 100 USD arasında bir fiyat noktasına sahip bir ürün grubunu hızlı bir şekilde almak için şöyle bir şey yapabilirsiniz:

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

Burada :last_rate, son güncelleme sırasındaki EUR / USD döviz kurunu içerir. Fikir, her para biriminin maksimum varyasyonunu dikkate almak için aralığı arttırmaktır. Aralığın her iki ucu için artış faktörleri oran güncellemeleri arasında sabittir, bu nedenle önceden hesaplanabilirler.

Oranlar kısa sürelerde çok az değiştiği için, yukarıdaki sorgunun nihai sonuca yakın bir tahmin vermesi muhtemeldir. Son sonucu almak için, son güncellemeden bu yana oranlardaki değişiklikler nedeniyle fiyatların sınırların dışına çıktığı ürünleri filtreleyelim 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;

nerede :current_ratekullanıcı tarafından seçilmiş para için EUR daha yukarı güncel oranıdır.

Verimlilik, oran aralığının küçük olması, değerlerin birbirine yakın olması gerçeğinden kaynaklanmaktadır.


2

Bu, somutlaştırılmış bir görüş için iş gibi geliyor. PostgreSQL bunları açıkça desteklemese de, normal tablolardaki işlevleri ve tetikleyicileri kullanarak somutlaştırılmış görünümler oluşturabilir ve koruyabilirsiniz.

İsterim:

  • products_summaryMevcut productstablonuzun şeması ile yeni bir tablo oluşturun ;
  • ALTER TABLE products DROP COLUMN calculated_pricecalculated_pricesütundan kurtulmakproducts
  • Eğer için istediğiniz çıktıyı üreten bir görünüm yaz products_summarytarafından SELECTgelen ing productsveJOIN üzerinde ing currencies. Bunu söylerdim products_summary_dynamicama adlandırma size kalmış. İsterseniz görünüm yerine bir işlev kullanabilirsiniz.
  • Malzeme görünümü tablosunu periyodik olarak yenileyin products_summary gelen products_summary_dynamicile BEGIN; TRUNCATE products_summary; INSERT INTO products_summary SELECT * FROM products_summary_dynamic; COMMIT;.
  • Bir oluşturma AFTER INSERT OR UPDATE OR DELETE ON productskorumak için bir tetikleyici prosedürü çalışan tetiği products_summarysilinen zaman satırları silme tablo products, eklendiğinde ekleyerek productstarafından (SELECTproducts_summary_dynamic görünümden girerek ) ve ürün ayrıntıları değiştiğinde güncelleştirmek için .

Bu yaklaşım , özet tablosunu güncelleyen işlem products_summarysırasında özel bir kilit alacaktır TRUNCATE ..; INSERT ...;. Bu, uygulamanızda çok uzun sürdüğü için duraklara neden oluyorsa, bunun yerine iki sürümü saklayabilirsiniz.products_summary tablonun . Kullanılmayan, ardından bir işlemde güncellemeALTER TABLE products_summary RENAME TO products_summary_old; ALTER TABLE products_summary_new RENAME TO products_summary;


Alternatif ama çok tehlikeli bir yaklaşım, bir ifade endeksi kullanmak olacaktır. Çünkü para birimi tablosunu bu yaklaşımla güncellemek, kaçınılmaz olarak birDROP INDEX ve CREATE INDEXbunu çok sık yapmam - ama bazı durumlar için uygun olabilir.

Fikir, para birimi dönüşümünüzü bir IMMUTABLEişlevde sarmaktır. Öyle yana IMMUTABLEherhangi verilen argümanlar için dönüş değeri her zaman aynı olacağı veritabanı motoruna garanti ve deli şeyler eğer dönüş değeri farklıdır her türlü yapmakta özgür olduğunu ediyoruz. Diyelim ki işlevi çağırın to_euros(amount numeric, currency char(3)) returns numeric. İstediğiniz gibi uygulayın; CASEpara birimine göre büyük bir ifade, bir arama tablosu, her neyse. Bir arama tablosu kullanıyorsanız , arama tablosunu aşağıda açıklananlar dışında hiçbir zaman değiştirmemelisiniz .

Şunun gibi bir ifade dizini oluşturun products:

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

Artık ürünleri hızlı bir şekilde hesaplanan fiyata göre arayabilirsiniz, örneğin:

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

Sorun artık para birimi tablolarının nasıl güncelleneceği haline geliyor. İşin püf noktası burada olmasıdır olabilir sadece bırakın ve bunu yapmak için indeks yeniden oluşturmak zorunda, para birimi tabloları değiştirin.

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;

Ben bırakın ve yalnızca o strese yukarıdaki işlevini yeniden tanımlamak gerekir damla şeyi bir değişmez işlevini yeniden tanımlamak durumunda işlevini kullanır. Bir CASCADEdamla kullanmak bunu yapmanın en iyi yoludur.

Maddi bir görüşün daha iyi bir yaklaşım olduğundan şüpheleniyorum. Kesinlikle daha güvenli. Bunu çoğunlukla tekmeler için ekliyorum.


Şu anda bunu düşünüyorum - neden güncellemeliyim calculated_price? Ben sadece initial_currency_value(bugün alınan sabit döviz kuru, diyelim, bugün) depolamak ve her zaman buna karşı hesaplamak olabilir! Ve fiyatı Euro olarak görüntülerken, elbette gerçek döviz kuruna göre hesaplayın. Haklı mıyım? Yoksa görmediğim bir sorun mu var?
Taai

1

Kendi fikrimi buldum. Bana gerçekten işe yarayıp yaramayacağını söyle lütfen!

Sorun.

Ürün productstabloya eklenirken , fiyat varsayılan para birimine (EUR) dönüştürülür vecalculated_price sütunda .

Kullanıcının herhangi bir para biriminin fiyatlarını arayabilmesini (filtreleyebilmesini) istiyoruz. Giriş fiyatını varsayılan para birimine (EUR) dönüştürerek vecalculated_price sütunla .

Döviz kurlarını güncellememiz gerekiyor, böylece kullanıcılar yeni döviz kuruna göre arama yapabiliyorlar. Ama sorun şu ki - calculated_priceverimli bir şekilde nasıl güncellenir .

Çözüm (umarım).

calculated_priceVerimli güncelleme .

Yapma! :)

Buradaki fikir, dünün döviz kurlarını ( aynı tarihin tümü ) yalnızcacalculated_price kullanımda almamızdır. bu . Mesela ... sonsuza! Günlük güncelleme yok. Fiyatları karşılaştırmadan / filtrelemeden / aramadan önce ihtiyacımız olan tek şey, dünkü gibi bugünkü döviz kurlarını almaktır.

Yani, calculated_pricesadece sabit tarihin döviz kurunu kullanacağız (diyelim ki dün seçtik). İhtiyacımız olan şey bugünün fiyatını dünün fiyatına dönüştürmektir. Başka bir deyişle, bugünkü fiyatı alın ve dünkü tarihe dönüştürün:

cash_in_euros * ( rate_newest / rate_fixed )

Ve bu para birimlerinin tablosu:

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 USD'ye mal olan bir ürün nasıl eklenir ve getiri nasıl calculated_pricehesaplanır: USD'den en yeni EUR oranına ve sabit (eski) orana

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

Bu aynı zamanda müşteri tarafında önceden hesaplanabilir ve yapacağım şey budur - calculated_pricebir sorgu yapmadan önce kullanıcı giriş fiyatını uyumlu değere hesaplayın , bu yüzden iyi eski kullanılırSELECT * FROM products WHERE calculated_price > 100.0 AND calculated_price < 200.0;

Sonuç.

Bu fikir bana sadece birkaç saat önce geldi ve şu anda sizden bu çözüm hakkında doğru olup olmadığımı kontrol etmenizi istiyorum. Ne düşünüyorsun? Bu işe yarayacak mı? Yoksa yanıldım mı?

Umarım bütün bunları anlarsın. Ben anadili İngilizce değilim, aynı zamanda geç ve yorgunum. :)

GÜNCELLEME

Görünüşe göre bir problemi çözüyor, ama başka bir problemi getiriyor. Çok kötü. :)


Sorun, rate_newest / rate_fixedpara birimi başına farklı olması ve bu çözümün, aramada kullanıcı tarafından seçilen para için yalnızca bir çözüm olduğunu düşünmesidir. Farklı bir para birimindeki hiçbir fiyat, güncel oranlarla karşılaştırılmaz. Gönderdiğim yanıtın bir şekilde benzer bir sorunu vardı ama bence güncellemede düzelttim.
Daniel Vérité

Bu yaklaşım ile gördüğüm ana sorun fiyat (ORDER BY hesaplanan_fiyat cümleleri) veritabanı endeksleri yararlanmak yok olmasıdır.
rosenfeld
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.