Effizienter Vergleich von Preisen in verschiedenen Währungen


10

Ich möchte dem Benutzer die Suche nach Produkten innerhalb einer Preisspanne ermöglichen. Der Benutzer sollte in der Lage sein, jede Währung (USD, EUR, GBP, JPY, ...) zu verwenden, unabhängig davon, welche Währung vom Produkt festgelegt wird. Der Produktpreis beträgt also 200 USD. Wenn der Benutzer nach Produkten sucht, die 100 bis 200 EUR kosten, kann er sie dennoch finden. Wie macht man es schnell und effektiv?

Folgendes habe ich bisher getan. Ich speichere das price, currency codeund das calculated_priceist der Preis in Euro (EUR), der die Standardwährung ist.

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

Wenn der Benutzer mit einer anderen Währung sucht, sagen wir USD, berechnen wir den Preis in EUR und durchsuchen die calculated_priceSpalte.

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

Auf diese Weise können wir die Preise sehr schnell vergleichen, da wir nicht den tatsächlichen Preis für jede Zeile berechnen müssen, da er einmal berechnet wird.

Schlecht ist, dass wir zumindest jeden Tag die default_pricefür alle Zeilen neu berechnen müssen , da die Wechselkurse geändert wurden.

Gibt es einen besseren Weg, um damit umzugehen?

Gibt es keine andere clevere Lösung? Vielleicht eine mathematische Formel? Ich habe die Idee, dass das calculated_priceein Verhältnis zu einer Variablen ist, Xund wenn sich die Währung ändert, aktualisieren wir nur diese Variable X, nicht die calculated_price, sodass wir nicht einmal etwas (Zeilen) aktualisieren müssen ... Vielleicht kann es ein Mathematiker lösen so was?

Antworten:


4

Hier ist ein anderer Ansatz, für den die Neuberechnung calculated_pricenur eine Optimierung darstellt, anstatt unbedingt erforderlich zu sein.

Angenommen currencies, Sie fügen in den Tabellen eine weitere Spalte hinzu, last_ratedie den Wechselkurs zum Zeitpunkt der calculated_priceletzten Aktualisierung enthält, unabhängig davon, wann dies geschehen ist.

Um schnell eine Reihe von Produkten mit einem Preis zwischen beispielsweise 50 USD und 100 USD abzurufen, die die gewünschten Ergebnisse enthalten, können Sie Folgendes tun:

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

wo :last_rateenthält den EUR / USD zum Zeitpunkt der letzten Aktualisierung Wechselkurs. Die Idee ist, das Intervall zu verlängern, um die maximale Variation jeder Währung zu berücksichtigen. Die Erhöhungsfaktoren für beide Enden des Intervalls sind zwischen den Ratenaktualisierungen konstant, sodass sie vorberechnet werden können.

Da sich die Raten über kurze Zeiträume nur geringfügig ändern, wird die obige Abfrage wahrscheinlich eine genaue Annäherung an das Endergebnis liefern. Um das Endergebnis zu erhalten, filtern wir die Produkte heraus, für die die Preise aufgrund der Preisänderungen seit der letzten Aktualisierung von 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;

Wo :current_rateist der aktuellere Kurs mit EUR für das vom Benutzer gewählte Geld?

Die Effizienz ergibt sich aus der Tatsache, dass der Bereich der Raten klein sein soll, wobei die Werte nahe beieinander liegen.


2

Das klingt nach einem Job für eine materialisierte Ansicht. PostgreSQL unterstützt sie zwar nicht explizit, Sie können jedoch materialisierte Ansichten mithilfe von Funktionen und Triggern für normale Tabellen erstellen und verwalten.

Ich würde:

  • Erstellen Sie beispielsweise eine neue Tabelle products_summarymit dem Schema Ihrer aktuellen productsTabelle.
  • ALTER TABLE products DROP COLUMN calculated_pricedie calculated_priceSpalte in loswerdenproducts
  • Schreiben Sie eine Ansicht, die die gewünschte Ausgabe erzeugt, products_summaryindem SELECTSie von productsund JOINan gehen currencies. Ich würde das nennen, products_summary_dynamicaber die Benennung liegt bei Ihnen. Sie können eine Funktion anstelle einer Ansicht verwenden, wenn Sie möchten.
  • Aktualisieren Sie die materialisierte Ansichtstabelle regelmäßig products_summaryvon products_summary_dynamicmit BEGIN; TRUNCATE products_summary; INSERT INTO products_summary SELECT * FROM products_summary_dynamic; COMMIT;.
  • Erstellen Sie einen AFTER INSERT OR UPDATE OR DELETE ON productsTrigger, der eine Triggerprozedur ausführt, um die products_summaryTabelle zu verwalten, Zeilen zu löschen products, wenn sie gelöscht werden , sie hinzuzufügen, wenn sie hinzugefügt werden products(indem SELECTSie sie in der products_summary_dynamicAnsicht anzeigen), und sie zu aktualisieren, wenn sich die Produktdetails ändern.

Dieser Ansatz wird products_summarywährend der TRUNCATE ..; INSERT ...;Transaktion, die die Übersichtstabelle aktualisiert, exklusiv gesperrt . Wenn dies zu Verzögerungen in Ihrer Anwendung führt, weil dies so lange dauert, können Sie stattdessen zwei Versionen der products_summaryTabelle behalten . Aktualisieren Sie die nicht verwendete Datei und anschließend eine TransaktionALTER TABLE products_summary RENAME TO products_summary_old; ALTER TABLE products_summary_new RENAME TO products_summary;


