在连接上扩展 SQL 查询的最佳实践?

Posted

技术标签:

【中文标题】在连接上扩展 SQL 查询的最佳实践?【英文标题】:Best practice for scaling SQL queries on joins? 【发布时间】:2020-11-06 23:01:45 【问题描述】:

我正在编写一个与 SQL 一起使用的 REST api,并且我经常发现自己处于与此类似的情况,我需要通过查询表连接来返回对象列表以及每个对象内的嵌套列表。

假设我在用户和组之间有一个多对多的关系。我有一个用户表和一个组表以及它们之间的连接表用户组。现在我想编写一个 REST 端点,它返回一个用户列表,以及每个用户他们注册的组。我想返回一个格式如下的 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 的方法有两种或三种:

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

示例(使用 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
    发出固定数量的 SQL 查询:这可以通过发出一个单一的 SQL 查询来完成,该查询执行所有连接,在用户的列中可能存在大量冗余数据,或者通常更优选地,几个单独的 SQL 查询。例如,查询用户列表,然后查询加入 GroupUsers 的组表,然后在服务器代码中手动对组进行分组。

示例代码:

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
    第三种方法,用处有限,是使用 string_agg 或类似的方法来强制 SQL 将分组连接到一个字符串列中,然后将字符串解压缩到服务器端列表中,例如,如果我想要的只是组号我可以使用 string_agg 和 group_by 在对 User 表的一次查询中返回“1,2”。但这仅在您不需要复杂对象时才有用。

我被第二种方法所吸引,因为我觉得它更高效且可扩展,因为 SQL 查询的数量(我认为这是主要瓶颈,没有特别好的理由)是恒定的,但它需要更多的工作在服务器端将所有组过滤到每个用户中。但我认为使用 SQL 的部分目的是利用其高效的排序/过滤功能,因此您不必自己动手。

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

【问题讨论】:

【参考方案1】:

使用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

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

【讨论】:

谢谢,这正是我所需要的。

以上是关于在连接上扩展 SQL 查询的最佳实践?的主要内容,如果未能解决你的问题,请参考以下文章

在 JDBC 中编写 SQL 查询的最佳实践是啥

text sql大数据集查询最佳实践

慢SQL治理最佳实践

MS Access 前端与 SQL Server 后端查询存储最佳实践 [关闭]

AWS Redshift ETL的几个性能最佳实践

SQL n:n - 联结表中查询的最佳实践