我有一些工作,但不令人满意。
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 作为字符串。理想情况下我想要的是
- 加载正确的数据
- 使用正确的 SQL
- 在字符串中使用 Rails 语法而不是 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]]
也许rails中有一些我没有尝试过的东西,但我觉得我尝试了很多组合。
字符串优势
无论如何,我通过运行查询 1,000 次进行了基准测试,发现字符串查询比 ruby 查询快 25%。此外,它们不会添加任何不必要的连接或条件,这对数据库服务器来说工作量较少。我想我会坚持使用琴弦。