内连接子查询 Django ORM 等效

Posted

技术标签:

【中文标题】内连接子查询 Django ORM 等效【英文标题】:Inner join subquery Django ORM equivalent 【发布时间】:2020-05-21 17:24:39 【问题描述】:

我有三个相关的表:

模块
+-------------+-------------+------+-----+---------+-------+
| Field       | Type        | Null | Key | Default | Extra |
+-------------+-------------+------+-----+---------+-------+
| module_id   | int(11)     | NO   | PRI | NULL    |       |
+-------------+-------------+------+-----+---------+-------+
活动
+------------------+--------------------------+------+-----+---------+----------------+
| Field            | Type                     | Null | Key | Default | Extra          |
+------------------+--------------------------+------+-----+---------+----------------+
| event_id         | int(11)                  | NO   | PRI | NULL    | auto_increment |
| event_time       | datetime(4)              | NO   |     | NULL    |                |
| module_id        | int(11)                  | NO   | MUL | NULL    |                |
| file_id          | int(11)                  | YES  | MUL | NULL    |                |
+------------------+--------------------------+------+-----+---------+----------------+
文件
+--------------+-----------------------------+------+-----+---------+----------------+
| Field        | Type                        | Null | Key | Default | Extra          |
+--------------+-----------------------------+------+-----+---------+----------------+
| file_id      | int(11)                     | NO   | PRI | NULL    | auto_increment |
| path         | varchar(512)                | NO   | UNI | NULL    |                |
+--------------+-----------------------------+------+-----+---------+----------------+

所以,有模块事件文件。 (为了简化,未使用的字段已从表中删除)。

目标: 我想获取每个模块上发生的最新事件及其文件路径

我尝试了什么: 所以,为此,起初我使用子查询在 Django 上创建了一个简单的实现:

last_event_subquery = Event.objects.filter(
        module_id__module_id=OuterRef('module__id')
    ).order_by('-event_time', '-event_id')

modules = Module.objects.all().annotate(
        last_event_path=Subquery(last_event_subquery.values('file_id__path')[:1])
    ).annotate(
        last_event_id=Subquery(last_event_subquery.values('event_id')[:1])
    ).annotate(
        last_event_datetime=Subquery(last_event_subquery.values('event_time')[:1])
    )

但是,我发现在事件表中运行超过 100 万条记录 的速度非常慢。当然,那里有几个索引可以优化所有东西,但即使这样我也找不到运行时间少于 5 秒的索引组合,这太多了imo。然后,我看到了原因,等价的SQL查询太傻了:

SELECT `module`.`module_id`,
       (SELECT U2.`path` FROM `events` U0 LEFT OUTER JOIN `files` U2 ON (U0.`file_id` = U2.`file_id`)
       WHERE U0.`module_id` = (`modules`.`module_id`) ORDER BY U0.`event_time` DESC, U0.`event_id` DESC  LIMIT 1)
       AS `last_event_path`,
       (SELECT U0.`event_id` FROM `events` U0
       WHERE U0.`module_id` = (`modules`.`module_id`) ORDER BY U0.`event_time` DESC, U0.`event_id` DESC  LIMIT 1)
       AS `last_event_id`,
       (SELECT U0.`event_time` FROM `events` U0
       WHERE U0.`module_id` = (`modules`.`module_id`) ORDER BY U0.`event_time` DESC, U0.`event_id` DESC  LIMIT 1)
       AS `last_event_time` FROM `events`

如你所见,它重复了子查询 3 次。

所以,我决定尽我所能在 SQL 中尝试一下,但我努力让以下工作:

SELECT module.module_id,
       events.event_id,
       events.event_time,
       files.path
       FROM modules INNER JOIN events ON events.event_id =
           (SELECT events.event_id FROM events
            WHERE modules.module_id = events.module_id
                ORDER BY events.event_time DESC, events.event_id DESC LIMIT 1)
       INNER JOIN files ON files.file_id = events.file_id;

运行时间为 0.001 秒。所以,现在的问题是我无法用 Django ORM 语言完成这项工作。当然,我可以只放置原始 SQL 查询,我就完成了,但是我怎么能忍受这样的耻辱呢?

我调查了整个 Django 文档,在 *** 问题上苦苦挣扎,但我找不到答案。我得到的最接近的是this,但问题是我无法将其限制为每个模块一个结果。

我也尝试过 FilteredRelation,但无法获得合适的过滤器。 我也不能使用 select_related(),因为它与 ForeignKey 是反向关系。 我不能将 distinct() 与列字段一起使用,因为我使用的是 mysql(更具体地说,MariaDB 版本 10.3)。

你对如何解决这个问题有什么建议吗?

谢谢!

【问题讨论】:

【参考方案1】:

所以,我自己找到了答案:)

它会生成我想要的完全相同的 SQL,除了子句的顺序,这根本不影响查询。关键是使用 .filter() 与关系。使用 filter() 中的子查询(我不知道这是可能的)取得了成功。我的灵感来自this answer。

last_event_subquery = Event.objects.filter(
    module_id__module_id=OuterRef('module_id')
).order_by('-event_time', '-event_id')
modules = Module.objects.filter(
    event__event_id=Subquery(
        last_event_subquery.values('module_id')[:1])
).values('id', 'event__event_id', 'event__event_time', 'event__file__path')

产生以下 SQL:

SELECT `modules`.`module_id`,
       `events`.`event_id`,
       `events`.`event_time`,
       `files`.`path`
       FROM `modules`
       INNER JOIN `events` ON (`modules`.`module_id` = `events`.`module_id`)
       LEFT OUTER JOIN `files` ON (`events`.`file_id` = `files`.`file_id`)
       WHERE `events`.`event_id` =
            (SELECT U0.`event_id` FROM `events` U0
            WHERE U0.`module_id` = (`modules`.`module_id`)
                 ORDER BY U0.`event_time` DESC, U0.`event_id` DESC  LIMIT 1)

我希望这对其他人有用。

【讨论】:

该子查询可能受益于INDEX(module_id, event_time, event_id) 是的,当然。现在我正在使用 INDEX(module_id, event_time),但我没有包含 event_id,我也会尝试添加它!谢谢你:) 如果event_id 是表的PRIMARY KEY,那么它已经隐式地存在了。

以上是关于内连接子查询 Django ORM 等效的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 sequelize ORM 编写内连接查询?

通过 orm 的 django 子查询

Django 内连接查询集

sql子查询和连接查询的区别是啥呢?

MySQL多表连接查询 内连接 外连接 子查询

使用内连接代替子查询