I am working on an app that allows Members to take a survey (Member has a one to many relationship with Response). Response holds the member_id, question_id, and their answe
If you are on Rails 5 and above you should use left_joins
. Otherwise a manual "LEFT OUTER JOINS" will also work. This is more performant than using includes
mentioned in https://stackoverflow.com/a/18234998/3788753. includes
will attempt to load the related objects into memory, whereas left_joins
will build a "LEFT OUTER JOINS" query.
def surveys_completed
members.left_joins(:responses).where.not(responses: { id: nil })
end
Even if there are no related records (like the query above where you are finding by nil) includes
still uses more memory. In my testing I found includes uses ~33x more memory on Rails 5.2.1. On Rails 4.2.x it was ~44x more memory compared to doing the joins manually.
See this gist for the test: https://gist.github.com/johnathanludwig/96fc33fc135ee558e0f09fb23a8cf3f1
You can use SQL EXISTS
keyword in elegant Rails-ish manner using Where Exists gem:
members.where_exists(:responses).count
Of course you can use raw SQL as well:
members.where("EXISTS" \
"(SELECT 1 FROM responses WHERE responses.member_id = members.id)").
count
You can use includes
and then test if the related response(s) exists like this:
def surveys_completed
members.includes(:responses).where('responses.id IS NOT NULL')
end
Here is an alternative, with joins
:
def surveys_completed
members.joins(:responses)
end
The solution using Rails 4:
def surveys_completed
members.includes(:responses).where.not(responses: { id: nil })
end
Alternative solution using activerecord_where_assoc:
This gem does exactly what is asked here: use EXISTS
to to do a condition.
It works with Rails 4.1 to the most recent.
members.where_assoc_exists(:responses)
It can also do much more!
Similar questions:
You can also use a subquery:
members.where(id: Response.select(:member_id))
In comparison to something with includes
it will not load the associated models (which is a performance benefit if you do not need them).