我有一个TextMessage
模型,它有很多历史
class TextMessage < ApplicationRecord
has_many :histories, class_name: :CustomerServiceHistory, as: :item
scope :latest_messages, -> {
includes(histories: :action, phone: :customer)
.where("customer_service_actions.name != 'close' OR customer_service_actions.name IS NULL")
.where("text_messages.created_at = (SELECT MAX(text_messages.created_at) FROM text_messages WHERE text_messages.phone_id = phones.id)")
}
end
CustomerServiceHistory
属于一个项目(可以是文本消息或电子邮件)。用户可以“读取”或“关闭”项目。为此,CustomerServiceHistory
属于用户和操作(读取或关闭)。
class CustomerServiceHistory < ApplicationRecord
belongs_to :action, class_name: :CustomerServiceAction,
foreign_key: :customer_service_action_id
belongs_to :item, polymorphic: true
belongs_to :user
end
我有一个索引页面,我想在其中加载所有已关闭的文本消息。这是latest_messages
的{{1}}进入的地方。
TextMessage
.where("customer_service_actions.name != 'close' OR customer_service_actions.name IS NULL")
将加载没有关联的“关闭”操作的短信。
where("customer_service_actions.name != 'close'...
将加载尚无任何customer_service_actions且被用户视为“未读”的文本消息。
问题是当用户先“阅读”然后“关闭”短信时,该短信上现在有两个历史记录。
where子句停止工作,因为它能够过滤出此文本消息与其“关闭”操作之间的关系,但不是其与“读”操作之间的关联。
此外,许多用户可以阅读短信。可能有100位用户阅读了该短信。我希望在此短信上只有一个“关闭”操作时不加载该短信,而不管有多少个“读”操作。
是否可以仅使用SQL?
这是我的SQL输出。
... OR customer_service_actions.name IS NULL
答案 0 :(得分:1)
也许使用EXCEPT吗?
(SELECT *
FROM "text_messages"
LEFT OUTER JOIN "customer_service_actions"
ON "customer_service_actions"."id" = "customer_service_histories"."customer_service_action_id")
EXCEPT
(SELECT *
FROM "text_messages"
LEFT OUTER JOIN "customer_service_actions"
ON "customer_service_actions"."id" = "customer_service_histories"."customer_service_action_id"
WHERE "customer_service_actions"."name" LIKE 'close')
编辑:显然,Rails ActiveRecord不支持EXCEPT查询。您可以在Rails tho中减去查询。
q1 = TextMessage.all
q2 = TextMessage.includes(:histories).where(customer_service_actions:{name: 'close'})
result = q1 - q2
可能有效
答案 1 :(得分:0)
我有一些工作要做,但是还不能令人满意。
class TextMessage
def self.search(query)
return latest_messages.active unless query.present?
# more code
end
scope :latest_messages, -> {
where("text_messages.created_at = (SELECT MAX(text_messages.created_at) FROM text_messages WHERE text_messages.phone_id = phones.id)")
}
scope :active, -> {
where(
<<~SQL.squish
text_messages.id NOT IN (
SELECT text_messages.id
FROM text_messages
INNER JOIN customer_service_histories
ON customer_service_histories.item_id = text_messages.id
AND customer_service_histories.item_type = 'TextMessage'
INNER JOIN customer_service_actions
ON customer_service_actions.id = customer_service_histories.customer_service_action_id
WHERE customer_service_actions.name = 'close'
)
SQL
)
}
这会产生SQL
SQL (1.9ms)
SELECT DISTINCT "text_messages"."id",
customer_service_histories.customer_service_action_id AS alias_0,
text_messages.created_at AS alias_1
FROM "text_messages"
INNER JOIN "phones"
ON "phones"."id" = "text_messages"."phone_id"
INNER JOIN "customers"
ON "customers"."id" = "phones"."customer_id"
AND "customers"."company_id" = $1
LEFT OUTER JOIN "customer_service_histories"
ON "customer_service_histories"."item_id" = "text_messages"."id"
AND "customer_service_histories"."item_type" = $2
LEFT OUTER JOIN "customer_service_actions"
ON "customer_service_actions"."id" = "customer_service_histories"."customer_service_action_id"
WHERE (
text_messages.created_at = (
SELECT MAX(text_messages.created_at)
FROM text_messages
WHERE text_messages.phone_id = phones.id
)
)
AND (
text_messages.id NOT IN (
SELECT text_messages.id
FROM text_messages
INNER JOIN customer_service_histories
ON customer_service_histories.item_id = text_messages.id
AND customer_service_histories.item_type = 'TextMessage'
INNER JOIN customer_service_actions
ON customer_service_actions.id = customer_service_histories.customer_service_action_id
WHERE customer_service_actions.name = 'close'
)
)
ORDER BY
customer_service_histories.customer_service_action_id DESC,
text_messages.created_at DESC
LIMIT $3 OFFSET $4
[["company_id", 1], ["item_type", "TextMessage"], ["LIMIT", 10], ["OFFSET", 0]]
这是正确的SQL,但它使用SQL作为字符串。理想情况下,我想要的是
类似这样的东西
class TextMessage
def self.search(query)
return latest_messages.active unless query.present?
# more code
end
scope :latest_messages, -> {
where("text_messages.created_at = (SELECT MAX(text_messages.created_at) FROM text_messages WHERE text_messages.phone_id = phones.id)")
}
scope :active, -> {
where.not(id: TextMessage.select(:id)
.joins(histories: :action)
.where(customer_service_actions: { name: 'close' })
)
}
# more code
end
使用此Rails代码可加载正确的数据,但由于某些原因会导致SQL过多
SQL (1.2ms)
SELECT DISTINCT "text_messages"."id", customer_service_histories.customer_service_action_id AS alias_0, text_messages.created_at AS alias_1
FROM "text_messages" INNER JOIN "phones" ON "phones"."id" = "text_messages"."phone_id"
INNER JOIN "customers" ON "customers"."id" = "phones"."customer_id" AND "customers"."company_id" = $1
LEFT OUTER JOIN "customer_service_histories" ON "customer_service_histories"."item_id" = "text_messages"."id" AND "customer_service_histories"."item_type" = $2
LEFT OUTER JOIN "customer_service_actions" ON "customer_service_actions"."id" = "customer_service_histories"."customer_service_action_id"
WHERE (
text_messages.created_at = ( -- first condition
SELECT MAX(text_messages.created_at)
FROM text_messages
WHERE text_messages.phone_id = phones.id
)
)
AND (
text_messages.id NOT IN (
SELECT "text_messages"."id"
FROM "text_messages"
INNER JOIN "customer_service_histories" ON "customer_service_histories"."item_id" = "text_messages"."id" AND "customer_service_histories"."item_type" = 'TextMessage'
INNER JOIN "customer_service_actions" ON "customer_service_actions"."id" = "customer_service_histories"."customer_service_action_id"
WHERE (
text_messages.created_at = ( -- repeated first condition
SELECT MAX(text_messages.created_at)
FROM text_messages
WHERE text_messages.phone_id = phones.id
)
)
AND "customer_service_actions"."name" = 'close' -- second condition
)
)
ORDER BY
customer_service_histories.customer_service_action_id DESC,
text_messages.created_at DESC
LIMIT $3 OFFSET $4
[["company_id", 1], ["item_type", "TextMessage"], ["LIMIT", 10], ["OFFSET", 0]]
重复created_at
条件,然后将其与actions.name
条件配对。我尝试了多种不同的组合方式以使其更简洁地使用ruby语法,但是我对SQL输出不满意。
我确实找到了一种使用ruby语法并获取所需SQL的方法,但是我必须将两个where()
函数都放在同一作用域中。
class TextMessage
def self.search(query)
return latest_messages unless query.present?
# more code
end
scope :latest_messages, -> {
where("text_messages.created_at = (SELECT MAX(text_messages.created_at) FROM text_messages WHERE text_messages.phone_id = phones.id)")
.where('text_messages.id NOT IN (?)', TextMessage.active_ids)
}
scope :active_ids, -> {
TextMessage.select(:id).joins(histories: :action).where.not(
customer_service_actions: { name: 'close' }
)
}
# more code
end
我试图将它们放在不同的范围内
def self.search(query)
return latest_messages.active unless query.present?
# more code
end
scope :latest_messages, -> {
where("text_messages.created_at = (SELECT MAX(text_messages.created_at) FROM text_messages WHERE text_messages.phone_id = phones.id)")
}
scope :active, -> {
where('text_messages.id NOT IN (?)', TextMessage.active_ids)
)
scope :active_ids, -> {
TextMessage.select(:id).joins(histories: :action).where.not(
customer_service_actions: { name: 'close' }
)
}
但这导致子查询中出现更多的连接子句
SQL (1.7ms)
SELECT DISTINCT "text_messages"."id",
customer_service_histories.customer_service_action_id AS alias_0,
text_messages.created_at AS alias_1
FROM "text_messages"
INNER JOIN "phones"
ON "phones"."id" = "text_messages"."phone_id"
INNER JOIN "customers"
ON "customers"."id" = "phones"."customer_id"
AND "customers"."company_id" = $1
LEFT OUTER JOIN "customer_service_histories"
ON "customer_service_histories"."item_id" = "text_messages"."id"
AND "customer_service_histories"."item_type" = $2
LEFT OUTER JOIN "customer_service_actions"
ON "customer_service_actions"."id" = "customer_service_histories"."customer_service_action_id"
WHERE (
text_messages.created_at = ( -- first condition
SELECT MAX(text_messages.created_at)
FROM text_messages
WHERE text_messages.phone_id = phones.id
)
)
AND (
"text_messages"."id" NOT IN (
SELECT "text_messages"."id"
FROM "text_messages"
INNER JOIN "phones" -- unnecessary joins on phones
ON "phones"."id" = "text_messages"."phone_id"
INNER JOIN "customers" -- unnecessary joins on customers
ON "customers"."id" = "phones"."customer_id"
AND "customers"."company_id" = $3
INNER JOIN "customer_service_histories"
ON "customer_service_histories"."item_id" = "text_messages"."id"
AND "customer_service_histories"."item_type" = $4
INNER JOIN "customer_service_actions"
ON "customer_service_actions"."id" = "customer_service_histories"."customer_service_action_id"
WHERE (
text_messages.created_at = ( -- repeated first condition
SELECT MAX(text_messages.created_at)
FROM text_messages
WHERE text_messages.phone_id = phones.id
)
)
AND "customer_service_actions"."name" = $5 -- second condition
)
)
ORDER BY
customer_service_histories.customer_service_action_id DESC,
text_messages.created_at
DESC LIMIT $6 OFFSET $7
[["company_id", 1], ["item_type", "TextMessage"], ["company_id", 1], ["item_type", "TextMessage"], ["name", "close"], ["LIMIT", 10], ["OFFSET", 0]]
也许有一些我没有尝试过的东西,但是我觉得我尝试了很多组合。
字符串优势
无论如何,我通过运行1000次查询来进行基准测试,发现字符串查询比ruby等效查询快25%。此外,它们不会添加任何不必要的联接或条件,这对于数据库服务器来说工作量较小。我想我会坚持下去。