Confronto efficiente dei prezzi in diverse valute


10

Voglio consentire all'utente di cercare prodotti all'interno di una fascia di prezzo. L'utente dovrebbe essere in grado di utilizzare qualsiasi valuta (USD, EUR, GBP, JPY, ...), indipendentemente dalla valuta impostata dal prodotto. Quindi, il prezzo del prodotto è di 200 USD e, se l'utente cerca i prodotti che costano 100EUR - 200EUR, potrebbe ancora trovarlo. Come renderlo veloce ed efficace?

Ecco cosa ho fatto fino ad ora. Conservo il price, currency codee calculated_pricequello è il prezzo in Euro (EUR) che è la valuta di 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';

Se l'utente sta cercando un'altra valuta, diciamo USD, calcoliamo il prezzo in EUR e cerchiamo la calculated_pricecolonna.

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

In questo modo possiamo confrontare i prezzi molto velocemente, perché non abbiamo bisogno di calcolare il prezzo effettivo per ogni riga, perché viene calcolato una volta.

La cosa brutta è che almeno ogni giorno dobbiamo ricalcolare il default_priceper tutte le righe, perché i tassi di cambio sono stati cambiati.

C'è un modo migliore per affrontare questo?

Non c'è qualche altra soluzione intelligente? Forse qualche formula matematica? Ho un'idea che calculated_pricesia un rapporto rispetto ad una variabile Xe, quando la valuta cambia, aggiorniamo solo quella variabile X, non la calculated_price, quindi non abbiamo nemmeno bisogno di aggiornare nulla (righe) ... Forse un matematico può risolverlo come questo?

Risposte:


4

Ecco un approccio diverso per il quale ricalcolare calculated_priceè solo un'ottimizzazione, anziché essere strettamente necessario.

Supponiamo che nelle currenciestabelle si aggiunga un'altra colonna, last_rateche contiene il tasso di cambio al momento calculated_pricedell'ultimo aggiornamento, indipendentemente da quando ciò è avvenuto.

Per recuperare rapidamente un set di prodotti con un prezzo compreso tra, ad esempio, 50 USD e 100 USD che includono i risultati desiderati, puoi fare qualcosa del genere:

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

dove :last_ratecontiene il tasso di cambio EUR / USD al momento dell'ultimo aggiornamento. L'idea è di aumentare l'intervallo per tenere conto della variazione massima di ogni valuta. I fattori di aumento per entrambe le estremità dell'intervallo sono costanti tra gli aggiornamenti delle tariffe, quindi potrebbero essere pre-calcolati.

Poiché le tariffe cambiano solo leggermente in brevi periodi di tempo, è probabile che la query di cui sopra fornisca una stretta approssimazione del risultato finale. Per ottenere il risultato finale, filtriamo i prodotti per i quali i prezzi sono scivolati fuori dai limiti a causa delle variazioni delle tariffe dall'ultimo aggiornamento di 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;

dove si :current_ratetrova il tasso più aggiornato con EUR per i soldi scelti dall'utente.

L'efficienza deriva dal fatto che la gamma di tariffe dovrebbe essere piccola, i valori vicini.


2

Sembra un lavoro per una visione materializzata. Sebbene PostgreSQL non li supporti esplicitamente, è possibile creare e gestire viste materializzate utilizzando funzioni e trigger su tabelle normali.

Vorrei:

  • Crea una nuova tabella, diciamo products_summary, con lo schema della tua productstabella corrente ;
  • ALTER TABLE products DROP COLUMN calculated_priceper sbarazzarsi della calculated_pricecolonnaproducts
  • Scrivi una vista che produce l'uscita che si desidera per products_summaryda SELECTing da productse JOINing on currencies. Lo chiamerei products_summary_dynamicma la denominazione dipende da te. Se lo si desidera, è possibile utilizzare una funzione anziché una vista.
  • Aggiorna periodicamente la tabella di visualizzazione materializzata products_summaryda products_summary_dynamiccon BEGIN; TRUNCATE products_summary; INSERT INTO products_summary SELECT * FROM products_summary_dynamic; COMMIT;.
  • Creare un AFTER INSERT OR UPDATE OR DELETE ON productstrigger che esegue una procedura di trigger per mantenere la products_summarytabella, eliminando le righe da cui vengono eliminati products, aggiungendoli quando aggiunti a products(tramite SELECTla products_summary_dynamicvisualizzazione) e aggiornandoli quando cambiano i dettagli del prodotto.

Questo approccio prevede un blocco esclusivo products_summarydurante la TRUNCATE ..; INSERT ...;transazione che aggiorna la tabella di riepilogo. Se ciò causa blocchi nella tua applicazione perché impiega così tanto tempo, puoi invece mantenere due versioni della products_summarytabella. Aggiorna quello che non è in uso, quindi in una transazioneALTER TABLE products_summary RENAME TO products_summary_old; ALTER TABLE products_summary_new RENAME TO products_summary;


