So sánh hiệu quả giá cả bằng các loại tiền tệ khác nhau


10

Tôi muốn làm cho người dùng có thể tìm kiếm sản phẩm trong phạm vi giá. Người dùng sẽ có thể sử dụng bất kỳ loại tiền tệ nào (USD, EUR, GBP, JPY, ...), bất kể sản phẩm được đặt theo loại tiền nào. Vì vậy, giá sản phẩm là 200USD và, nếu người dùng tìm kiếm các sản phẩm có giá 100 EUR - 200 tỷ, anh ta vẫn có thể tìm thấy nó. Làm thế nào để làm cho nó nhanh và hiệu quả?

Đây là những gì tôi đã làm cho đến bây giờ. Tôi lưu trữ price, currency codecalculated_priceđó là cái giá bằng Euro (EUR) đó là tiền tệ mặc định.

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

Nếu người dùng đang tìm kiếm bằng loại tiền khác, giả sử USD, chúng tôi sẽ tính giá bằng EUR và tìm kiếm calculated_pricecột.

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

Bằng cách này, chúng tôi có thể so sánh giá rất nhanh, vì chúng tôi không cần tính giá thực tế cho mỗi hàng, vì nó được tính một lần.

Điều tồi tệ là ít nhất mỗi ngày chúng ta phải tính lại default_pricecho tất cả các hàng, vì tỷ giá tiền tệ đã bị thay đổi.

Có cách nào tốt hơn để làm việc này không?

Không có giải pháp thông minh nào khác? Có lẽ một số công thức toán học? Tôi có một ý tưởng rằng calculated_pricetỷ lệ này là một tỷ lệ so với một số biến Xvà khi tiền tệ thay đổi, chúng tôi chỉ cập nhật biến đó Xchứ không phải calculated_price, vì vậy chúng tôi thậm chí không cần cập nhật bất cứ điều gì (hàng) ... Có lẽ một số nhà toán học có thể giải quyết nó như thế này?

Câu trả lời:


4

Đây là một cách tiếp cận khác nhau trong đó tính toán lại calculated_pricechỉ là một tối ưu hóa, trái ngược với việc rất cần thiết.

Giả sử trong các currenciesbảng, bạn thêm một cột khác, last_ratechứa tỷ giá hối đoái tại thời điểm calculated_priceđược cập nhật lần cuối, bất kể điều này xảy ra khi nào.

Để nhanh chóng truy xuất một bộ sản phẩm có mức giá nằm trong khoảng từ 50 USD đến 100 USD bao gồm các kết quả mong muốn, bạn có thể làm một cái gì đó như thế:

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

nơi :last_ratechứa tỷ giá EUR / USD tại thời điểm cập nhật cuối cùng. Ý tưởng là tăng khoảng thời gian để tính đến sự thay đổi tối đa của mỗi loại tiền tệ. Các yếu tố tăng cho cả hai đầu của khoảng thời gian là không đổi giữa các lần cập nhật giá, vì vậy chúng có thể được tính toán trước.

Vì tỷ lệ chỉ thay đổi một chút trong khoảng thời gian ngắn, nên truy vấn trên có thể đưa ra một xấp xỉ gần đúng của kết quả cuối cùng. Để có được kết quả cuối cùng, bộ lọc cho phép của các sản phẩm mà giá đã trượt ra khỏi giới hạn do sự thay đổi tỷ giá kể từ khi cập nhật mới nhất của 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;

nơi :current_ratelà tỷ lệ up-to-date hơn với EUR cho tiền choosen bởi người sử dụng.

Hiệu quả đến từ thực tế là phạm vi tỷ lệ được cho là nhỏ, các giá trị gần nhau.


2

Điều này nghe có vẻ như một công việc cho một cái nhìn cụ thể hóa. Mặc dù PostgreSQL không hỗ trợ rõ ràng cho chúng, bạn có thể tạo và duy trì các chế độ xem được cụ thể hóa bằng các hàm và trình kích hoạt trên các bảng thông thường.