Ein alternativer, aber sehr zweifelhafter Ansatz wäre die Verwendung eines Ausdrucksindex. Da das Aktualisieren der Währungstabelle mit diesem Ansatz wahrscheinlich eine Sperre während eines Zeitraums erfordert DROP INDEXund CREATE INDEXich dies nicht zu oft tun würde - aber möglicherweise für einige Situationen geeignet.

Die Idee ist, Ihre Währungsumrechnung in eine IMMUTABLEFunktion zu packen . Da Sie IMMUTABLEdem Datenbankmodul garantieren, dass der Rückgabewert für ein bestimmtes Argument immer der gleiche ist und dass es kostenlos ist, alle möglichen verrückten Dinge zu tun, wenn der Rückgabewert unterschiedlich ist. Rufen Sie die Funktion auf to_euros(amount numeric, currency char(3)) returns numeric. Implementieren Sie es, wie Sie möchten. eine große CASEAussage nach Währung, eine Nachschlagetabelle, was auch immer. Wenn Sie eine Nachschlagetabelle verwenden , dürfen Sie die Nachschlagetabelle nur wie unten beschrieben ändern .

Erstellen Sie einen Ausdrucksindex für products:

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

Sie können jetzt schnell nach Produkten anhand des berechneten Preises suchen, z.

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

Das Problem wird nun, wie die Währungstabellen aktualisiert werden. Der Trick dabei ist, dass Sie die Währungstabellen ändern können. Sie müssen lediglich den Index löschen und neu erstellen, um dies zu tun.

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;

Ich lasse die obige Funktion fallen und definiere sie neu, nur um zu betonen, dass Sie alles löschen müssen , was die Funktion verwendet, wenn Sie eine unveränderliche Funktion neu definieren. Die Verwendung eines CASCADETropfens ist der beste Weg, dies zu tun.

Ich vermute sehr, dass eine materialisierte Sichtweise der bessere Ansatz ist. Es ist sicherlich das sicherere. Ich schließe dieses hauptsächlich für Tritte ein.


Im Moment denke ich darüber nach - warum sollte ich das calculated_priceüberhaupt aktualisieren ? Ich könnte einfach den initial_currency_value(konstanten Wechselkurs, der heute angenommen wird) speichern und immer dagegen rechnen! Und wenn Sie den Preis in Euro anzeigen, rechnen Sie natürlich mit dem tatsächlichen Wechselkurs. Habe ich recht? Oder gibt es ein Problem, das ich nicht sehe?
Taai

1

Ich hatte meine eigene Idee. Sagen Sie mir bitte, ob es tatsächlich funktionieren wird!

Das Problem.

Wenn das Produkt in die productsTabelle aufgenommen wird, wird der Preis in die Standardwährung (EUR) umgerechnet und in der calculated_priceSpalte gespeichert .

Wir möchten, dass der Benutzer Preise jeder Währung suchen (filtern) kann. Dazu wird der Eingangspreis in die Standardwährung (EUR) umgerechnet und mit der calculated_priceSpalte verglichen .

Wir müssen die Wechselkurse aktualisieren, damit die Benutzer nach neuen Wechselkursen suchen können. Das Problem ist jedoch, wie calculated_priceeffizient aktualisiert werden kann.

Die Lösung (hoffentlich).

So aktualisieren Sie calculated_priceeffizient.

Nicht! :) :)

Die Idee ist , dass wir den gestrigen Wechselkurse (nehmen alle vom selben Tag ) in der calculated_priceVerwendung nur diejenigen. Wie ... für immer! Keine täglichen Updates. Bevor wir die Preise vergleichen / filtern / suchen, müssen wir nur die heutigen Wechselkurse so nehmen, wie sie gestern waren.

Also, calculated_pricewir nur den Wechselkurs des Fixtermin verwenden (wir gewählt haben, sagen wir mal, gestern). Wir müssen den heutigen Preis in den gestrigen Preis umrechnen. Mit anderen Worten, nehmen Sie den heutigen Kurs und konvertieren Sie ihn in den gestrigen Kurs:

cash_in_euros * ( rate_newest / rate_fixed )

Und das ist Tabelle der Währungen:

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

Auf diese Weise können Sie ein Produkt hinzufügen, das 200 USD kostet, und wie das calculated_priceGet berechnet wird: vom USD zum neuesten EUR-Kurs und zum festen (alten) Kurs

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

Dies kann auch auf der Clientseite vorberechnet werden, und das werde ich tun - den Benutzereingabepreis auf den calculated_pricekompatiblen Wert berechnen, bevor wir eine Abfrage durchführen, damit es gut alt verwendet wirdSELECT * FROM products WHERE calculated_price > 100.0 AND calculated_price < 200.0;

Fazit.

Diese Idee ist mir erst vor wenigen Stunden gekommen, und derzeit bitte ich Sie, zu prüfen, ob ich mit dieser Lösung Recht habe. Was denkst du? Wird das funktionieren? Oder habe ich mich geirrt?

Ich hoffe du verstehst das alles. Ich bin kein englischer Muttersprachler, es ist auch spät und ich bin müde. :) :)

AKTUALISIEREN

Nun, es sieht so aus, als würde es ein Problem lösen, aber ein anderes einführen. Schade. :) :)


Das Problem ist, dass das rate_newest / rate_fixedpro Währung unterschiedlich ist und diese Lösung nur das für das vom Benutzer ausgewählte Geld bei der Suche berücksichtigt. Ein Preis in einer anderen Währung würde nicht mit aktuellen Kursen verglichen. Die Antwort, die ich eingereicht habe, hatte ein ähnliches Problem, aber ich glaube, ich habe es in der aktualisierten Version behoben.
Daniel Vérité

Das Hauptproblem, das ich bei diesem Ansatz sehe, besteht darin, dass Datenbankindizes für den Preis nicht genutzt werden (ORDER BY berechne_Preisklauseln).
Rosenfeld
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.