2012-10-24 3 views
1

J'essaie d'extraire dans une seule requête un ensemble fixe de lignes, plus quelques autres lignes trouvées par une sous-requête. Mon problème est que la requête générée par mon code SQLAlchemy est incorrecte.Comment spécifier les tables FROM dans les sous-requêtes SQLAlchemy?

Le problème est que la requête générée par SQLAlchemy est comme suit:

SELECT tbl.id AS tbl_id 
FROM tbl 
WHERE tbl.id IN 
(
SELECT t2.id AS t2_id 
FROM tbl AS t2, tbl AS t1 
WHERE t2.id = 
(
SELECT t3.id AS t3_id 
FROM tbl AS t3, tbl AS t1 
WHERE t3.id < t1.id ORDER BY t3.id DESC LIMIT 1 OFFSET 0 
) 
AND t1.id IN (4, 8) 
) 
OR tbl.id IN (0, 8) 

alors que la requête correcte ne devrait pas avoir la deuxième tbl AS t1 (le but de cette requête est de sélectionner ID 0 et 8, ainsi comme les ID juste avant 4 et 8).

Malheureusement, je ne peux pas trouver comment obtenir SQLAlchemy pour générer le bon (voir le code ci-dessous).

Des suggestions pour obtenir le même résultat avec une requête plus simple sont également les bienvenues (elles doivent cependant être efficaces - j'ai essayé quelques variantes et certaines étaient beaucoup plus lentes sur mon cas d'utilisation réel).

Le code produisant la requête:

from sqlalchemy import create_engine, or_ 
from sqlalchemy import Column, Integer, MetaData, Table 
from sqlalchemy.orm import sessionmaker 

engine = create_engine('sqlite:///:memory:', echo=True) 
meta = MetaData(bind=engine) 
table = Table('tbl', meta, Column('id', Integer)) 
session = sessionmaker(bind=engine)() 
meta.create_all() 

# Insert IDs 0, 2, 4, 6, 8. 
i = table.insert() 
i.execute(*[dict(id=i) for i in range(0, 10, 2)]) 
print session.query(table).all() 
# output: [(0,), (2,), (4,), (6,), (8,)] 

# Subquery of interest: look for the row just before IDs 4 and 8. 
sub_query_txt = (
     'SELECT t2.id ' 
     'FROM tbl t1, tbl t2 ' 
     'WHERE t2.id = (' 
     ' SELECT t3.id from tbl t3 ' 
     ' WHERE t3.id < t1.id ' 
     ' ORDER BY t3.id DESC ' 
     ' LIMIT 1) ' 
     'AND t1.id IN (4, 8)') 
print session.execute(sub_query_txt).fetchall() 
# output: [(2,), (6,)] 

# Full query of interest: get the rows mentioned above, as well as more rows. 
query_txt = (
     'SELECT * ' 
     'FROM tbl ' 
     'WHERE (' 
     ' id IN (%s) ' 
     'OR id IN (0, 8))' 
     ) % sub_query_txt 
print session.execute(query_txt).fetchall() 
# output: [(0,), (2,), (6,), (8,)] 

# Attempt at an SQLAlchemy translation (from innermost sub-query to full query). 
t1 = table.alias('t1') 
t2 = table.alias('t2') 
t3 = table.alias('t3') 
q1 = session.query(t3.c.id).filter(t3.c.id < t1.c.id).order_by(t3.c.id.desc()).\ 
      limit(1) 
q2 = session.query(t2.c.id).filter(t2.c.id == q1, t1.c.id.in_([4, 8])) 
q3 = session.query(table).filter(
           or_(table.c.id.in_(q2), table.c.id.in_([0, 8]))) 
print list(q3) 
# output: [(0,), (6,), (8,)] 
+0

pouvez-vous expliquer un peu plus sur la requête que vous * voulez *; Les questions que vous tentez d'obtenir ne sont pas claires d'après votre question. Est-ce que 'query_text% sub_query_text' renverra les lignes correctes si elles ont été collées dans l'invite de ligne de commande de votre base de données? – SingleNegationElimination

+0

@TokenMacGuy: Le code inclus montre que 'query_text% sub_query_text' renvoie les résultats corrects. La différence est que la sous-requête dans 'sub_query_text' (le plus interne) n'inclut pas de définition pour' t1'; cela fait partie de la requête externe, et cela change le sens. –

