Find records where join doesn't exist
I have a scope to limit all questions
by whether or not a user has voted on them. In the model:
scope :answered_by, lambda {|u| joins(:votes).where("votes.user_id = ?", u.id) }
scope :unanswered_by, lambda {|u| joins(:votes).where("votes.user_id != ?", u.id) }
In the controller, I call them like this:
@answered = Question.answered_by(current_user)
@unanswered = Question.unanswered_by(current_user)
The unanswered_by scope is incorrect. I essentially want to find where there is no vote. Instead, it is trying to look for if there is a vote that doesn't equal the current user. Any ideas how to return all records where a join doesn't exist?
Use an EXISTS
expression:
WHERE NOT EXISTS (
SELECT FROM votes v -- SELECT list can be empty
WHERE v.some_id = base_table.some_id
AND v.user_id = ?
)
The difference
... between NOT EXISTS()
(Ⓔ) and NOT IN()
(Ⓘ) is twofold:
-
Performance
Ⓔ is generally faster. It stops processing the subquery as soon as the first match is found. The manual:
The subquery will generally only be executed long enough to determine whether at least one row is returned, not all the way to completion.
Ⓘ can also be optimized by the query planner, but to a lesser extent since
NULL
handling makes it more complex. -
Correctness
If one of the resulting values in the subquery expression is
NULL
, the result of Ⓘ isNULL
, while common logic would expectTRUE
- and Ⓔ will returnTRUE
. The manual:If all the per-row results are either unequal or null, with at least one null, then the result of
NOT IN
is null.
Essentially, (NOT) EXISTS
is the better choice in most cases.
Example
Your query can look like this:
SELECT *
FROM questions q
WHERE NOT EXISTS (
SELECT FROM votes v
WHERE v.question_id = q.id
AND v.user_id = ?
);
Do not join to votes
in the base query. That would void the effort.
Besides NOT EXISTS
and NOT IN
there are additional syntax options with LEFT JOIN / IS NULL
and EXCEPT
. See:
- Select rows which are not present in other table
try this and let me know if it works
EDIT-1
scope :unanswered_questions, lambda { joins('LEFT OUTER JOIN votes ON questions.id = votes.question_id').where('votes.question_id IS NULL') }
EDIT-2
scope :unanswered_by, lambda {|u| where("questions.id NOT IN (SELECT votes.question_id from votes where votes.user_id = ?)",u.id) }
And if you want to do EXISTS
query in elegant and Rails-ish manner, you can use Where Exists gem I've written:
Question.where_not_exists(:votes, user_id: current_user.id)
Of course, you can made scope of it as well:
scope :unanswered_by, ->(user){ where_not_exists(:votes, user_id: user.id) }