限制初始查询中的子集合sqlalchemy

时间:2017-05-01 22:11:47

标签: python sqlalchemy limit flask-sqlalchemy declarative

我正在构建一个api,如果用户请求它,它可以返回资源的子节点。例如,usermessages。我希望查询能够限制返回的message个对象的数量。

我找到了一个有用的提示,用于模仿子集合here中的对象数量。基本上,它表示以下流程:

class User(...):
    # ...
    messages = relationship('Messages', order_by='desc(Messages.date)', lazy='dynamic')

user = User.query.one()
users.messages.limit(10)

我的用例涉及有时返回大量用户。

如果我按照该链接中的建议并使用.limit(),那么我需要遍历在每个用户上调用.limit()的整个用户集合。例如,在创建集合的原始sql表达式中使用LIMIT时效率要低得多。

我的问题是,是否可以使用声明来有效地(N + 0)加载大量对象,同时使用sqlalchemy限制子集合中的子项数量?

更新

要明确,以下是我试图避免

users = User.query.all()
messages = {}
for user in users:
    messages[user.id] = user.messages.limit(10).all()

我想做更多的事情:

users = User.query.option(User.messages.limit(10)).all()

4 个答案:

答案 0 :(得分:1)

这个答案来自迈克拜耳sqlalchemy google group。我在这里张贴它来帮助人们: 的 TLDR: 我使用Mike的答案的version 1来解决我的问题,因为在这种情况下,我没有涉及此关系的外键,因此无法使用LATERAL。版本1工作得很好,但请务必注意offset的效果。在测试期间它让我离开了一段时间,因为我没有注意到它被设置为0以外的其他东西。

版本1的代码块

subq = s.query(Messages.date).\
    filter(Messages.user_id == User.id).\
    order_by(Messages.date.desc()).\
    limit(1).offset(10).correlate(User).as_scalar()

q = s.query(User).join(
    Messages,
    and_(User.id == Messages.user_id, Messages.date > subq)
).options(contains_eager(User.messages))

Mike的回答 所以你应该忽略它是否使用“声明”,这与查询无关,事实上最初忽略查询,因为首先这是一个SQL问题。您需要一个执行此操作的SQL语句。 SQL中的哪个查询会从主表中加载大量行,并为每个主表连接到辅助表的前十行?

LIMIT很棘手,因为它实际上并不是通常的“关系代数”计算的一部分。它不在其中,因为它是对行的人为限制。例如,我第一次想到如何做到这一点是错误的:

    select * from users left outer join (select * from messages limit 10) as anon_1 on users.id = anon_1.user_id

这是错误的,因为它只收集聚合中的前十条消息,而忽略了用户。我们希望为每个用户获取前十条消息,这意味着我们需要为每个用户单独“从消息限制10中选择”。也就是说,我们需要以某种方式进行关联。相关子查询通常不允许作为FROM元素,并且只允许作为SQL表达式使用,它只能返回单个列和单个行;我们通常无法在普通的SQL中加入相关的子查询。但是,我们可以在JOIN的ON子句内部进行关联,以便在vanilla SQL中实现这一点。

但首先,如果我们使用的是现代Postgresql版本,我们可以打破通常的相关规则,并使用一个名为LATERAL的关键字,它允许在FROM子句中进行关联。 LATERAL仅受现代Postgresql版本的支持,它使这很容易:

    select * from users left outer join lateral
    (select * from message where message.user_id = users.id order by messages.date desc limit 10) as anon1 on users.id = anon_1.user_id

我们支持LATERAL关键字。上面的查询如下所示:

subq = s.query(Messages).\
    filter(Messages.user_id == User.id).\
    order_by(Messages.date.desc()).limit(10).subquery().lateral()

q = s.query(User).outerjoin(subq).\
     options(contains_eager(User.messages, alias=subq))

注意,在上面,为了选择用户和消息并将它们生成到User.messages集合中,必须使用“contains_eager()”选项,因此“动态”必须消失。这不是唯一的选项,例如,您可以为没有“动态”的User.messages构建第二个关系,或者您可以单独从查询(User,Message)加载并根据需要组织结果元组。 / p>

如果您不使用Postgresql,或者不支持LATERAL的Postgresql版本,则必须将相关性用于连接的ON子句。 SQL看起来像:

select * from users left outer join messages on
users.id = messages.user_id and messages.date > (select date from messages where messages.user_id = users.id order by date desc limit 1 offset 10)

这里,为了将LIMIT卡在那里,我们实际上是使用OFFSET逐步执行前10行,然后执行LIMIT 1以获取表示每个用户所需的下限日期的日期。然后我们必须在该日期进行比较时加入,如果此列未编入索引,则可能会很昂贵,如果存在重复日期,则可能不准确。

此查询如下所示:

subq = s.query(Messages.date).\
    filter(Messages.user_id == User.id).\
    order_by(Messages.date.desc()).\
    limit(1).offset(10).correlate(User).as_scalar()

q = s.query(User).join(
    Messages,
    and_(User.id == Messages.user_id, Messages.date >= subq)
).options(contains_eager(User.messages))

如果没有一个好的测试,这些类型的查询是我不信任的,所以下面的POC包括两个版本,包括健全性检查。

from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
import datetime

Base = declarative_base()


class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    messages = relationship(
        'Messages', order_by='desc(Messages.date)')

class Messages(Base):
    __tablename__ = 'message'
    id = Column(Integer, primary_key=True)
    user_id = Column(ForeignKey('user.id'))
    date = Column(Date)

e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
Base.metadata.drop_all(e)
Base.metadata.create_all(e)

s = Session(e)

