问题
I have two models, Conversation and Phones, both of which has_and_belongs_to_many each other. Phones can have a lot of conversations, and conversations can have a lot of phones (two or more).
class Conversation < ActiveRecord::Base
has_and_belongs_to_many :phones
end
class Phone < ActiveRecord::Base
has_and_belongs_to_many :conversations
end
Of course, there's a conversations_phones join table as well.
If I have two or more phone objects, how do I find a list of all the conversations they share? The catch: the conversations can't include any other phones (IE the number of phone IDs equals the number we search with).
I've been able to do it with pure Rails, but it involves looping every conversation and counting on the db. Not good.
I don't mind doing pure SQL; using the model IDs should help stop injection attacks.
The closest I've come is:
SELECT conversations.* FROM conversations
INNER JOIN conversations_phones AS t0_r0 ON conversations.id = t0_r0.conversation_id
INNER JOIN conversations_phones AS t0_r1 ON conversations.id = t0_r1.conversation_id
WHERE (t0_r0.phone_id = ? AND t0_r1.phone_id = ?), @phone_from.id, @phone_to.id
But it includes conversations with outside phones. I have a feeling GROUP BY and HAVING COUNT would help, I'm just too new to SQL.
回答1:
I think you were almost there. Just exclude conversations with outsiders with an additional NOT EXISTS
anti-semi-join:
SELECT c.*
FROM conversations c
JOIN conversations_phones AS cp1 ON cp1.conversation_id = c.id
AND cp1.phone_id = ?
JOIN conversations_phones AS cp2 ON cp2.conversation_id = c.id
AND cp2.phone_id = ?
...
WHERE NOT EXISTS (
SELECT 1
FROM conversations_phones cp
WHERE cp.conversation_id = c.id
AND cp.phone_id NOT IN (cp1.phone_id, cp2.phone_id, ...) -- or repeat param
)
, @phone1.id, @phone2.id, ...
I pulled conditions into the JOIN clause for simplicity, doesn't change the query plan.
Goes without saying that you need indices on conversations(id)
and conversations_phones(conversation_id, phone_id)
.
Alternatives (much slower):
Very simple, but slow:
SELECT cp.conversation_id
FROM (
SELECT conversation_id, phone_id
FROM conversations_phones
ORDER BY 1,2
) cp
GROUP BY 1
HAVING array_agg(phone_id) = ?
.. where ?
is a sorted array of ids like '{559,12801}'::int[]
30x slower in a quick test.
For completeness, the (simplified) proposed alternative by @BroiSatse in the comments performs around 20x slower in a similar quick test:
...
JOIN (
SELECT conversation_id, COUNT(*) AS phone_count
FROM conversations_phones
GROUP BY prod_id
) AS pc ON pc.conversation_id = c.id AND phone_count = 2
Or, slightly simpler and faster:
...
JOIN (
SELECT conversation_id
FROM conversations_phones
GROUP BY prod_id
HAVING COUNT(*) = 2
) AS pc ON pc.conversation_id = c.id
来源:https://stackoverflow.com/questions/18163233/rails-has-and-belongs-to-many-find-unique-objects-in-common