Répondre

2

Ce que vous manquez une corrélation entre la sous-requête interne et le niveau suivant; sans corrélation, SQLAlchemy comprendra les t1 alias dans la sous-requête le plus interne:

>>> print str(q1) 
SELECT t3.id AS t3_id 
FROM tbl AS t3, tbl AS t1 
WHERE t3.id < t1.id ORDER BY t3.id DESC 
LIMIT ? OFFSET ? 
>>> print str(q1.correlate(t1)) 
SELECT t3.id AS t3_id 
FROM tbl AS t3 
WHERE t3.id < t1.id ORDER BY t3.id DESC 
LIMIT ? OFFSET ? 

Notez que tbl AS t1 est maintenant absent de la requête. De l'.correlate() method documentation:

retour d'une construction de requêtes qui corréler la donnée à partir de clauses à celle d'une requête ou renfermant sélectionner().

Par conséquent, t1 est supposé faire partie de la requête englobante et n'est pas répertorié dans la requête elle-même.

Maintenant, votre requête fonctionne:

>>> q1 = session.query(t3.c.id).filter(t3.c.id < t1.c.id).order_by(t3.c.id.desc()).\ 
...    limit(1).correlate(t1) 
>>> q2 = session.query(t2.c.id).filter(t2.c.id == q1, t1.c.id.in_([4, 8])) 
>>> q3 = session.query(table).filter(
...        or_(table.c.id.in_(q2), table.c.id.in_([0, 8]))) 
>>> print list(q3) 
2012-10-24 22:16:22,239 INFO sqlalchemy.engine.base.Engine SELECT tbl.id AS tbl_id 
FROM tbl 
WHERE tbl.id IN (SELECT t2.id AS t2_id 
FROM tbl AS t2, tbl AS t1 
WHERE t2.id = (SELECT t3.id AS t3_id 
FROM tbl AS t3 
WHERE t3.id < t1.id ORDER BY t3.id DESC 
LIMIT ? OFFSET ?) AND t1.id IN (?, ?)) OR tbl.id IN (?, ?) 
2012-10-24 22:16:22,239 INFO sqlalchemy.engine.base.Engine (1, 0, 4, 8, 0, 8) 
[(0,), (2,), (6,), (8,)] 
+0

Génial, merci beaucoup! Et merci aussi (je suppose que c'était vous, sinon merci à quiconque l'a fait) d'avoir édité ma question et d'avoir inclus le code (d'une certaine façon je n'ai pas pu l'afficher correctement, au moins dans l'aperçu). – tiho

+0

@tiho: C'était moi aussi; voir [Comment formater mes blocs de code?] (http://meta.stackexchange.com/q/22186) pour une aide plus détaillée pour la prochaine fois. :-) –

+0

