【问题标题】:Django ORM create nested query prevent using `having`Django ORM创建嵌套查询防止使用`have`
【发布时间】:2021-07-12 23:27:17
【问题描述】:

假设我有一个 Transaction 模型,它具有以下字段 [token, pair_token, amount, related_transaction],我想在 mysql 中生成这样的查询:

SELECT token_id, pair_token_id, (
  SELECT ABS(t3.amount / t2.amount) price
    FROM 
    Transaction t2 join Transaction t3 on t2.related_transaction_id=t3.id 
    WHERE t2.id=MAX(t1.id)) 
FROM 
Transaction t1 
WHERE 
t1.token_id in (1, 2, 3, 5, 6) and t2.pair_token_id in (4) and t1.timestamp > CURRENT_TIMESTAMP - interval 24 hour 
GROUP BY
token_id, pair_token_id;

此查询查找两个相关交易的最后一个值,该值等于令牌及其 pair_token 组合的价格。为了在 django 中获得类似的结果,我使用了Subquery,如下所示:

trs = Transaction.objects.filter(token_id__in=[1, 2, 3], pair_token_id__in=[4], timestamp__gt=Now()-timedelta(hours=24))
last_price = Transaction.objects.annotate(price=Abs(F('related_transaction__amount') / F('amount')))
trs = trs.values('token_id', 'pair_token_id').annotate(
    price=Subquery(last_price.filter(id=Max(OuterRef('id'))).values('price'))).\
values('token_id', 'pair_token_id', 'price')

但它会生成错误的查询:

SELECT
`Transaction`.`token_id`, `Transaction`.`pair_token_id`, (
        SELECT ABS((U1.`amount` / U0.`amount`)) AS `price` 
        FROM 
        `Transaction` U0 LEFT OUTER JOIN `Transaction` U1 ON (U0.`related_transaction_id` =U1.`id`) 
        HAVING U0.`id` = MAX(`Transaction`.`id`)) AS `price` 
FROM 
`Transaction` 
WHERE (`Transaction`.`pair_token_id` IN (4) AND `Transaction`.`timestamp` > (CURRENT_TIMESTAMP - INTERVAL 86400000000 MICROSECOND) AND `Transaction`.`token_id` IN (1, 2, 3, 5, 6))

Mysql 为这个查询生成了一个错误,它一定是,我不知道如何避免在使用Subquery 时生成一个having 查询。如果我在没有任何 Subquery 的情况下使用此查询,则会生成 group by 子句,但在使用 Subquery group by term 时将被删除并出现 having。

我正在使用django 3.1.1 和mysql 8.0.19

更新:

交易模型:

class Token:
    name = models.CharField(max_length=20)

class Transaction:
    token = models.ForeignKey(
        Token, blank=False, null=False, on_delete=models.CASCADE
    )
    pair_token = models.ForeignKey(
        Token, blank=True, null=True, on_delete=models.SET_NULL
    )

    related_transaction = models.ForeignKey(
        Transaction, blank=True, null=True, on_delete=models.PROTECT
    )

    amount = models.DecimalFeild(
        max_digits=10, price_decimals=3
    )


t1 = Token.objects.create(name='T1')
t2 = Token.objects.create(name='T2')
t3 = Token.objects.create(name='T3')

tr11 = Transaction.objects.create(
    token=t1, pair_token=t2, amount=Decimal('2.4')
)

tr12 = Transaction.obejcts.create(
    token=t2, pair_token=t1, related_transaction=tr11, amount=Decimal('3')
)

tr21 = Transaction.objects.create(
    token=t1, pair_token=t2, amount=Decimal('1.4')
)

tr22 = Transaction.obejcts.create(
    token=t2, pair_token=t1, related_transaction=tr21, amount=Decimal('3')
)


# If I want to get price of t2 in t1 I must divide tr21.amount / tr22.amount
# Notice that there are many transactions related to each pair and I want to just list of the last price of each pairs.  

【问题讨论】:

  • 你能添加你的模型和示例数据并输出吗?我发现子查询在 MySQL 中执行得非常糟糕,这不是问题吗?
  • 我更新了问题并添加了模型代码,请注意我的模型比这更复杂,但这个简单的模型可以代表我所需要的。我不明白 mysql 中的子查询有什么问题,我在 mysql 中使用这个查询得到了可接受的响应时间,我正在尝试在 django 中生成相同的查询。
  • 这个查询是否给出了你想要的结果? Transaction.objects.annotate(price=Abs(F('related_transaction__amount') / F('amount'))).values('token_id', 'pair_token_id', 'price')。如果您只是注释除法的结果,不确定是否需要子查询?
  • 不,这不会产生我想要的结果。有许多与相同令牌和 pair_token 相关的交易,我只想列出每对的最后价格(与每个令牌和 pair_token 的最大 id 相关)。我编辑了问题以显示更多有用的示例
  • 你能举一个你想要的结果的例子吗?您是否只想要具有该组合的最后价格的唯一令牌和 pair_token 对,还是希望所有交易行都带有最后价格注释?

标签: mysql django django-subquery


【解决方案1】:

以下应该会给你想要的结果。

首先我们创建一个计算交易价格的子查询,通过外部令牌/pair_token 进行过滤,然后进行排序以使最大 id 排在第一位

latest_prices = Transaction.objects.filter(
    related_transaction__isnull=False
).annotate(
    price=Abs(F('related_transaction__amount') / F('amount'))
).filter(
    token=OuterRef('token'),
    pair_token=OuterRef('pair_token')
).order_by('-id')

然后使用这个子查询用 token/pair_token 的最新价格注释每一行,并使用 distinct 获取唯一值

Transaction.objects.filter(
    related_transaction__isnull=False
).annotate(
    price=Subquery(latest_prices.values('price')[:1])
).values('token_id', 'pair_token_id', 'price').distinct()

【讨论】:

  • 这会产生相同的结果,但不如我在问题中显示的结果有效。 Transaction 是一个巨大的表,这会导致每对发生两次嵌套搜索。但在我的请求中,嵌套查询是一个简单直接的查询。我在我的表中都尝试了,并为您的建议获得了 100 个慢速查询。
  • 慢 100 倍是相当糟糕的:D 出于兴趣,你有多少行?使用related_transaction__isnull=False 过滤有帮助吗?这就是我所说的 MySQL 对 Django 生成的子查询表现不佳的意思
  • 它有超过 1000 万行,但基于一些索引和其他一些过滤器,它减少了很多。我可以使用explain 命令来提示您更多关于在返回结果之前检查的行数。但正如我所说,我期望的两个查询之间存在显着差异,而不是您的建议。我必须更正我之前的说法,它慢了大约 20 倍,0.6 秒对 0.03 秒。
猜你喜欢
  • 2018-12-01
  • 2021-09-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多