On Tuesday, 23 February 2016 23:16:25 UTC+8, Mike Bayer wrote:
>
>
> On 02/23/2016 04:00 AM, Daniel Kraus wrote:
> > Here is a simple script to demonstrate the error:
> >
> > https://gist.github.com/dakra/0424086f5837d722bc58
>
> the joinedload() case "works", as long as you don't use LIMIT or OFFSET,
> as there's no subquery:
>
> SELECT SQL_CALC_FOUND_ROWS users.id AS users_id, users.name AS
> users_name, addresses_1.id AS addresses_1_id, addresses_1.email_address
> AS addresses_1_email_address, addresses_1.user_id AS addresses_1_user_id
> FROM users LEFT OUTER JOIN addresses AS addresses_1 ON users.id =
> addresses_1.user_id
>
> but looking at the docs for the purpose of found_rows, it would only be
> used with a LIMIT. Therefore it's not really valid to use this
> function with joined eager loading of a collection because it only works
> at the top level of the query and a joined eager load is going to return
> more rows than there are actual entities. If OTOH you are only
> retrieving a many-to-one via joined eager load, this should all work
> totally fine, and even in the case of LIMIT I don't think a subquery is
> applied for simple many-to-one relationships.
>
> So subqueryload is the only practical option when you need collection
> eager loading plus the found rows feature with limit. In this case you
> definitely don't want this emitted in the subquery because even if it
> were accepted it would mess up your found_rows(). Longer term solution
> here would be to provide flags to the query.prefix_with() method to
> indicate prefixes that should always move to the outside of the query as
> well as prefixes that should not be passed along to subqueryloaders and
> other transformations.
>
> Here's a found_rows modifier that will anticipate a subqueryload and
> erase any _prefixes() nested:
>
> from sqlalchemy.orm.interfaces import MapperOption
> from sqlalchemy.sql import visitors
>
>
> class FoundRows(MapperOption):
> def process_query(self, query):
> query._prefixes = "SQL_CALC_FOUND_ROWS",
>
> def process_query_conditionally(self, query):
> # when subqueryload calls upon loader options, it is passing
> # the fully contructed query w/ the original query already
> # embedded as a core select() object. So we will modify the
> # select() after the fact.
>
> def visit_select(select):
> select._prefixes = ()
>
> # this can be more hardcoded, but here we're searching throughout
> # all select() objects and erasing their _prefixes
> for from_ in query._from_obj:
> visitors.traverse(
> from_, {}, {"select": visit_select})
>
>
> users = s.query(User).options(FoundRows(),
> subqueryload(User.addresses)).limit(3).all()
> row_count = s.execute('SELECT FOUND_ROWS()').scalar()
>
> print(users, row_count)
>
>
Thanks, this does remove the 'SQL_CALC_FOUND_ROWS' from the subquery,
but when I query like you above with option subqueryload, sqlalchemy fires
_2_ queries,
the first one having SQL_CALC_FOUND_ROWS and the second one not, so
s.execute('SELECT FOUND_ROWS()') only returns the found rows for that
second query:
--- cut ---
[...INSERTs...]
2016-02-25 17:20:02,625 INFO sqlalchemy.engine.base.Engine SELECT
SQL_CALC_FOUND_ROWS users.id AS users_id, users.name AS users_name
FROM users
LIMIT %(param_1)s
2016-02-25 17:20:02,625 INFO sqlalchemy.engine.base.Engine {'param_1': 1}
2016-02-25 17:20:02,627 INFO sqlalchemy.engine.base.Engine SELECT
addresses.id AS addresses_id, addresses.email_address AS
addresses_email_address, addresses.user_id AS addresses_user_id,
anon_1.users_id AS anon_1_users_id
FROM (SELECT users.id AS users_id
FROM users
LIMIT %(param_1)s) AS anon_1 INNER JOIN addresses ON anon_1.users_id =
addresses.user_id ORDER BY anon_1.users_id
2016-02-25 17:20:02,627 INFO sqlalchemy.engine.base.Engine {'param_1': 1}
2016-02-25 17:20:02,628 INFO sqlalchemy.engine.base.Engine SELECT
FOUND_ROWS()
2016-02-25 17:20:02,628 INFO sqlalchemy.engine.base.Engine {}
[<__main__.User object at 0x7f66e19a61d0>] 2
--- cut ---
But looking more into it, SQL_CALC_FOUND_ROWS seems to be slower in most
cases anyway. See:
https://www.percona.com/blog/2007/08/28/to-sql_calc_found_rows-or-not-to-sql_calc_found_rows/
https://mariadb.atlassian.net/browse/MDEV-4592 (The related issue is open
since 2009).
So now generate the query and to get the row count I have:
--- cut ---
if model_query.statement._group_by_clause.clauses:
# if there's a GROUP BY we count the slow way:
# SELECT count(*) FROM (SELECT ... FROM Model ... )
row_count =
model_query.limit(None).offset(None).order_by(None).count()
else:
# Remove limit, offset and order by from query and
# SELECT count(DISTINCT Model.id) FROM Model ...
count_query =
model_query.statement.with_only_columns([func.count(distinct(Model.id))])
row_count =
count_query.limit(None).offset(None).order_by(None).scalar()
--- cut ---
Thanks for your fast answer,
Daniel
> > --- cut ---
> > from sqlalchemy import create_engine, Column, Integer, String,
> ForeignKey
> > from sqlalchemy.orm import Session, relationship, subqueryload,
> joinedload
> > from sqlalchemy.ext.declarative import declarative_base
> >
> >
> > Base = declarative_base()
> >
> > e = create_engine("mysql+mysqlconnector://scott:tiger@localhost/test",
> > echo=True)
> >
> > class Address(Base):
> > __tablename__ = 'addresses'
> > id = Column(Integer, primary_key=True)
> > email_address = Column(String(64))
> > user_id = Column(Integer, ForeignKey('users.id'))
> >
> >
> > class User(Base):
> > __tablename__ = 'users'
> >
> > id = Column(Integer, primary_key=True)
> > name = Column(String(64))
> >
> > addresses = relationship(Address, backref="user")
> >
> >
> > Base.metadata.drop_all(e)
> > Base.metadata.create_all(e)
> >
> > s = Session(e)
> >
> >
> > u = User(name='test')
> > s.add_all([u, Address(email_address='email1', user=u),
> > Address(email_address='email2', user=u)])
> > s.commit()
> >
> > # this works like expected
> > users = s.query(User).prefix_with('SQL_CALC_FOUND_ROWS').all()
> > row_count = s.execute('SELECT FOUND_ROWS()').scalar()
> >
> > print(users, row_count)
> >
> > # with eager loading (subqueryload or joinedload) it fails
> > users =
> >
> s.query(User).prefix_with('SQL_CALC_FOUND_ROWS').options(subqueryload(User.addresses)).all()
>
>
> > row_count = s.execute('SELECT FOUND_ROWS()').scalar()
> >
> > print(users, row_count)
> > --- cut ---
> >
> >
> > If I execute, the relevant error message is:
> > sqlalchemy.exc.ProgrammingError:
> > (mysql.connector.errors.ProgrammingError) 1234 (42000): Incorrect
> > usage/placement of 'SQL_CALC_FOUND_ROWS' [SQL: 'SELECT addresses.id AS
> > addresses_id, addresses.email_address AS addresses_email_address,
> > addresses.user_id AS addresses_user_id, anon_1.users_id AS
> > anon_1_users_id \nFROM (SELECT SQL_CALC_FOUND_ROWS users.id AS users_id
> > \nFROM users) AS anon_1 INNER JOIN addresses ON anon_1.users_id =
> > addresses.user_id ORDER BY anon_1.users_id']
> >
> > Thanks,
> > Daniel
>
>
--
You received this message because you are subscribed to the Google Groups
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.