Sur quelle version de SQLAlchemy cette réponse a-t-elle été construite? Je ne peux pas reproduire la sortie du premier échantillon de code avec 1.0.8. (Correspondant à http://stackoverflow.com/questions/32835335/sqlalchemy-correlated-subqueries-not-working) –

1

Je suis seulement un peu que je comprends la question que vous demandez. Permet de décomposer, si:

le but de cette requête est de sélectionner ID 0 et 8, ainsi que les ID juste avant 4 et 8.

On dirait que vous voulez interroger pour deux types de choses, puis les combiner. L'opérateur approprié pour cela est union. Faites les requêtes simples et ajoutez-les à la fin. Je vais commencer par le deuxième bit, "ids juste avant X".

Pour commencer; permet de regarder tous les identifiants qui sont avant une valeur donnée. Pour cela, nous allons joindre à la table sur elle-même avec un <:

# select t1.id t1_id, t2.id t2_id from tbl t1 join tbl t2 on t1.id < t2.id; 
t1_id | t2_id 
-------+------- 
    0 |  2 
    0 |  4 
    0 |  6 
    0 |  8 
    2 |  4 
    2 |  6 
    2 |  8 
    4 |  6 
    4 |  8 
    6 |  8 
(10 rows) 

Cela nous donne certainement toutes les paires de lignes où la gauche est inférieure à la droite.De tous, nous voulons que les lignes pour un t2_id donné soient aussi élevées que possible; Nous allons par groupe t2_id et sélectionnez la t1_id maximale

# select max(t1.id), t2.id from tbl t1 join tbl t2 on t1.id < t2.id group by t2.id; 
max | id 
-----+------- 
    0 |  2 
    2 |  4 
    4 |  6 
    6 |  8 
(4 rows) 

Votre requête, en utilisant un limit, pourrait atteindre cet objectif, mais il est généralement une bonne idée d'éviter d'utiliser cette technique lorsque des alternatives existent parce que le partitionnement n'a pas bon, support portable à travers les implémentations de base de données. Sqlite peut utiliser cette technique, mais postgresql ne l'aime pas, il utilise une technique appelée "requêtes analytiques" (qui sont à la fois standardisées et plus générales). MySQL ne peut ni l'un ni l'autre. La requête ci-dessus, cependant, fonctionne de manière cohérente sur tous les moteurs de base de données SQL.

le reste du travail n'utilise que in ou d'autres requêtes de filtrage équivalentes et n'est pas difficile à exprimer dans sqlalchemy. La plaque passe-partout ...

>>> import sqlalchemy as sa 
>>> from sqlalchemy.orm import Query 
>>> engine = sa.create_engine('sqlite:///:memory:') 
>>> meta = sa.MetaData(bind=engine) 
>>> table = sa.Table('tbl', meta, sa.Column('id', sa.Integer)) 
>>> meta.create_all() 

>>> table.insert().execute([{'id':i} for i in range(0, 10, 2)]) 

>>> t1 = table.alias() 
>>> t2 = table.alias() 

>>> before_filter = [4, 8] 

Premier bit intéressant, nous donnons un nom à l'expression 'max (id)'. cela est nécessaire pour que nous puissions y faire référence plus d'une fois, et pour le sortir d'une sous-requête.

>>> c1 = sa.func.max(t1.c.id).label('max_id') 
>>> #        ^^^^^^ 

La partie « de soulever des objets lourds » de la requête, rejoindre les alias ci-dessus, groupe et sélectionnez le max

>>> q1 = Query([c1, t2.c.id]) \ 
...  .join((t2, t1.c.id < t2.c.id)) \ 
...  .group_by(t2.c.id) \ 
...  .filter(t2.c.id.in_(before_filter)) 

Parce que nous allons utiliser une union, nous avons besoin de cela pour produire le droit nombre de champs: nous l'enveloppons dans une sous-requête et projetons jusqu'à la seule colonne qui nous intéresse. Cela aura le nom que nous lui avons donné dans l'appel label() ci-dessus.

>>> q2 = Query(q1.subquery().c.max_id) 
>>> #       ^^^^^^ 

L'autre moitié du syndicat est beaucoup plus simple:

>>> t3 = table.alias() 
>>> exact_filter = [0, 8] 
>>> q3 = Query(t3).filter(t3.c.id.in_(exact_filter)) 

Tout ce qui reste est de les combiner:

>>> q4 = q2.union(q3) 
>>> engine.execute(q4.statement).fetchall() 
[(0,), (2,), (6,), (8,)] 
+0

Merci, j'apprécie la réponse détaillée (que je n'ai pas le temps de lire entièrement maintenant car je dois courir, mais je vais y jeter un coup d'œil plus tard). – tiho

+0

Lisez-le :) Merci encore, tous les bons points! Quelques commentaires: (1) J'utilise réellement MySQL dans mon vrai code du monde et l'approche LIMIT fonctionne, (2) Pourquoi ne pas simplement sélectionner c1 dans q1 pour éviter d'avoir à supprimer le champ supplémentaire?(3) Je vais devoir le repérer, mais je crois que j'ai d'abord essayé l'approche MAX/GROUP BY et que c'était beaucoup plus lent qu'avec LIMIT (je n'utilisais pas une jointure explicite, ce qui pourrait faire la différence) . – tiho

+0

Donc, juste pour confirmer: pour une raison que je ne comprends pas complètement, l'approche MAX/GROUP BY est beaucoup plus lente (2 min contre 2 sec). C'est sur MySQL avec une table de 10 millions de lignes ('id' est la clé primaire). – tiho

Questions connexes