Comment puis-je GROUP BY
Une colonne, tout en triant seulement par une autre.
J'essaie de faire ce qui suit:
SELECT dbId,retreivalTime
FROM FileItems
WHERE sourceSite='something'
GROUP BY seriesName
ORDER BY retreivalTime DESC
LIMIT 100
OFFSET 0;
Je veux sélectionner les derniers /n/éléments de FileItems, dans l'ordre décroissant, avec les lignes filtrées par DISTINCT
valeurs de seriesName
. La requête ci-dessus génère des erreurs ERROR: column "fileitems.dbid" must appear in the GROUP BY clause or be used in an aggregate function
. J'ai besoin de la valeur dbid
afin de prendre ensuite la sortie de cette requête, et JOIN
sur la table source pour obtenir le reste des colonnes que je n'étais pas.
Notez qu'il s'agit essentiellement de la gestalt de la question ci-dessous, avec beaucoup de détails étrangers supprimés pour plus de clarté.
J'ai un système que je migre de sqlite3 vers PostgreSQL, car j'ai largement dépassé sqlite:
SELECT
d.dbId,
d.dlState,
d.sourceSite,
[snip a bunch of rows]
d.note
FROM FileItems AS d
JOIN
( SELECT dbId
FROM FileItems
WHERE sourceSite='{something}'
GROUP BY seriesName
ORDER BY MAX(retreivalTime) DESC
LIMIT 100
OFFSET 0
) AS di
ON di.dbId = d.dbId
ORDER BY d.retreivalTime DESC;
Fondamentalement, je veux sélectionner les derniers n DISTINCT
éléments de la base de données, où la contrainte distincte est sur une colonne, et le tri l'ordre est sur une autre colonne.
Malheureusement, la requête ci-dessus, bien qu'elle fonctionne correctement dans sqlite, génère des erreurs dans PostgreSQL avec l'erreur psycopg2.ProgrammingError: column "fileitems.dbid" must appear in the GROUP BY clause or be used in an aggregate function
.
Malheureusement, tout en ajoutant dbId
à la clause GROUP BY résout le problème (par exemple GROUP BY seriesName,dbId
), Cela signifie que le filtrage distinct sur les résultats de la requête ne fonctionne plus, puisque dbid
est le clé primaire de la base de données, et en tant que telles toutes les valeurs sont distinctes.
De la lecture de la documentation Postgres , il y a SELECT DISTINCT ON ({nnn})
, mais cela nécessite que les résultats retournés soient triés par {nnn}
.
Par conséquent, pour faire ce que je voudrais via SELECT DISTINCT ON
, Je dois interroger tous les DISTINCT {nnn}
Et leur MAX(retreivalTime)
, trier à nouveau par retreivalTime
plutôt que {nnn}
, puis prenez le plus grand 100 et interrogez en utilisant ceux contre la table pour obtenir le reste des lignes, que je ' j'aimerais éviter, car la base de données a ~ 175 Ko lignes et ~ 14 Ko valeurs distinctes dans la colonne seriesName
, je ne veux que les 100 dernières, et cette requête est quelque peu critique en termes de performances (j'ai besoin de temps de requête <1/2 seconde).
Mon hypothèse naïve est que la base de données doit simplement itérer sur chaque ligne dans l'ordre décroissant de retreivalTime
, et s'arrêter simplement une fois qu'elle a vu les éléments LIMIT
, donc une requête de table complète n'est pas idéale, mais je ne prétends pas vraiment comprendre comment le système de base de données est optimisé en interne, et je me trompe peut-être complètement.
FWIW, I do utilise parfois des valeurs OFFSET
différentes, mais de longs délais de requête dans les cas où l'offset> ~ 500 est tout à fait acceptable. Fondamentalement, OFFSET
est un mécanisme de pagination merdique qui me permet de m'en tirer sans avoir à consacrer des curseurs de défilement à chaque connexion, et je vais probablement le revisiter à un moment donné.
Ref - Question que j'ai posée il y a un mois et qui a conduit à cette requête .
Ok, plus de notes:
SELECT
d.dbId,
d.dlState,
d.sourceSite,
[snip a bunch of rows]
d.note
FROM FileItems AS d
JOIN
( SELECT seriesName, MAX(retreivalTime) AS max_retreivalTime
FROM FileItems
WHERE sourceSite='{something}'
GROUP BY seriesName
ORDER BY max_retreivalTime DESC
LIMIT %s
OFFSET %s
) AS di
ON di.seriesName = d.seriesName AND di.max_retreivalTime = d.retreivalTime
ORDER BY d.retreivalTime DESC;
Fonctionne correctement pour la requête comme décrit, mais si je supprime la clause GROUP BY
, Elle échoue (elle est facultative dans mon application).
psycopg2.ProgrammingError: column "FileItems.seriesname" must appear in the GROUP BY clause or be used in an aggregate function
Je pense que je ne comprends pas fondamentalement comment les sous-requêtes fonctionnent dans PostgreSQL. Où est-ce que je vais mal? J'avais l'impression qu'une sous-requête est simplement une fonction en ligne, où les résultats sont simplement introduits dans la requête principale.
La question importante qui ne semble pas encore sur votre radar:
À partir de chaque ensemble de lignes pour le même seriesName
, voulez-vous les colonnes de un ligne, ou juste tout les valeurs de plusieurs lignes (qui peuvent ou non aller de pair)?
Votre réponse fait ce dernier, vous combinez le maximum dbid
avec le maximum retreivaltime
, qui peut provenir d'une ligne différente.
Pour obtenir cohérent lignes, utilisez DISTINCT ON
Et enveloppez-le dans une sous-requête pour classer le résultat différemment:
SELECT * FROM (
SELECT DISTINCT ON (seriesName)
dbid, seriesName, retreivaltime
FROM FileItems
WHERE sourceSite = 'mk'
ORDER BY seriesName, retreivaltime DESC NULLS LAST -- latest retreivaltime
) sub
ORDER BY retreivaltime DESC NULLS LAST
LIMIT 100;
Détails pour DISTINCT ON
:
A part: devrait probablement être retrievalTime
, ou mieux encore: retrieval_time
. Les identificateurs de casse mixte non cotés sont une source courante de confusion dans Postgres.
Puisque nous avons affaire à une grande table ici, nous aurions besoin d'une requête qui peut utiliser un index, ce qui n'est pas le cas pour la requête ci-dessus (sauf pour WHERE sourceSite = 'mk'
)
En y regardant de plus près, votre problème semble être un cas particulier de balayage d'index lâche . Postgres ne prend pas en charge nativement les analyses d'index lâches, mais il peut être émulé avec un CTE récursif . Il y a un exemple de code pour le cas simple dans le wiki Postgres.
Réponse associée sur SO avec des solutions plus avancées, explication, violon:
Votre cas est cependant plus complexe. Mais je pense que j'ai trouvé une variante pour le faire fonctionner pour vous. Construire sur cet index (sans WHERE sourceSite = 'mk'
)
CREATE INDEX mi_special_full_idx ON MangaItems
(retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST, dbid)
Ou (avec WHERE sourceSite = 'mk'
)
CREATE INDEX mi_special_granulated_idx ON MangaItems
(sourceSite, retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST, dbid)
Le premier index peut être utilisé pour les deux requêtes, mais n'est pas entièrement efficace avec la condition WHERE supplémentaire. Le deuxième index est d'une utilité très limitée pour la première requête. Étant donné que vous disposez des deux variantes de la requête, pensez à créer les deux index.
J'ai ajouté dbid
à la fin pour permettre Index uniquement scans .
Cette requête avec un CTE récursif utilise l'index. J'ai testé avec Postgres 9.3 et cela fonctionne pour moi: pas de scan séquentiel, tout index seulement scans:
WITH RECURSIVE cte AS (
(
SELECT dbid, seriesName, retreivaltime, 1 AS rn, ARRAY[seriesName] AS arr
FROM MangaItems
WHERE sourceSite = 'mk'
ORDER BY retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST
LIMIT 1
)
UNION ALL
SELECT i.dbid, i.seriesName, i.retreivaltime, c.rn + 1, c.arr || i.seriesName
FROM cte c
, LATERAL (
SELECT dbid, seriesName, retreivaltime
FROM MangaItems
WHERE (retreivaltime, seriesName) < (c.retreivaltime, c.seriesName)
AND sourceSite = 'mk' -- repeat condition!
AND seriesName <> ALL(c.arr)
ORDER BY retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST
LIMIT 1
) i
WHERE c.rn < 101
)
SELECT dbid
FROM cte
ORDER BY rn;
Vous avez besoin d'inclure seriesName
dans ORDER BY
, Puisque retreivaltime
n'est pas unique. "Presque" unique est toujours pas unique.
La requête non récursive commence par la dernière ligne.
La requête récursive ajoute la ligne suivante avec un seriesName
qui n'est pas dans la liste, etc., jusqu'à ce que nous ayons 100 lignes.
Les parties essentielles sont la condition JOIN
(b.retreivaltime, b.seriesName) < (c.retreivaltime, c.seriesName)
Et la clause ORDER BY
ORDER BY retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST
. Les deux correspondent à l'ordre de tri de l'index, ce qui permet à la magie de se produire.
Collecte de seriesName
dans un tableau pour éliminer les doublons. Le coût de b.seriesName <> ALL(c.foo_arr)
augmente progressivement avec le nombre de lignes, mais pour seulement 100 lignes, il reste bon marché.
Renvoyer simplement dbid
comme expliqué dans les commentaires.
Nous avons déjà été confrontés à des problèmes similaires. Voici une solution complète hautement optimisée basée sur des index partiels et une fonction de bouclage:
Probablement le moyen le plus rapide (sauf pour une vue matérialisée) s'il est bien fait. Mais plus complexe.
Comme vous n'avez pas beaucoup d'opérations d'écriture et qu'elles ne sont pas critiques en termes de performances comme indiqué dans les commentaires (devrait être dans la question), save les n premières lignes pré-calculées dans une vue matérialisée et actualisez-la après les modifications pertinentes de la table sous-jacente. Basez plutôt vos requêtes critiques sur les performances sur la vue matérialisée.
Pourrait être juste un mv "mince" des derniers 1000 dbid
ou plus. Dans la requête, rejoignez la table d'origine. Par exemple, si le contenu est parfois mis à jour, mais les n premières lignes peuvent rester inchangées.
Ou un mv "gras" avec des rangées entières pour revenir. Plus rapide encore. A besoin d'être rafraîchi plus souvent, évidemment.
Ok, j'ai lu plus les documents, et maintenant je comprends le problème au moins un peu mieux.
Fondamentalement, ce qui se passe est qu'il existe plusieurs valeurs possibles pour dbid
à la suite du GROUP BY seriesName
agrégation. Avec SQLite et MySQL, apparemment le moteur de base de données en choisit juste un au hasard (ce qui est tout à fait correct dans mon application).
Cependant, PostgreSQL est beaucoup plus conservateur, donc plutôt que de choisir une valeur aléatoire, il génère une erreur.
Un moyen simple de faire fonctionner cette requête consiste à appliquer une fonction d'agrégation à la valeur appropriée:
SELECT MAX(dbid) AS mdbid, seriesName, MAX(retreivaltime) AS mrt
FROM MangaItems
WHERE sourceSite='mk'
GROUP BY seriesName
ORDER BY mrt DESC
LIMIT 100
OFFSET 0;
Cela rend la sortie de la requête pleinement qualifiée et la requête fonctionne maintenant.
Eh bien, j'ai fini par utiliser une logique procédurale en dehors de la base de données pour accomplir ce que je voulais faire.
Fondamentalement, 99% du temps, je veux que le dernier 100 200 résultats. Le planificateur de requêtes ne semble pas être optimisé pour cela, et si la valeur de OFFSET
est grande, mon filtre procédural sera beaucoup plus lent.
Quoi qu'il en soit, j'ai utilisé un curseur nommé pour parcourir manuellement les lignes de la base de données, récupérant les lignes par groupes de quelques centaines. Je les filtre ensuite par distinction dans mon code d'application et ferme le curseur immédiatement après avoir accumulé le nombre de résultats distincts que je voulais.
Le code mako
(essentiellement python). Beaucoup d'instructions de débogage restantes.
<%def name="fetchMangaItems(flags='', limit=100, offset=0, distinct=False, tableKey=None, seriesName=None)">
<%
if distinct and seriesName:
raise ValueError("Cannot filter for distinct on a single series!")
if flags:
raise ValueError("TODO: Implement flag filtering!")
whereStr, queryAdditionalArgs = buildWhereQuery(tableKey, None, seriesName=seriesName)
params = Tuple(queryAdditionalArgs)
anonCur = sqlCon.cursor()
anonCur.execute("BEGIN;")
cur = sqlCon.cursor(name='test-cursor-1')
cur.arraysize = 250
query = '''
SELECT
dbId,
dlState,
sourceSite,
sourceUrl,
retreivalTime,
sourceId,
seriesName,
fileName,
originName,
downloadPath,
flags,
tags,
note
FROM MangaItems
{query}
ORDER BY retreivalTime DESC;'''.format(query=whereStr)
start = time.time()
print("time", start)
print("Query = ", query)
print("params = ", params)
print("tableKey = ", tableKey)
ret = cur.execute(query, params)
print("Cursor ret = ", ret)
# for item in cur:
# print("Row", item)
seenItems = []
rowsBuf = cur.fetchmany()
rowsRead = 0
while len(seenItems) < offset:
if not rowsBuf:
rowsBuf = cur.fetchmany()
row = rowsBuf.pop(0)
rowsRead += 1
if row[6] not in seenItems or not distinct:
seenItems.append(row[6])
retRows = []
while len(seenItems) < offset+limit:
if not rowsBuf:
rowsBuf = cur.fetchmany()
row = rowsBuf.pop(0)
rowsRead += 1
if row[6] not in seenItems or not distinct:
retRows.append(row)
seenItems.append(row[6])
cur.close()
anonCur.execute("COMMIT;")
print("duration", time.time()-start)
print("Rows used", rowsRead)
print("Query complete!")
return retRows
%>
</%def>
Ceci récupère actuellement la dernière 100 200 éléments de série distincts dans 115 ~ 80 millisecondes (le temps le plus court est lorsque vous utilisez une connexion locale, plutôt qu'un TCP), tout en traitant environ 1500 lignes.
Venez commentaires:
buildWhereQuery
est mon propre générateur de requêtes dynamiques. Oui, c'est une horrible idée. Oui, je connais SQLalchemy et al. J'ai écrit le mien parce que A. c'est un projet personnel que je ne m'attends pas à utiliser en dehors de mon réseau local domestique, et B. C'est une excellente façon d'apprendre SQL.autocommit
off). Je dois instancier un curseur anonyme, émettre du SQL (juste un BEGIN
, ici), créer mon curseur nommé, l'utiliser, le fermer et enfin valider avec le curseur anonyme.