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
使其工作的方法。我们希望避免使用单独的查询来加载 posts
和 categories
,但由于性能不受其影响,我们不介意。
这是它的样子:
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)
上下文
我们有一个 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
使其工作的方法。我们希望避免使用单独的查询来加载 posts
和 categories
,但由于性能不受其影响,我们不介意。
这是它的样子:
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)