Un approccio alternativo ma molto rischioso sarebbe quello di utilizzare un indice di espressione. Perché l'aggiornamento della tabella delle valute con questo approccio probabilmente richiederà inevitabilmente un blocco durante un DROP INDEXe CREATE INDEXnon lo farei troppo spesso - ma potrebbe essere adatto per alcune situazioni.

L'idea è di racchiudere la conversione di valuta in una IMMUTABLEfunzione. Dal momento IMMUTABLEche stai garantendo al motore di database che il valore di ritorno per ogni dato argomento sarà sempre lo stesso, e che è libero di fare ogni sorta di cose folli se il valore di ritorno differisce. Chiamare la funzione, diciamo, to_euros(amount numeric, currency char(3)) returns numeric. Implementalo come preferisci; una grande CASEdichiarazione per valuta, una tabella di ricerca, qualunque cosa. Se si utilizza una tabella di ricerca non è mai necessario modificare la tabella di ricerca ad eccezione di quanto descritto di seguito .

Crea un indice di espressione su products, come:

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

Ora puoi cercare rapidamente i prodotti in base al prezzo calcolato, ad esempio:

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

Il problema ora diventa come aggiornare le tabelle delle valute. Il trucco qui è che puoi cambiare le tabelle delle valute, devi solo rilasciare e ricreare l'indice per farlo.

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;

Rilascio e ridefinisco la funzione sopra solo per sottolineare che è necessario eliminare tutto ciò che utilizza la funzione se si ridefinisce una funzione immutabile. Usare una CASCADEgoccia è il modo migliore per farlo.

Sospetto fortemente che una visione materializzata sia l'approccio migliore. È sicuramente il più sicuro. Sto includendo questo principalmente per i calci.


In questo momento ci sto pensando: perché dovrei aggiornare il calculated_pricetutto? Potrei semplicemente memorizzare il initial_currency_value(tasso di cambio costante che viene preso, diciamo, oggi) e calcolare sempre contro quello! E quando si visualizza il prezzo in euro, calcolare ovviamente il tasso di cambio effettivo. Ho ragione? O c'è un problema che non vedo?
Taai,

1

Ho avuto la mia idea. Dimmi se funzionerà davvero, per favore!

Il problema.

Quando il prodotto viene aggiunto nella productstabella, il prezzo viene convertito nella valuta predefinita (EUR) e archiviato nella calculated_pricecolonna.

Vogliamo che l'utente possa cercare (filtrare) i prezzi di qualsiasi valuta. Viene fatto convertendo il prezzo di input nella valuta di default (EUR) e confrontandolo con la calculated_pricecolonna.

Dobbiamo aggiornare i tassi di cambio, in modo che gli utenti possano effettuare ricerche per tasso di cambio fresco. Ma il problema è: come aggiornare in modo calculated_priceefficiente.

La soluzione (si spera).

Come aggiornare in modo calculated_priceefficiente.

Non farlo! :)

L'idea è che prendiamo i tassi di cambio di ieri ( tutti della stessa data ) calculated_pricenell'uso solo quelli. Come ... per sempre! Nessun aggiornamento giornaliero. L'unica cosa di cui abbiamo bisogno prima di confrontare / filtrare / cercare i prezzi è prendere i tassi di cambio di oggi come quelli di ieri.

Quindi, in calculated_priceuseremo solo il tasso di cambio della data fissa (abbiamo scelto, diciamo, ieri). Ciò di cui avremo bisogno è convertire il prezzo di oggi nel prezzo di ieri. In altre parole, prendi il tasso di oggi e convertilo nel tasso di ieri:

cash_in_euros * ( rate_newest / rate_fixed )

E questa è la tabella delle valute:

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

Ecco come aggiungere un prodotto che costa 200 USD e come calculated_priceviene calcolato il guadagno: dall'USD alla nuova tariffa EUR e alla tariffa fissa (vecchia)

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

Questo può anche essere pre-calcolato sul lato client ed è quello che ho intenzione di fare: calcolare il prezzo di input dell'utente sul calculated_pricevalore compatibile prima di fare una query, quindi ci sarà usato molto vecchioSELECT * FROM products WHERE calculated_price > 100.0 AND calculated_price < 200.0;

Conclusione.

Questa idea mi è venuta poche ore fa e attualmente ti sto chiedendo di verificare se ho ragione su questa soluzione. Cosa ne pensi? Funzionerà? O ho sbagliato?

Spero tu capisca tutto questo. Non sono di madrelingua inglese, inoltre è tardi e sono stanco. :)

AGGIORNARE

Bene, sembra che risolva un problema, ma ne introduce un altro. Peccato. :)


Il problema è che rate_newest / rate_fixedè diverso per valuta e questa soluzione considera solo quella per il denaro scelto dall'utente nella ricerca. Qualsiasi prezzo in una valuta diversa non verrebbe confrontato con i tassi aggiornati. La risposta che ho inviato in qualche modo ha avuto un problema simile ma penso di averlo risolto nella versione aggiornata.
Daniel Vérité,

Il problema principale che vedo con questo approccio è che non sfrutta gli indici del database sul prezzo (clausole ORDINATE calcolato_prezzo).
rosenfeld,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.