เปรียบเทียบราคาในสกุลเงินที่แตกต่างกันอย่างมีประสิทธิภาพ


10

ฉันต้องการให้ผู้ใช้สามารถค้นหาผลิตภัณฑ์ภายในช่วงราคาได้ ผู้ใช้ควรสามารถใช้สกุลเงินใดก็ได้ (USD, EUR, GBP, JPY, ... ) ไม่ว่าผลิตภัณฑ์จะมีการตั้งค่าสกุลเงินใดก็ตาม ดังนั้นราคาผลิตภัณฑ์คือ 200 ดอลล่าร์สหรัฐฯและหากผู้ใช้ค้นหาผลิตภัณฑ์ที่มีค่าใช้จ่าย 100EUR - 200EUR เขาก็อาจจะหาได้ จะทำให้มันเร็วและมีประสิทธิภาพได้อย่างไร?

นี่คือสิ่งที่ฉันได้ทำมาจนถึงตอนนี้ ฉันเก็บ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 เราจะคำนวณราคาเป็น EUR และค้นหา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_rateEUR มีอัตราที่เป็นปัจจุบันมากขึ้นสำหรับการเลือกใช้เงินของผู้ใช้

ประสิทธิภาพมาจากข้อเท็จจริงที่ว่าช่วงของอัตราควรจะเล็กค่าที่อยู่ใกล้กัน


2

ดูเหมือนว่างานสำหรับมุมมองที่ปรากฏ แม้ว่า PostgreSQL จะไม่สนับสนุนอย่างชัดเจนคุณสามารถสร้างและรักษามุมมองที่เป็นรูปธรรมโดยใช้ฟังก์ชันและทริกเกอร์ในตารางปกติ

ฉันจะ:

  • สร้างตารางใหม่products_summaryด้วยสคีมาของproductsตารางปัจจุบันของคุณ
  • ALTER TABLE products DROP COLUMN calculated_priceเพื่อกำจัดcalculated_priceคอลัมน์ในproducts
  • เขียนมุมมองที่ผลิตออกที่คุณต้องการหาproducts_summaryโดยSELECTไอเอ็นจีจากproductsและไอเอ็นจีในJOIN currenciesฉันจะเรียกมันว่าproducts_summary_dynamicแต่การตั้งชื่อนั้นขึ้นอยู่กับคุณ คุณสามารถใช้ฟังก์ชั่นแทนมุมมองถ้าคุณต้องการ
  • ระยะฟื้นฟูตารางดู materialized products_summaryจากที่มีproducts_summary_dynamicBEGIN; 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(อัตราแลกเปลี่ยนคงที่ที่สมมติว่าวันนี้) และคำนวณกับมันเสมอ ! และเมื่อแสดงราคาเป็นสกุลเงินยูโรให้คำนวณเทียบกับอัตราสกุลเงินจริง ฉันถูกไหม? หรือมีปัญหาที่ฉันไม่เห็น?
Taai

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 USD และวิธีการcalculated_priceคำนวณของการรับ: จาก USD ไปยังอัตรา EUR ล่าสุดและเป็นอัตราคงที่ (เก่า)

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;

ข้อสรุป

ความคิดนี้มาถึงฉันไม่กี่ชั่วโมงที่ผ่านมาและตอนนี้ฉันขอให้คุณตรวจสอบว่าฉันถูกเกี่ยวกับการแก้ปัญหานี้ คุณคิดอย่างไร? มันจะใช้งานได้หรือไม่ หรือฉันเข้าใจผิด?

ฉันหวังว่าคุณจะเข้าใจทั้งหมดนี้ ฉันไม่ใช่เจ้าของภาษาอังกฤษ แต่ก็สายไปแล้วและฉันก็เหนื่อย :)

UPDATE

ดูเหมือนว่าจะช่วยแก้ปัญหาหนึ่ง แต่แนะนำอีกปัญหาหนึ่ง เลวร้ายเกินไป. :)


ปัญหาคือว่าrate_newest / rate_fixedจะแตกต่างกันต่อสกุลเงินและการแก้ปัญหานี้จะพิจารณาเพียงหนึ่งเดียวสำหรับเงินที่ผู้ใช้เลือกในการค้นหา ราคาใดในสกุลเงินอื่นจะไม่ถูกนำมาเปรียบเทียบกับอัตราที่ทันสมัย คำตอบที่ฉันส่งมามีปัญหาที่คล้ายกัน แต่ฉันคิดว่าฉันได้แก้ไขในเวอร์ชันที่อัปเดตแล้ว
Daniel Vérité

ปัญหาหลักที่ฉันเห็นด้วยวิธีการนี้ก็คือมันไม่ได้ใช้ประโยชน์จากดัชนีฐานข้อมูลในราคา (เรียงตามคำสั่งคำนวณ
rosenfeld
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.