扩展 SQL 联接查询的最佳实践?

Best practice for scaling SQL queries on joins?

我正在写一个与 SQL 一起工作的 REST api 并且我经常发现自己处于与此类似的情况,我需要 return 具有嵌套的对象列表通过查询 table 连接在每个对象中列出。

假设我在用户和组之间存在多对多关系。我有一个用户 table 和一个组 table 以及它们之间的连接 table 用户组。现在我想写一个 REST 端点,它 return 是一个用户列表,每个用户都是他们注册的组。我想 return 一个 json 格式如下:

[
    {
        "username": "test_user1",
        <other attributes ...>
        "groups": [
            {
                "group_id": 2,
                <other attributes ...>
            },
            {
                "group_id": 3,
                <other attributes ...>
            }
        ]
    },
    {
        "username": "test_user2",
        <other attributes ...>
        "groups": [
            {
                "group_id": 1,
                <other attributes ...>
            },
            {
                "group_id": 2,
                <other attributes ...>
            }
        ]
    },
    etc ...

我能想到的查询SQL的方法有两三种:

  1. 发出可变数量的 SQL 查询:查询用户列表,然后遍历每个用户以查询联结链接以填充每个用户的组列表。 SQL 查询的数量随着用户数量 returned.
  2. 线性增加

示例(使用 python flask_sqlalchemy / flask_restx):

users = db.session.query(User).filter( ... )
for u in users:
    groups = db.session.query(Group).join(UserGroup, UserGroup.group_id == Group.id) \
        .filter(UserGroup.user.id == u.id)
retobj = api.marshal([{**u.__dict__, 'groups': groups} for u in users], my_model)
# Total number of queries: 1 + number of users in result
  1. 发出固定数量的 SQL 查询:这可以通过发出一个整体 SQL 查询来完成,该查询执行所有连接,用户列中可能有大量冗余数据,或者通常更可取,几个单独的 SQL 查询。比如查询Users列表,然后查询GroupUsers上加入的Grouptable,然后在server代码中手动分组groups。

示例代码:

from collections import defaultdict
users = db.session.query(User).filter( ... )
uids = [u.id for u in users]
groups = db.session.query(User.user_id, Group).join(UserGroup, UserGroup.group_id == Group.id) \
        .filter(UserGroup.user_id._in(uids))
aggregate = defaultdict(list)
for g in groups:
    aggregate[g.user_id].append(g[1].__dict__)
retobj = api.marshal([{**u.__dict__, 'groups': aggregate[u.id]} for u in users], my_model)
# Total number of queries: 2
  1. 第三种方法,用处有限,是使用 string_agg 或类似的方法来强制 SQL 将分组连接到一个字符串列中,然后将字符串解压缩到列表服务器中-一方面,例如,如果我想要的只是组号,我可以使用 string_agg 和 group_by 在对用户 table 的一次查询中返回“1,2”。但这仅在您不需要复杂对象时才有用。

我对第二种方法很感兴趣,因为我觉得它更高效且可扩展,因为 SQL 查询的数量(我没有特别充分的理由认为这是主要瓶颈)是恒定的,但是在服务器端需要做更多的工作才能将所有组过滤到每个用户中。但我认为使用 SQL 的部分意义在于利用它的高效 sorting/filtering 所以你不必自己做。

所以我的问题是,我认为以牺牲更多的服务器端处理和开发时间为代价使 SQL 查询的数量保持不变是个好主意是否正确?尝试减少不必要的 SQL 查询的数量是否浪费时间? API 大规模测试时,如果我不这样做,我会后悔吗?有没有更好的方法来解决我不知道的这个问题?

使用 joinedload 选项,您只需一个查询即可加载所有数据:

q = (
    session.query(User)
    .options(db.joinedload(User.groups))
    .order_by(User.id)
)
users = q.all()
for user in users:
    print(user.name)
    for ug in user.groups:
        print("  ", ug.name)

当您运行上面的查询时,所有的组都已经使用类似于下面的查询从数据库中加载:

SELECT "user".id,
       "user".name,
       group_1.id,
       group_1.name
FROM   "user"
LEFT OUTER JOIN (user_group AS user_group_1
                 JOIN "group" AS group_1 ON group_1.id = user_group_1.group_id)
            ON  "user".id = user_group_1.user_id

现在您只需要使用适当的模式序列化结果。