s.add_all([
    User(id=i, messages=[
        Messages(id=(i * 20) + j, date=datetime.date(2017, 3, j))
        for j in range(1, 20)
    ]) for i in range(1, 51)
])

s.commit()

top_ten_dates = set(datetime.date(2017, 3, j) for j in range(10, 20))


def run_test(q):
    all_u = q.all()
    assert len(all_u) == 50
    for u in all_u:

        messages = u.messages
        assert len(messages) == 10

        for m in messages:
            assert m.user_id == u.id

        received = set(m.date for m in messages)

        assert received == top_ten_dates

# version 1.   no LATERAL

s.close()

subq = s.query(Messages.date).\
    filter(Messages.user_id == User.id).\
    order_by(Messages.date.desc()).\
    limit(1).offset(10).correlate(User).as_scalar()

q = s.query(User).join(
    Messages,
    and_(User.id == Messages.user_id, Messages.date > subq)
).options(contains_eager(User.messages))

run_test(q)

# version 2.  LATERAL

s.close()

subq = s.query(Messages).\
    filter(Messages.user_id == User.id).\
    order_by(Messages.date.desc()).limit(10).subquery().lateral()

q = s.query(User).outerjoin(subq).\
    options(contains_eager(User.messages, alias=subq))

run_test(q)

答案 1 :(得分:0)

如果您应用限制,然后在其上调用.all(),您将获得所有对象一次,它将不会逐个获取对象,从而导致您提到的性能问题。

只需应用限制并获取所有对象。

users = User.query.limit(50).all()
print(len(users))
>>50

或儿童对象/关系

user = User.query.one()
all_messages = user.messages.limit(10).all()


users = User.query.all()
messages = {}
for user in users:
    messages[user.id] = user.messages.limit(10).all()

答案 2 :(得分:0)

所以,我认为您需要在第二个查询中加载消息,然后以某种方式与您的用户关联。 以下是数据库相关的;作为discussed in this question,mysql不支持带限制的查询,但sqlite至少会解析查询。我没看好计划,看看它是否做得很好。 以下代码将查找您关注的所有消息对象。然后,您需要将它们与用户关联 我已经测试了这个以确认它产生了一个sqlite可以解析的查询;我还没有确认sqlite或任何其他数据库对此查询做了正确的事情。 我不得不作弊并使用text原语来引用select中的外部user.id列,因为SQLAlchemy一直希望在内部select子查询中为用户包含一个额外的连接。

from sqlalchemy import Column, Integer, String, ForeignKey, alias
from sqlalchemy.sql import text

from sqlalchemy.orm import Session
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key = True)
    name = Column(String)

class Message(Base):
    __tablename__ = 'messages'
    user_id = Column(Integer, ForeignKey(User.id), nullable = False)
    id = Column(Integer, primary_key = True)


s = Session()
m1 = alias(Message.__table__)

user_query = s.query(User) # add any user filtering you want
inner_query = s.query(m1.c.id).filter(m1.c.user_id == text('users.id')).limit(10)
all_messages_you_want = s.query(Message).join(User).filter(Message.id.in_(inner_query))

要将消息与用户相关联,您可以执行以下操作,假设您的Message具有用户关系,并且您的用户对象具有got_child_message方法,可以为此执行任何操作

users_resulting = user_query.all() #load objects into session and hold a reference
for m in all_messages_you_want: m.user.got_child_message(m)

由于您已在会话中拥有用户,并且因为关系位于用户的主键上,因此m.user将根据身份映射解析为query.get。 我希望这可以帮助你到达某个地方。

答案 3 :(得分:0)

@melchoirs的答案是最好的。我基本上把这个放在这里供以后使用

我尝试了以上所述的答案,并且确实有效,我更需要它,以便在传递给棉花糖序列化器之前限制返回的关联数。

一些需要澄清的问题:

  • 子查询按关联关系运行,因此它会找到相应的date以正确地作为基础
  • 考虑极限/偏移量,因为从下一个X(偏移量)开始给我1(极限)记录。因此,第X个最旧的记录是什么,然后在主查询中将所有内容从中返回。该死的聪明人
  • 如果关联少于X个记录,则它似乎不返回任何内容,因为偏移量超出了记录,因此,主查询此后不返回任何记录。

使用以上内容作为模板,我想出了以下答案。初始查询/计数保护是由于以下问题:如果关联记录小于偏移量,则不会找到任何内容。另外,如果也没有关联,我需要添加一个externaljoin。

最后,我发现此查询有点像ORM伏都教,但不想走那条路。相反,我从设备序列化器中排除了histories,并要求使用history ID进行第二次device查找。该集可以分页,使所有内容都更加清晰。

这两种方法都可以使用,它只取决于您需要执行一次查询还是执行一对查询的why。在上面,可能是出于商业原因,使用单个查询可以更有效地恢复一切。就我的用例而言,可读性和约定胜过伏都教徒

@classmethod
    def get_limited_histories(cls, uuid, limit=10):

        count = DeviceHistory.query.filter(DeviceHistory.device_id == uuid).count()

        if count > limit:
            sq = db.session.query(DeviceHistory.created_at) \
                .filter(DeviceHistory.device_id == Device.uuid) \
                .order_by(DeviceHistory.created_at.desc()) \
                .limit(1).offset(limit).correlate(Device)


        return db.session.query(Device).filter(Device.uuid == uuid) \
                .outerjoin(DeviceHistory,
                    and_(DeviceHistory.device_id == Device.uuid, DeviceHistory.created_at > sq)) \
                .options(contains_eager(Device.device_histories)).all()[0]

它的行为类似于Device.query.get(id),但Device.get_limited_histories(id)

  • 享受