Rails 修改订单时中断 SQL 查询

Rails is breaking SQL query when modifying order

上下文

我们有一个 Rails 应用程序正在检索与以下原始 SQL 查询的对话:

SELECT sub.*,
       profiles.status AS interlocutor_status
FROM (
  SELECT DISTINCT ON (conversations.id) conversations.id,
         conversation_preferences.unread_counter,
         left(messages.content, 50) AS last_message,
         posts.id AS post_id,
         messages.created_at AS last_activity_on,
         categories.root_name AS site_name,
         conversation_preferences.state,
         COALESCE(NULLIF(post_owner, 1234567), NULLIF(post_applicant, 1234567)) AS interlocutor_id
  FROM "conversations"
  LEFT OUTER JOIN "conversation_preferences" ON "conversation_preferences"."conversation_id" = "conversations"."id"
  LEFT OUTER JOIN "posts" ON "posts"."id" = "conversations"."post_id"
  LEFT OUTER JOIN "categories" ON "categories"."id" = "posts"."category_id"
  LEFT OUTER JOIN "messages" ON "messages"."conversation_id" = "conversations"."id"
  WHERE (post_applicant = 1234567 OR post_owner = 1234567)
    AND "conversation_preferences"."user_id" = 1234567
  ORDER BY "conversations"."id" ASC, messages.created_at DESC
) sub
LEFT OUTER JOIN users ON interlocutor_id = users.id
LEFT OUTER JOIN profiles ON interlocutor_id = profiles.user_id
WHERE ("profiles"."status" != 'pending')
  AND (last_activity_on >= '2021-01-19 04:40:22.881985')
  AND (state = 'active')
ORDER BY profiles.status, sub.unread_counter DESC, sub.last_activity_on DESC
LIMIT 25

我们使用以下 ActiveRecord 代码生成此查询:

def fetch
  distinct = Conversation.left_outer_joins(:preferences)
                         .left_outer_joins(post: :category)
                         .left_outer_joins(:messages)
                         .where('post_applicant = :id OR post_owner = :id', id: current_user.id)
                         .where(conversation_preferences: { user_id: current_user.id })
                         .select(
                           <<-SQL.squish
                             DISTINCT ON (conversations.id) conversations.id,
                             conversation_preferences.unread_counter,
                             left(messages.content, 50) AS last_message,
                             posts.id AS post_id,
                             messages.created_at AS last_activity_on,
                             categories.root_name AS site_name,
                             conversation_preferences.state,
                             COALESCE(NULLIF(post_owner, #{current_user.id}), NULLIF(post_applicant, #{current_user.id})) AS interlocutor_id
                           SQL
                         )
                         .order(:id, 'messages.created_at DESC')

  Conversation.includes(post: :category)
              .from(distinct, :sub)
              .select('sub.*, profiles.status AS interlocutor_status')
              .joins('LEFT OUTER JOIN users ON interlocutor_id = users.id')
              .joins('LEFT OUTER JOIN profiles ON interlocutor_id = profiles.user_id')
              .where.not('profiles.status' => :pending)
              .order('profiles.status, sub.unread_counter DESC, sub.last_activity_on DESC')
end

问题

我们想在 profiles.status 之前停止订购。为此,我们很自然地将其从最后一个 order 语句中删除:

order('sub.unread_counter DESC, sub.last_activity_on DESC')

这就是问题所在。这样做完全破坏了生成的查询,这会生成一个与此处无关的错误,因为我们不想要修改后的查询(请注意它与第一个查询有何不同):

SELECT sub.*,
       profiles.status AS interlocutor_status,
       "conversations"."id" AS t0_r0,
       "conversations"."post_id" AS t0_r1,
       "conversations"."post_owner" AS t0_r2,
       "conversations"."post_applicant" AS t0_r3,
       "conversations"."created_at" AS t0_r4,
       "conversations"."updated_at" AS t0_r5,
       "posts"."id" AS t1_r0,
       "posts"."title" AS t1_r1,
       "posts"."description" AS t1_r2,
       "categories"."id" AS t2_r0,
       "categories"."name" AS t2_r1
FROM (
  SELECT DISTINCT ON (conversations.id) conversations.id, 
         conversation_preferences.unread_counter,
         left(messages.content, 50) AS last_message,
         posts.id AS post_id,
         messages.created_at AS last_activity_on,
         categories.root_name AS site_name,
         conversation_preferences.state,
         COALESCE(NULLIF(post_owner, 1234567), NULLIF(post_applicant, 1234567)) AS interlocutor_id
  FROM "conversations"
  LEFT OUTER JOIN "conversation_preferences" ON "conversation_preferences"."conversation_id" = "conversations"."id"
  LEFT OUTER JOIN "posts" ON "posts"."id" = "conversations"."post_id"
  LEFT OUTER JOIN "categories" ON "categories"."id" = "posts"."category_id"
  LEFT OUTER JOIN "messages" ON "messages"."conversation_id" = "conversations"."id" 
  WHERE (post_applicant = 1234567 OR post_owner = 1234567)
    AND "conversation_preferences"."user_id" = 1234567
  ORDER BY "conversations"."id" ASC, messages.created_at DESC
) sub
LEFT OUTER JOIN "posts" ON "posts"."id" = "conversations"."post_id"
LEFT OUTER JOIN "categories" ON "categories"."id" = "posts"."category_id"
LEFT OUTER JOIN users ON interlocutor_id = users.id
LEFT OUTER JOIN profiles ON interlocutor_id = profiles.user_id
WHERE ("profiles"."status" != 'pending')
  AND (last_activity_on >= '2021-01-19 05:04:06.084499')
  AND (state = 'active')
ORDER BY sub.unread_counter DESC, sub.last_activity_on DESC
LIMIT 25

我知道没有一点上下文很难帮助我们,但如果有人知道为什么 ActiveRecord 在尝试从订单语句中删除 profiles.status 后更改查询,那就太棒了。提前致谢

注意:直接(从我们的 postgres 客户端)修改第一个原始 SQL 确实有效。问题不是第一个查询,而是 ActiveRecord 如何处理它

终于找到了一种使用 preload 而不是 includes 使其工作的方法。我们希望避免使用单独的查询来加载 postscategories,但由于性能不受其影响,我们不介意。

这是它的样子:

Conversation.preload(post: :category)
            .from(distinct, :sub)
            .select('sub.*, profiles.status AS interlocutor_status')
            .joins('LEFT OUTER JOIN users ON interlocutor_id = users.id')
            .joins('LEFT OUTER JOIN profiles ON interlocutor_id = profiles.user_id')
            .where.not('profiles.status' => :pending)
            .order('sub.unread_counter DESC, sub.last_activity_on DESC')

生成 3 个查询:

-- Query 1
SELECT sub.*, profiles.status AS interlocutor_status
FROM (
  SELECT DISTINCT
  ....

-- Query 2
SELECT "posts".* FROM "posts" WHERE "posts"."id" IN (............)

-- Query 3
SELECT "categories".* FROM "categories" WHERE "categories"."id" IN (..........)

感谢大家在评论中的帮助(max and Sebastian Palma)