Cela dépend beaucoup des circonstances et des exigences exactes. Considérez mon commentaire à la question .
Solution simple
Avec DISTINCT ON
dans Postgres:
SELECT DISTINCT ON (i.good, i.the_date)
i.the_date, p.the_date AS pricing_date, i.good, p.price
FROM inventory i
LEFT JOIN price p ON i.good = p.good AND i.the_date >= p.the_date
ORDER BY i.good, i.the_date, p.the_date DESC;
Résultat commandé.
Ou avec NOT EXISTS
en SQL standard (fonctionne avec tous les SGBDR que je connais):
SELECT i.the_date, p.the_date AS pricing_date, i.good, i.quantity, p.price
FROM inventory i
LEFT JOIN price p ON p.good = i.good AND p.the_date <= i.the_date
WHERE NOT EXISTS (
SELECT 1 FROM price p1
WHERE p1.good = p.good
AND p1.the_date <= i.the_date
AND p1.the_date > p.the_date
);
Même résultat, mais avec un ordre de tri arbitraire - sauf si vous ajoutez ORDER BY
.
En fonction de la distribution des données, des exigences exactes et des index, l'un ou l'autre peut être plus rapide.
En général, DISTINCT ON
c'est le vainqueur et vous obtenez un résultat trié en plus. Mais dans certains cas, d’autres techniques d’interrogation sont (beaucoup) plus rapides, pour le moment. Voir ci-dessous.
Les solutions avec sous-requêtes pour calculer les valeurs max / min sont généralement plus lentes. Les variantes avec CTE sont généralement plus lentes encore.
Les vues claires (comme celles proposées par une autre réponse) n’aident pas du tout les performances dans Postgres.
Fiddle SQL.
Bonne solution
Cordes et collation
Tout d'abord, vous souffrez d'une disposition de table sous-optimale. Cela peut sembler trivial, mais normaliser votre schéma peut aller très loin.
Tri par types de caractères ( text
, varchar
...) doit être fait selon les paramètres locaux - le COLLATIONNEMENT en particulier. Il est fort probable que votre base de données utilise un ensemble de règles locales (comme dans mon cas:) de_AT.UTF-8
. Découvrez avec:
SHOW lc_collate;
Cela rend le tri et l' index look-ups plus lent . Plus vos ficelles (noms de marchandises) sont longues, plus les choses vont mal. Si vous ne vous souciez pas réellement des règles de classement dans votre sortie (ou de l'ordre du tri), cela peut être plus rapide si vous ajoutez COLLATE "C"
:
SELECT DISTINCT ON (i.good COLLATE "C", i.the_date)
i.the_date, p.the_date AS pricing_date, i.good, p.price
FROM inventory i
LEFT JOIN price p ON i.good = p.good AND i.the_date >= p.the_date
ORDER BY i.good COLLATE "C", i.the_date, p.the_date DESC;
Notez comment j'ai ajouté le classement à deux endroits.
Deux fois plus vite dans mon test avec 20k lignes chacune et des noms très basiques ('good123').
Indice
Si votre requête est supposée utiliser un index, les colonnes contenant des données de type caractères doivent utiliser un classement correspondant ( good
dans l'exemple):
CREATE INDEX inventory_good_date_desc_collate_c_idx
ON price(good COLLATE "C", the_date DESC);
Assurez-vous de lire les deux derniers chapitres de cette réponse sur SO:
Vous pouvez même avoir plusieurs index avec des classements différents sur les mêmes colonnes - si vous avez également besoin de marchandises triées selon un autre classement (ou le classement par défaut) dans d'autres requêtes.
Normaliser
Les chaînes redondantes (nom du bien) gonflent également vos tables et index, ce qui ralentit encore le processus. Avec une bonne disposition de la table, vous pourriez éviter la plupart des problèmes. Pourrait ressembler à ceci:
CREATE TABLE good (
good_id serial PRIMARY KEY
, good text NOT NULL
);
CREATE TABLE inventory (
good_id int REFERENCES good (good_id)
, the_date date NOT NULL
, quantity int NOT NULL
, PRIMARY KEY(good_id, the_date)
);
CREATE TABLE price (
good_id int REFERENCES good (good_id)
, the_date date NOT NULL
, price numeric NOT NULL
, PRIMARY KEY(good_id, the_date));
Les clés primaires fournissent automatiquement (presque) tous les index dont nous avons besoin.
Selon les éléments manquants, un indice de multicolumn sur price
avec ordre décroissant sur la deuxième colonne peut améliorer les performances:
CREATE INDEX price_good_date_desc_idx ON price(good, the_date DESC);
Encore une fois, le classement doit correspondre à votre requête (voir ci-dessus).
Dans Postgres 9.2 ou version ultérieure, des "indices de couverture" pour les analyses d'index pourraient en aider davantage, en particulier si vos tables contiennent des colonnes supplémentaires, ce qui la rend nettement plus grande que l'indice de couverture.
Ces requêtes résultantes sont beaucoup plus rapides:
N'EXISTE PAS
SELECT i.the_date, p.the_date AS pricing_date, g.good, i.quantity, p.price
FROM inventory i
JOIN good g USING (good_id)
LEFT JOIN price p ON p.good_id = i.good_id AND p.the_date <= i.the_date
AND NOT EXISTS (
SELECT 1 FROM price p1
WHERE p1.good_id = p.good_id
AND p1.the_date <= i.the_date
AND p1.the_date > p.the_date
);
DISTINCT ON
SELECT DISTINCT ON (i.the_date)
i.the_date, p.the_date AS pricing_date, g.good, i.quantity, p.price
FROM inventory i
JOIN good g USING (good_id)
LEFT JOIN price p ON p.good_id = i.good_id AND p.the_date <= i.the_date
ORDER BY i.the_date, p.the_date DESC;
Fiddle SQL.
Des solutions plus rapides
Si cela n’est toujours pas assez rapide, il pourrait y avoir des solutions plus rapides.
CTE récursif / JOIN LATERAL
/ sous-requête corrélée
Surtout pour les distributions de données avec beaucoup de prix par bien :
Vue matérialisée
Si vous devez exécuter cela souvent et rapidement, je vous suggère de créer une vue matérialisée. Je pense qu'il est prudent de supposer que les prix et les stocks pour les dates antérieures changent rarement. Calculez le résultat une fois et stockez un instantané sous forme de vue matérialisée.
Postgres 9.3+ prend en charge automatiquement les vues matérialisées. Vous pouvez facilement implémenter une version de base dans les anciennes versions.