Tôi sẽ:

  • Tạo một bảng mới, giả sử products_summary, với lược đồ của productsbảng hiện tại của bạn ;
  • ALTER TABLE products DROP COLUMN calculated_priceđể thoát khỏi calculated_pricecột trongproducts
  • Viết một quan điểm cho rằng sẽ cho kết quả bạn muốn cho products_summarybởi SELECTing từ productsJOINing trên currencies. Tôi sẽ gọi nó products_summary_dynamicnhưng việc đặt tên là tùy thuộc vào bạn. Bạn có thể sử dụng một chức năng thay vì một khung nhìn nếu bạn muốn.
  • Định kỳ làm mới bảng xem vật chất products_summarytừ products_summary_dynamicvới BEGIN; TRUNCATE products_summary; INSERT INTO products_summary SELECT * FROM products_summary_dynamic; COMMIT;.
  • Tạo trình AFTER INSERT OR UPDATE OR DELETE ON productskích hoạt chạy quy trình kích hoạt để duy trì products_summarybảng, xóa các hàng khi bị xóa khỏi products, thêm chúng khi được thêm vào products(bằng cách nhập SELECTtừ products_summary_dynamicchế độ xem) và cập nhật chúng khi chi tiết sản phẩm thay đổi.

Cách tiếp cận này sẽ có một khóa độc quyền products_summarytrong quá trình TRUNCATE ..; INSERT ...;giao dịch cập nhật bảng tóm tắt. Nếu điều đó gây ra các quầy hàng trong ứng dụng của bạn vì mất quá nhiều thời gian, thay vào đó bạn có thể giữ hai phiên bản của products_summarybảng. Cập nhật cái không được sử dụng, sau đó trong giao dịchALTER TABLE products_summary RENAME TO products_summary_old; ALTER TABLE products_summary_new RENAME TO products_summary;


Một cách tiếp cận khác nhưng rất tinh ranh sẽ là sử dụng một chỉ số biểu thức. Bởi vì việc cập nhật bảng tiền tệ theo cách tiếp cận này có thể không tránh khỏi yêu cầu khóa trong một DROP INDEXCREATE INDEXtôi sẽ không làm điều đó quá thường xuyên - nhưng nó có thể phù hợp với một số tình huống.

Ý tưởng là để bọc chuyển đổi tiền tệ của bạn trong một IMMUTABLEchức năng. Vì IMMUTABLEbạn đang đảm bảo với công cụ cơ sở dữ liệu rằng giá trị trả về cho bất kỳ đối số đã cho nào sẽ luôn giống nhau và sẽ tự do thực hiện tất cả các loại điều điên rồ nếu giá trị trả về khác nhau. Gọi hàm, nói , to_euros(amount numeric, currency char(3)) returns numeric. Thực hiện nó theo cách bạn muốn; một CASEtuyên bố lớn bằng tiền tệ, một bảng tra cứu, bất cứ điều gì. Nếu bạn sử dụng bảng tra cứu, bạn không bao giờ phải thay đổi bảng tra cứu trừ khi được mô tả dưới đây .

Tạo một chỉ mục biểu thức trên products, như:

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

Bây giờ bạn có thể tìm kiếm nhanh các sản phẩm theo giá tính toán, ví dụ:

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

Vấn đề bây giờ trở thành làm thế nào để cập nhật các bảng tiền tệ. Mẹo ở đây là bạn có thể thay đổi các bảng tiền tệ, bạn chỉ cần thả và tạo lại chỉ mục để làm điều đó.

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;

Tôi bỏ và định nghĩa lại hàm trên chỉ để nhấn mạnh rằng bạn phải loại bỏ mọi thứ sử dụng hàm nếu bạn xác định lại hàm bất biến. Sử dụng một CASCADEgiọt là cách tốt nhất để làm điều đó.

Tôi nghi ngờ rằng một quan điểm cụ thể hóa là cách tiếp cận tốt hơn. Nó chắc chắn là an toàn hơn. Tôi bao gồm cái này chủ yếu cho đá.


Ngay bây giờ tôi đang nghĩ về điều này - tại sao tôi nên cập nhật calculated_pricetất cả? Tôi chỉ có thể lưu trữ initial_currency_value(tỷ giá tiền tệ không đổi được thực hiện, giả sử, hôm nay) và luôn luôn tính toán với điều đó! Và khi hiển thị giá bằng Euro, tất nhiên, tính theo tỷ giá tiền tệ thực tế. Tôi có đúng không Hoặc có một vấn đề mà tôi không thấy?
Taai

