Select model based on amount of related models with certain conditions - sql

I have a Post that has_many :comments. Let's say Comment has the following field: another_model_id. I would like to select Posts, that have from 2 to 5 comments with another_model_id = 10 (for example). I tried several constructions but with no success :(
Example of what I tried:
# Invalid SQL syntax error
Post
.joins(:comments)
.select("count(comment.another_model_id = 10) as comments_count)
.where("comments_count BETWEEN 2 AND 5")
I have literally no idea where to dig. Is it possible to achieve that in a single query? Thanks!

Post
.joins(:comments)
.where(comments: { another_model_id: 10 })
.group('posts.id')
.having('count(comments.id) > 2 AND count(comments.id) < 5')

Using counter_cache is the best practice for your scenario:
In your Comment model, you need to set the counter_cache:
belongs_to :post, counter_cache: true
Then simply you can use this query:
Post.joins(:comments)
.where(comments: { another_model_id: 10 })
.where('comments_count > ? AND comments_count < ?', 2, 5)

Related

Rails SQL Where Count

I have a Posts Table and A Sec_photo table :
class Post < ActiveRecord::Base
has_many :sec_photos
I am trying to do an advanced search form where it finds posts based on their sum of sec_photos :
#posts = #posts.where(:sec_photos.count > 2) is failing and I have looked online and attempted many other solutions but none seem to work.
Does anyone have any ideas?
Ps: It's a necessity for the line to be in the form #posts = #posts.where as that coincides with other conditions.
The advanced search from searches for other fields like category_id and location_id as such
#posts = Post.all
#posts = #posts.where('category_id = ?', #advanced_search.category_search) if #advanced_search.category_search.present?
#posts = #posts.where('location_id = ?', #advanced_search.location_search) if #advanced_search.location_search.present?
#posts = #posts.where("strftime('%m %Y', created_at) = ?", #advanced_search.newdate_search.strftime('%m %Y')) if #advanced_search.newdate_search.present?
The last option would be to show posts with sec_photos either more than 2 or less than 2
You can do as following:
#posts = Post.whatever_scopes_you_use
#posts = #posts.joins(:sec_photos).group('posts.id').having('COUNT(sec_photos.id)
> 2')
This last line will select all posts having strictly more than 2 sec_photos associated. Of course, you can easily make a scope from this, accepting a count variable to make it dynamic ;-)
From my previous answer: How to return results filtered on relations count to a view in RAILS?

Create a scope in rails based on HABTM association count

I'm trying to create a rails scope based on the count of a model's HABTM assocation, but I'm struggling with the SQL.
I want Match.open to return matches with less than two users. I also have Match.upcoming, which returns matches with a 'future_date' in the future, which is working well.
My code:
class Match < ActiveRecord::Base
has_and_belongs_to_many :users
scope :open, joins('matches_users').
select('*').
group('matches.id').
having('count(matches_users.user_id) < 2')
scope :upcoming, lambda {
where("proposed_date between ? and ?", Date.today, Date.today.next_month.beginning_of_month)
}
I'm currently getting the error:
SQLite3::SQLException: no such column: matches_users.user_id: SELECT * FROM "matches" matches_users GROUP BY matches.id HAVING count(matches_users.user_id) < 2
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: matches_users.user_id: SELECT * FROM "matches" matches_users GROUP BY matches.id HAVING count(matches_users.user_id) < 2
I'm currently achieving this with a class method:
def self.open
self.select{|match| match.users.length < 2}
end
Which works, but I'd really like to move this into a scope for speed, and so that I can chain the scopes like Match.open.upcoming.
What am I doing wrong here? What's the correct way to do this? Any help would be appreciated.
Give this a shot - I've used something similar before and it seems to work for me:
class Match < ActiveRecord::Base
has_and_belongs_to_many :users
scope :open, joins(:matches_users)
.select('matches.*')
.group('matches.id')
.having('count(matches_users.id) < 2')
...
end

Find all records which have a count of an association greater than zero

