J'ai affaire à une table Postgres (appelée "lives") qui contient des enregistrements avec des colonnes pour time_stamp, usr_id, transaction_id et lives_remaining. J'ai besoin d'une requête qui me donnera le plus récent total de lives_remaining pour chaque usr_id
exemple:
time_stamp | lives_remaining | usr_id | trans_id -------------------------------------- --- 07:00 | 1 | 1 | 1 09:00 | 4 | 2 | 2 10:00 | 2 | 3 | 3 10:00 | 1 | 2 | 4 11h00 | 4 | 1 | 5 11h00 | 3 | 1 | 6 13h00 | 3 | 3 | 1
Comme je devrai accéder à d'autres colonnes de la ligne avec les dernières données pour chaque usr_id donné, j'ai besoin d'une requête qui donne un résultat comme celui-ci:
time_stamp | lives_remaining | usr_id | trans_id -------------------------------------- --- 11h00 | 3 | 1 | 6 10:00 | 1 | 2 | 4 13h00 | 3 | 3 | 1
Comme mentionné, chaque usr_id peut gagner ou perdre des vies, et parfois ces événements horodatés se produisent si près les uns des autres qu'ils ont le même horodatage! Par conséquent, cette requête ne fonctionnera pas:
SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM
(SELECT usr_id, max(time_stamp) AS max_timestamp
FROM lives GROUP BY usr_id ORDER BY usr_id) a
JOIN lives b ON a.max_timestamp = b.time_stamp
Au lieu de cela, j'ai besoin d'utiliser à la fois time_stamp (premier) et trans_id (deuxième) pour identifier la ligne correcte. J'ai également besoin de transmettre ces informations de la sous-requête à la requête principale qui fournira les données pour les autres colonnes des lignes appropriées. Voici la requête piratée que je me suis mise à travailler:
SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM
(SELECT usr_id, max(time_stamp || '*' || trans_id)
AS max_timestamp_transid
FROM lives GROUP BY usr_id ORDER BY usr_id) a
JOIN lives b ON a.max_timestamp_transid = b.time_stamp || '*' || b.trans_id
ORDER BY b.usr_id
D'accord, donc cela fonctionne, mais je n'aime pas ça. Cela nécessite une requête dans une requête, une auto-jointure, et il me semble que cela pourrait être beaucoup plus simple en saisissant la ligne que MAX a trouvée avoir le plus grand horodatage et trans_id. La table "lives" a des dizaines de millions de lignes à analyser, donc j'aimerais que cette requête soit aussi rapide et efficace que possible. Je suis nouveau dans RDBM et Postgres en particulier, donc je sais que je dois utiliser efficacement les index appropriés. Je suis un peu perdu sur la façon d'optimiser.
J'ai trouvé une discussion similaire ici . Puis-je effectuer un type de Postgres équivalent à une fonction analytique Oracle?
Tout conseil sur l'accès aux informations de colonne associées utilisées par une fonction d'agrégation (comme MAX), la création d'index et la création de meilleures requêtes serait très apprécié!
P.S. Vous pouvez utiliser ce qui suit pour créer mon exemple de cas:
create TABLE lives (time_stamp timestamp, lives_remaining integer,
usr_id integer, trans_id integer);
insert into lives values ('2000-01-01 07:00', 1, 1, 1);
insert into lives values ('2000-01-01 09:00', 4, 2, 2);
insert into lives values ('2000-01-01 10:00', 2, 3, 3);
insert into lives values ('2000-01-01 10:00', 1, 2, 4);
insert into lives values ('2000-01-01 11:00', 4, 1, 5);
insert into lives values ('2000-01-01 11:00', 3, 1, 6);
insert into lives values ('2000-01-01 13:00', 3, 3, 1);
Sur une table avec 158k lignes pseudo-aléatoires (usr_id uniformément répartie entre 0 et 10k, trans_id
Uniformément répartie entre 0 et 30),
Par coût de requête, ci-dessous, je fais référence à l'estimation des coûts de l'optimiseur basé sur les coûts de Postgres (avec les valeurs par défaut de xxx_cost
De Postgres), qui est une estimation de la fonction pondérée des ressources d'E/S et de CPU requises; vous pouvez l'obtenir en lançant PgAdminIII et en exécutant "Query/Explain (F7)" sur la requête avec "Query/Explain options" défini sur "Analyze"
usr_id
, trans_id
, time_stamp
))usr_id
, trans_id
))usr_id
, trans_id
, time_stamp
))usr_id
, EXTRACT(Epoch FROM time_stamp)
, trans_id
)) usr_id
, time_stamp
, trans_id
)); il a l'avantage de balayer la table lives
une seule fois et, si vous augmentez temporairement (si nécessaire) work_mem pour accueillir le tri en mémoire, ce sera de loin le plus rapide de tous requêtes.Toutes les heures ci-dessus incluent la récupération de l'ensemble des résultats de 10 000 lignes.
Votre objectif est un coût minimal estimé et un temps d'exécution des requêtes minimal, en mettant l'accent sur le coût estimé. L'exécution des requêtes peut dépendre de manière significative des conditions d'exécution (par exemple, si les lignes pertinentes sont déjà entièrement mises en cache ou non en mémoire), contrairement à l'estimation des coûts. D'un autre côté, gardez à l'esprit que l'estimation des coûts est exactement cela, une estimation.
Le meilleur temps d'exécution des requêtes est obtenu lors de l'exécution sur une base de données dédiée sans charge (par exemple, en jouant avec pgAdminIII sur un PC de développement). Lorsqu'une requête apparaît légèrement plus rapide (<20%) que l'autre mais a un coût beaucoup plus élevé, il sera généralement plus sage de choisir celle qui a un temps d'exécution plus élevé mais un coût moindre.
Lorsque vous vous attendez à ce qu'il n'y ait pas de concurrence pour la mémoire sur votre machine de production au moment où la requête est exécutée (par exemple, le cache RDBMS et le cache du système de fichiers ne seront pas détruits par des requêtes simultanées et/ou l'activité du système de fichiers), puis l'heure de requête que vous avez obtenue en mode autonome (par exemple pgAdminIII sur un PC de développement) sera représentatif. En cas de conflit sur le système de production, le temps de requête se dégradera proportionnellement au rapport de coût estimé, car la requête avec le moindre coût ne dépend pas autant du cache alors que la requête avec le coût le plus élevé sera revisitez les mêmes données encore et encore (déclenchant des E/S supplémentaires en l'absence d'un cache stable), par exemple:
cost | time (dedicated machine) | time (under load) |
-------------------+--------------------------+-----------------------+
some query A: 5k | (all data cached) 900ms | (less i/o) 1000ms |
some query B: 50k | (all data cached) 900ms | (lots of i/o) 10000ms |
N'oubliez pas d'exécuter ANALYZE lives
Une fois après avoir créé les indices nécessaires.
Requête n ° 1
-- incrementally narrow down the result set via inner joins
-- the CBO may elect to perform one full index scan combined
-- with cascading index lookups, or as hash aggregates terminated
-- by one nested index lookup into lives - on my machine
-- the latter query plan was selected given my memory settings and
-- histogram
SELECT
l1.*
FROM
lives AS l1
INNER JOIN (
SELECT
usr_id,
MAX(time_stamp) AS time_stamp_max
FROM
lives
GROUP BY
usr_id
) AS l2
ON
l1.usr_id = l2.usr_id AND
l1.time_stamp = l2.time_stamp_max
INNER JOIN (
SELECT
usr_id,
time_stamp,
MAX(trans_id) AS trans_max
FROM
lives
GROUP BY
usr_id, time_stamp
) AS l3
ON
l1.usr_id = l3.usr_id AND
l1.time_stamp = l3.time_stamp AND
l1.trans_id = l3.trans_max
Requête n ° 2
-- cheat to obtain a max of the (time_stamp, trans_id) Tuple in one pass
-- this results in a single table scan and one nested index lookup into lives,
-- by far the least I/O intensive operation even in case of great scarcity
-- of memory (least reliant on cache for the best performance)
SELECT
l1.*
FROM
lives AS l1
INNER JOIN (
SELECT
usr_id,
MAX(ARRAY[EXTRACT(Epoch FROM time_stamp),trans_id])
AS compound_time_stamp
FROM
lives
GROUP BY
usr_id
) AS l2
ON
l1.usr_id = l2.usr_id AND
EXTRACT(Epoch FROM l1.time_stamp) = l2.compound_time_stamp[1] AND
l1.trans_id = l2.compound_time_stamp[2]
Mise à jour du 01/01/2013
Enfin, à partir de la version 8.4, Postgres prend en charge fonction Window ce qui signifie que vous pouvez écrire quelque chose d'aussi simple et efficace que:
Requête n ° 3
-- use Window Functions
-- performs a SINGLE scan of the table
SELECT DISTINCT ON (usr_id)
last_value(time_stamp) OVER wnd,
last_value(lives_remaining) OVER wnd,
usr_id,
last_value(trans_id) OVER wnd
FROM lives
WINDOW wnd AS (
PARTITION BY usr_id ORDER BY time_stamp, trans_id
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
);
Je proposerais une version propre basée sur DISTINCT ON
(voir docs ):
SELECT DISTINCT ON (usr_id)
time_stamp,
lives_remaining,
usr_id,
trans_id
FROM lives
ORDER BY usr_id, time_stamp DESC, trans_id DESC;
Voici une autre méthode, qui n'utilise aucune sous-requête corrélée ou GROUP BY. Je ne suis pas expert en optimisation des performances de PostgreSQL, donc je vous suggère d'essayer à la fois cela et les solutions proposées par d'autres personnes pour voir laquelle fonctionne mieux pour vous.
SELECT l1.*
FROM lives l1 LEFT OUTER JOIN lives l2
ON (l1.usr_id = l2.usr_id AND (l1.time_stamp < l2.time_stamp
OR (l1.time_stamp = l2.time_stamp AND l1.trans_id < l2.trans_id)))
WHERE l2.usr_id IS NULL
ORDER BY l1.usr_id;
Je suppose que trans_id
est unique au moins sur toute valeur donnée de time_stamp
.
J'aime le style de réponse de Mike Woodhouse sur l'autre page que vous avez mentionnée. C'est particulièrement concis lorsque la chose agrandie n'est qu'une seule colonne, auquel cas la sous-requête peut simplement utiliser MAX(some_col)
et GROUP BY
Les autres colonnes, mais dans votre cas, vous avez un 2- quantité de pièce à maximiser, vous pouvez toujours le faire en utilisant ORDER BY
plus LIMIT 1
à la place (comme fait par Quassnoi):
SELECT *
FROM lives outer
WHERE (usr_id, time_stamp, trans_id) IN (
SELECT usr_id, time_stamp, trans_id
FROM lives sq
WHERE sq.usr_id = outer.usr_id
ORDER BY trans_id, time_stamp
LIMIT 1
)
Je trouve que l'utilisation de la syntaxe du constructeur de lignes WHERE (a, b, c) IN (subquery)
Bien parce qu'elle réduit la quantité de verbiage nécessaire.
En fait, il existe une solution hacky pour ce problème. Supposons que vous souhaitiez sélectionner le plus grand arbre de chaque forêt dans une région.
SELECT (array_agg(tree.id ORDER BY tree_size.size)))[1]
FROM tree JOIN forest ON (tree.forest = forest.id)
GROUP BY forest.id
Lorsque vous regroupez des arbres par forêt, il y aura une liste d'arbres non triés et vous devez trouver le plus grand. La première chose à faire est de trier les lignes selon leur taille et de sélectionner la première de votre liste. Cela peut sembler inefficace mais si vous avez des millions de lignes, ce sera bien plus rapide que les solutions qui incluent les conditions JOIN
et WHERE
.
BTW, notez que ORDER_BY
pour array_agg
est introduit dans Postgresql 9.0
Il existe une nouvelle option dans Postgressql 9.5 appelée DISTINCT ON
SELECT DISTINCT ON (location) location, time, report
FROM weather_reports
ORDER BY location, time DESC;
Il élimine les lignes en double et ne laisse que la première ligne telle que définie dans la clause ORDER BY.
voir l'officiel documentation
SELECT l.*
FROM (
SELECT DISTINCT usr_id
FROM lives
) lo, lives l
WHERE l.ctid = (
SELECT ctid
FROM lives li
WHERE li.usr_id = lo.usr_id
ORDER BY
time_stamp DESC, trans_id DESC
LIMIT 1
)
Création d'un index sur (usr_id, time_stamp, trans_id)
améliorera considérablement cette requête.
Vous devriez toujours, toujours avoir une sorte de PRIMARY KEY
dans vos tables.
Je pense que vous avez un problème majeur ici: il n'y a pas de "compteur" augmentant de façon monotone pour garantir qu'une ligne donnée s'est produite plus tard dans le temps qu'une autre. Prenez cet exemple:
timestamp lives_remaining user_id trans_id
10:00 4 3 5
10:00 5 3 6
10:00 3 3 1
10:00 2 3 2
Vous ne pouvez pas déterminer à partir de ces données quelle est l'entrée la plus récente. Est-ce le deuxième ou le dernier? Il n'y a pas de fonction de tri ou max () que vous pouvez appliquer à l'une de ces données pour vous donner la bonne réponse.
Augmenter la résolution de l'horodatage serait d'une grande aide. Étant donné que le moteur de base de données sérialise les demandes, avec une résolution suffisante, vous pouvez garantir qu'il n'y aura pas deux horodatages identiques.
Sinon, utilisez un trans_id qui ne se renversera pas pendant très, très longtemps. Avoir un trans_id qui survole signifie que vous ne pouvez pas dire (pour le même horodatage) si trans_id 6 est plus récent que trans_id 1 à moins que vous ne fassiez des calculs compliqués.