1

Tôi đã đưa ra ý tưởng của riêng tôi. Hãy cho tôi biết nếu nó thực sự đi làm, xin vui lòng!

Vấn đề.

Khi sản phẩm đang được thêm vào productsbảng, giá sẽ được chuyển đổi thành tiền tệ mặc định (EUR) và được lưu trữ trong calculated_pricecột.

Chúng tôi muốn người dùng có thể tìm kiếm (bộ lọc) giá của bất kỳ loại tiền tệ nào. Nó được thực hiện bằng cách chuyển đổi giá đầu vào thành tiền tệ mặc định (EUR) và so sánh nó với calculated_pricecột.

Chúng tôi cần cập nhật tỷ giá tiền tệ, vì vậy người dùng sẽ có thể tìm kiếm theo tỷ giá tiền tệ mới. Nhưng vấn đề là - làm thế nào để cập nhật calculated_pricehiệu quả.

Giải pháp (hy vọng).

Cách cập nhật calculated_pricehiệu quả.

Đừng! :)

Ý tưởng là chúng tôi lấy tỷ giá tiền tệ của ngày hôm qua ( tất cả cùng một ngày ) chỉcalculated_price sử dụng các tỷ giá. Giống như ... mãi mãi! Không cập nhật hàng ngày. Điều duy nhất chúng ta cần trước khi so sánh / lọc / tìm kiếm giá là lấy tỷ giá tiền tệ ngày hôm nay giống như ngày hôm qua.

Vì vậy, trong calculated_pricechúng tôi sẽ chỉ sử dụng các tỷ lệ tiền tệ của ngày cố định (chúng tôi đã lựa chọn, chẳng hạn cho phép của, ngày hôm qua). Những gì chúng ta sẽ cần là chuyển đổi giá hôm nay thành giá của ngày hôm qua. Nói cách khác, lấy tỷ lệ hôm nay và chuyển đổi nó thành tỷ lệ của ngày hôm qua:

cash_in_euros * ( rate_newest / rate_fixed )

Và đây là bảng các loại tiền:

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

Đây là cách thêm một sản phẩm có giá 200 USD và cách calculated_pricetính tiền: từ tỷ giá USD đến tỷ giá EUR mới nhất và vào tỷ lệ cố định (cũ)

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

Điều này cũng có thể được tính toán trước ở phía khách hàng và đó là điều tôi sẽ làm - tính giá đầu vào của người dùng calculated_pricevới giá trị tương thích trước khi chúng tôi thực hiện truy vấn, do đó sẽ được sử dụng tốtSELECT * FROM products WHERE calculated_price > 100.0 AND calculated_price < 200.0;

Phần kết luận.

Ý tưởng này đã đến với tôi chỉ vài giờ trước và hiện tại tôi đang yêu cầu bạn kiểm tra xem tôi có đúng về giải pháp này không. Bạn nghĩ sao? Đây có phải là đi làm không? Hay tôi đã nhầm?

Tôi hy vọng bạn hiểu tất cả điều này. Tôi không phải là người nói tiếng Anh bản địa, cũng muộn và tôi mệt mỏi. :)

CẬP NHẬT

Vâng, có vẻ như nó giải quyết một vấn đề, nhưng giới thiệu một vấn đề khác. Quá tệ. :)


Vấn đề là sự rate_newest / rate_fixedkhác nhau về mỗi loại tiền tệ và giải pháp này chỉ xem xét loại tiền dành cho người dùng chọn trong tìm kiếm. Bất kỳ giá nào trong một loại tiền tệ khác nhau sẽ không được so sánh với tỷ lệ cập nhật. Câu trả lời tôi đã gửi bằng cách nào đó có một vấn đề tương tự nhưng tôi nghĩ rằng tôi đã sửa nó trong phiên bản cập nhật.
Daniel Vérité

Vấn đề chính tôi thấy với cách tiếp cận đó là nó không tận dụng được các chỉ số cơ sở dữ liệu về giá (ORDER BY tính toán_ mệnh đề tính toán).
rosenfeld
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.