I'm trying to do something that I thought it would be simple but it seems not to be.
I have a project model that has many vacancies.
class Project < ActiveRecord::Base
has_many :vacancies, :dependent => :destroy
end
I want to get all the projects that have at least 1 vacancy.
I tried something like this:
Project.joins(:vacancies).where('count(vacancies) > 0')
but it says
SQLite3::SQLException: no such column: vacancies: SELECT "projects".* FROM "projects" INNER JOIN "vacancies" ON "vacancies"."project_id" = "projects"."id" WHERE ("projects"."deleted_at" IS NULL) AND (count(vacancies) > 0).
1) To get Projects with at least 1 vacancy:
Project.joins(:vacancies).group('projects.id')
2) To get Projects with more than 1 vacancy:
Project.joins(:vacancies).group('projects.id').having('count(project_id) > 1')
3) Or, if Vacancy model sets counter cache:
belongs_to :project, counter_cache: true
then this will work, too:
Project.where('vacancies_count > ?', 1)
Inflection rule for vacancy may need to be specified manually?
joins uses an inner join by default so using Project.joins(:vacancies) will in effect only return projects that have an associated vacancy.
UPDATE:
As pointed out by #mackskatz in the comment, without a group clause, the code above will return duplicate projects for projects with more than one vacancies. To remove the duplicates, use
Project.joins(:vacancies).group('projects.id')
UPDATE:
As pointed out by #Tolsee, you can also use distinct.
Project.joins(:vacancies).distinct
As an example
[10] pry(main)> Comment.distinct.pluck :article_id
=> [43, 34, 45, 55, 17, 19, 1, 3, 4, 18, 44, 5, 13, 22, 16, 6, 53]
[11] pry(main)> _.size
=> 17
[12] pry(main)> Article.joins(:comments).size
=> 45
[13] pry(main)> Article.joins(:comments).distinct.size
=> 17
[14] pry(main)> Article.joins(:comments).distinct.to_sql
=> "SELECT DISTINCT \"articles\".* FROM \"articles\" INNER JOIN \"comments\" ON \"comments\".\"article_id\" = \"articles\".\"id\""
Yeah, vacancies is not a field in the join. I believe you want:
Project.joins(:vacancies).group("projects.id").having("count(vacancies.id)>0")
# None:
Project.left_joins(:vacancies).group('projects.id').having('count(vacancies) = 0')
# Any
Project.joins(:vacancies).group('projects.id').having('count(vacancies) > 0')
# One
Project.joins(:vacancies).group('projects.id').having('count(vacancies) = 1')
# More than 1
Project.joins(:vacancies).group('projects.id').having('count(vacancies) > 1')
Performing an inner join to the has_many table combined with a group or uniq is potentially very inefficient, and in SQL this would be better implemented as a semi-join that uses EXISTS with a correlated subquery.
This allows the query optimiser to probe the vacancies table to check for the existence of a row with the correct project_id. It doesn't matter whether there is one row or a million that have that project_id.
That's not as straightforward in Rails, but can be achieved with:
Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)
Similarly, find all projects that have no vacancies:
Project.where.not(Vacancies.where("vacancies.project_id = projects.id").exists)
Edit: in recent Rails versions you get a deprecation warning telling you to not to rely on exists being delegated to arel. Fix this with:
Project.where.not(Vacancies.where("vacancies.project_id = projects.id").arel.exists)
Edit: if you're uncomfortable with raw SQL, try:
Project.where.not(Vacancies.where(Vacancy.arel_table[:project_id].eq(Project.arel_table[:id])).arel.exists)
You can make this less messy by adding class methods to hide the use of arel_table, for example:
class Project
def self.id_column
arel_table[:id]
end
end
... so ...
Project.where.not(
Vacancies.where(
Vacancy.project_id_column.eq(Project.id_column)
).arel.exists
)
In Rails 4+, you can also use includes or eager_load to get the same answer:
Project.includes(:vacancies).references(:vacancies).
where.not(vacancies: {id: nil})
Project.eager_load(:vacancies).where.not(vacancies: {id: nil})
I think there's a simpler solution:
Project.joins(:vacancies).distinct
Without much Rails magic, you can do:
Project.where('(SELECT COUNT(*) FROM vacancies WHERE vacancies.project_id = projects.id) > 0')
This type of conditions will work in all Rails versions as much of the work is done directly on the DB side. Plus, chaining .count method will work nicely too. I've been burned by queries like Project.joins(:vacancies) before. Of course, there are pros and cons as it's not DB agnostic.
You can also use EXISTS with SELECT 1 rather than selecting all the columns from the vacancies table:
Project.where("EXISTS(SELECT 1 from vacancies where projects.id = vacancies.project_id)")
If I want to know how many records have at least one of an associated record, I would do:
Project.joins(:vacancies).uniq.count
The error is telling you that vacancies is not a column in projects, basically.
This should work
Project.joins(:vacancies).where('COUNT(vacancies.project_id) > 0')

