DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

SQLAlchemy count 返回 100,all 却只有一个实体

区分关联后的 SQL 行与 Query 按身份去重的实体,将父主键去重放在分页之前,并保留现代 Result 接口和复合主键边界。

SQLAlchemy 的关联查询里,query.limit(100).count() 返回 100,紧接着 query.limit(100).all() 却只有一个对象。增加 SQL DISTINCT 后列表变长,容易让人怀疑 ORM 少读了数据。

使用传统 Session.query(Model) 接口返回完整实体时,SQLAlchemy 会按实体身份去重。关联后的 100 行若都属于同一个父记录,SQL 的行数是 100,结果列表中的父实体仍可以只有一个。Query.count() 统计查询行数,不等于实体去重后的数量。

下面限定 SQLAlchemy 1.4,以及 2.0 中保留的 legacy Query API;现代 select() 与 Session.execute() 的结果默认不采用同样的自动去重规则。

先检查关联行中的父主键

假设已有 Parent、Child 映射,Child.parent_id 引用 Parent.id,父表使用单列主键。以下片段使用应用已有的 session,不创建数据库或写入样例数据:

q = session.query(Parent).join(
    Child, Child.parent_id == Parent.id
)

rows = (
    session.query(Parent.id, Child.id)
    .join(Child, Child.parent_id == Parent.id)
    .order_by(Parent.id, Child.id)
    .limit(20)
    .all()
)

实际检查必须保留原查询的租户、权限、状态等筛选条件。观察有限样本里的父主键是否重复:例如二十行中 Parent.id 都相同,而 Child.id 不同,就说明 SQL 返回了多个关联行,不是二十个不同父实体。

查询具体列时,这些重复父主键仍会保留;查询完整 Parent 实体时,传统 Query 才会按身份合并。不要为了让列表长度等于 count(),把 ORM 主键映射改成不符合真实身份的字段。

以父实体为分页单位,就先形成不同父键

如果页面要展示“有符合条件子记录的父记录”,总数和分页都应基于不同的父主键,而不是对关联行先切 100 行、再在 Python 端合并。

下面继续使用已有映射和会话,Child.active 是示例筛选列。实际使用时替换为完整业务筛选;示例采用单列非空主键:

from sqlalchemy import func

parent_ids = (
    session.query(Parent.id.label('parent_id'))
    .join(Child, Child.parent_id == Parent.id)
    .filter(Child.active.is_(True))
    .distinct()
)

id_set = parent_ids.subquery()
total = session.query(func.count()).select_from(id_set).scalar()

page_ids = (
    parent_ids.order_by(Parent.id)
    .offset(0)
    .limit(100)
    .subquery()
)

items = (
    session.query(Parent)
    .join(page_ids, Parent.id == page_ids.c.parent_id)
    .order_by(Parent.id)
    .all()
)

去重发生在 SQL 的主键集合中,随后才按父主键排序和分页。外层重新连接父表,没有再次连接一对多子表去扩展行。总数未套分页限制;total 与 items 的长度也不应在超过一页时直接比较。

若需要按子记录的时间排序,必须先决定一个父记录采用哪一条子记录的排序值,再设计对应查询。不能直接把多个不同的子列塞进 DISTINCT 查询,以为父键仍然唯一。复合主键则需选择完整主键组合,并在外层按所有键连接,不能只选其中一列。

总数与页面查询是两次读取,并发写入可能让它们看到不同的数据。需要一致快照时应结合实际数据库的事务隔离规则处理;主键去重只解决统计单位和分页单位不一致,不保证并发期间总数不变。

要的是每条关联记录,就返回相应列

若业务本来要展示父子配对,多个子记录对应同一个父记录是有效结果,可以直接查询所需列或父子实体组合,并为每行明确身份。不要再把单一父实体列表当成关联明细。

也不要通过加载全量 .all() 后再取 len() 来修总数。这样既改变资源使用方式,也没有解决“先 LIMIT 关联行再合并”的分页问题;应在数据库中明确统计的对象。

迁移到 SQLAlchemy 2.0 风格时,还需重新核对结果接口。session.execute(select(...)) 默认不自动对 ORM 行去重;需要时显式使用 unique()。其中集合型 joinedload() 有要求显式去重的规则。对已经带 SQL LIMIT 的结果调用 unique(),同样不能补回被 LIMIT 排除的其他父记录。

验收时准备一个父记录对应多个子记录、多个父记录均有子记录,以及筛选后为空的情况。检查总数、跨页主键及最后一页,确认没有因重复关联行导致页内对象不足。保存原查询和排序契约便于回退,不通过删除子记录或改动主键来掩盖查询单位的问题。