Filtering Parents by Children

I'm doing a simple blog application - There are posts, which have many tags through a posts_tags table (my models are below). What I have implemented is if a user clicks a tag, it will show just the posts with that tag. What I want is for the user to them be able to select another tag, and it will filter to only the posts that have both of those tags, then a third, then a fourth, etc. I'm having difficulty making the active record query - especially dynamically. The closest I've gotten is listed below - however its in pure SQL and I would like to at least have it in ActiveRecord Rubyland syntax even with the complexity it contains.
Also, the "having count 2" does not work, its saying that "count" does not exist and even if I assign it a name. However, it is outputting in my table (the idea behind count is that if it contains a number that is as much as how many tags we are searching for, then theoretically/ideally it has all the tags)
My current test SQL query
select posts_tags.post_id,count(*) from posts_tags where tag_id=1 or tag_id=3 group by post_id ### having count=2
The output from the test SQL (I know it doesnt contain much but just with some simple seed data).
post_id | count
---------+-------
1 | 2
2 | 1
My Models:
/post.rb
class Post < ActiveRecord::Base
has_many :posts_tags
has_many :tags, :through => :posts_tags
end
/tag.rb
class Tag < ActiveRecord::Base
has_many :posts_tags
has_many :posts, :through => :posts_tags
end
/poststag.rb
class PostsTag < ActiveRecord::Base
belongs_to :tag
belongs_to :post
end
Give a try to:
Post.joins(:tags).where(tags: {id: [1, 3]}).select("posts.id, count(*)").group("posts.id").having("count(*) > 2")
I think "count = 2" is not correct. It should be "count(*) = 2". Your query then will be
select post_id,count(post_id)
from posts_tags
where tag_id = 1 or tag_id = 3
group by post_id
having count(post_id) = 2
In general you want to stay away from writing raw sql when using rails. Active Record has great helper methods to make your sql more readable and maintainable.
If you only have a few tags you can create scopes for each of them (http://guides.rubyonrails.org/active_record_querying.html#scopes)
Since people are clicking on tags one at a time you could just query for each tag and then use the & operator on the arrays. Because you have already requested the exact same set of data from the database the query results should be cached meaning you are only hitting the db for the newest query.

ActiveRecord condition with count less than for association

I have a User that has_many messages.
I need a create a query that will
'Get me all users who's (message.opened == false) count < 3'
Right now, I am using User.all, iterating through all users, and counting manually. I understand that this isn't very efficient and it can be all done in one query, but I am new to SQL/ActiveRecord so need some help here.
Thanks
Assuming Rails 3 syntax. You can do something like:
User.joins(:messages).where(:messages => {:opened => false}).group(:user_id).having("COUNT(messages.id) < 3)
This should work:
User.includes(:messages).group("users.id").where("messages.opened = 0").having("count(messages.id) < 3")
This will create two queries, one for the grouped query, and one for eager loading the resulting users and messages with a join.
Here is solution to your problem
User.includes(:messages).group("users.id").where("messages.opened = 0").having("count(messages.id) < 3")
but what else you can do is to create a scope for this
scope :not_opened_less_three_count, includes(:messages).group("users.id").where("messages.opened = 0").having("count(messages.id) < 3")
And then you can use it anywhere you needed as follow
User.not_opened_less_three_count
Try this
User.includes(:messages).group('users.id').having('SUM(IFNULL(messages.opened = 0, 1)) < 3')
It works at least on MySQL, AND assuming your boolean true are 1 in database.
EDIT I had reversed the condition
PS IFNULL is there to handle if messages.opened can be NULL