Inverse of IN in Rails - sql

I feel foolish, but I cannot find the answer to this.
If I have a User with many attributes, given a list of attributes, I can ask rails something like this:
User.where("attributes.id IN ?", list_of_attribute_ids)
With the appropriate joins or includes or whatever.
However, I have no idea how to find the inverse set of those users. That is, given 100 users, if the result return 75 entries, I don't know how to find the other 25!
I thought
User.where("attributes.id NOT IN ?", list_of_attribute_ids)
might work (similarly, User.where.not), but it doesn't! Instead, it looks for those users where any of their attributes are not one of the list, which is useful, but not what I want.
The only way I know how to do it, is with something like:
User.where.not(id: User.where("attributes.id IN ?", list_of_attribute_ids).pluck(:id))
Which is sort of like the SQL for select user where id not in (gather a list of ids).
But this is massively non-performant, and generally just can't cope with a database with more than a few (hundred) entries.
How do you do this?

I think you could use left outer joins, like #Vishal mentioned in the comments.
See the guides: http://guides.rubyonrails.org/active_record_querying.html#left-outer-joins
rails 4:
joins("LEFT OUTER JOIN <something>")
rails 5:
left_outer_joins(:something)

Related

Having vs. Where in SQL, using the ORM in Laravel

I think my question is more related to SQL than to Laravel or its ORM, but I'm having the problem while programming in Laravel, so that's why I tagged it in the question.
My problem is as follows, I have the following model (sorry for the Spanglish):
I have the users table, nothing special here,
Then the juegos (games) tables, in it there's a jornada column (its like the week, to know which games are played in a certain week)
And finally the pronosticos (who the user says will win, which is stored in the diferencia column)
So I want to make a form where the user can make his bet. Basically this form will take its data from the pronosticos table, like this:
$juegos = Juego::where('jornada', $jor)
-> orderBy('expira')
-> get();
This produces what I want, a collection of models that I can iterate to show all the games for a given jornada (week).
Now, if the user has already make its bet, I want to bring also the scores values the user is betting on, with a query, so I thought I could use something like:
$juegos = Juego::where('jornada', $jor)
-> leftJoin('pronosticos', 'juegos.id', '=', 'pronosticos.juego_id')
-> addSelect(['pronosticos.user_id', 'juegos.id', 'expira', 'visitante', 'local', 'diferencia'])
-> having('pronosticos.user_id', $uid)
-> orderBy('expira')
-> get();
Now, the problem is, it is bringing an empty set, and thats quite obvious, if the user has made his bet, it will work, but if he hasn't the having will filter out everything, giving the empty set.
So I think I'm not getting clearly how to make the having or where to work correctly. Maybe what I want is to do a leftJoin not with the pronosticos table, but from the pronosticos table already filtered with a where clause.
Maybe I'm doing everything wrong and should do the leftJoin to a subselect? If that's so, I have no idea how to do it.
Or maybe my expectations are outside what can be done to SQL and I may return two different sets, and process them in the app?
EDIT
This is the query I want to express in Laravel's ORM:
SELECT * from juegos
LEFT JOIN (SELECT * FROM pronosticos WHERE user_id=1) AS p
ON p.juego_id = juegos.id
WHERE jornada = 2 ORDER BY expira

How to simulate ActiveRecord Model.count.to_sql

I want to display the SQL used in a count. However, Model.count.to_sql will not work because count returns a FixNum that doesn't have a to_sql method. I think the simplest solution is to do this:
Model.where(nil).to_sql.sub(/SELECT.*FROM/, "SELECT COUNT(*) FROM")
This creates the same SQL as is used in Model.count, but is it going to cause a problem further down the line? For example, if I add a complicated where clause and some joins.
Is there a better way of doing this?
You can try
Model.select("count(*) as model_count").to_sql
You may want to dip into Arel:
Model.select(Arel.star.count).to_sql
ASIDE:
I find I often want to find sub counts, so I embed the count(*) into another query:
child_counts = ChildModel.select(Arel.star.count)
.where(Model.arel_attribute(:id).eq(
ChildModel.arel_attribute(:model_id)))
Model.select(Arel.star).select(child_counts.as("child_count"))
.order(:id).limit(10).to_sql
which then gives you all the child counts for each of the models:
SELECT *,
(
SELECT COUNT(*)
FROM "child_models"
WHERE "models"."id" = "child_models"."model_id"
) child_count
FROM "models"
ORDER BY "models"."id" ASC
LIMIT 10
Best of luck
UPDATE:
Not sure if you are trying to solve this in a generic way or not. Also not sure what kind of scopes you are using on your Model.
We do have a method that automatically calls a count for a query that is put into the ui layer. I found using count(:all) is more stable than the simple count, but sounds like that does not overlap your use case. Maybe you can improve your solution using the except clause that we use:
scope.except(:select, :includes, :references, :offset, :limit, :order)
.count(:all)
The where clause and the joins necessary for the where clause work just fine for us. We tend to want to keep the joins and where clause since that needs to be part of the count. While you definitely want to remove the includes (which should be removed by rails automatically in my opinion), but the references (much trickier especially in the case where it references a has_many and requires a distinct) that starts to throw a wrench in there. If you need to use references, you may be able to convert these over to a left_join.
You may want to double check the parameters that these "join" methods take. Some of them take table names and others take relation names. Later rails version have gotten better and take relation names - be sure you are looking at the docs for the right version of rails.
Also, in our case, we spend more time trying to get sub selects with more complicated relationships, we have to do some munging. Looks like we are not dealing with where clauses as much.
ref2

sql count filtering - rails way

Suppose I have Posts and posts' Comments. I want to filter all the Posts that have more than 10 comments. I began writing something like Posts.includes(:comments).group("post.id").count("comments.id"), to obtain a hash of posts and their counts, and I can extract the information from there, but I want some one-line straightforward way to do that
Sure I can use some pure sql syntax statements, but I want it in a pure rails way. Any idea ?
Assuming the models are named in the more typical singular form of Post and Comment and have the usual association relationship, then the following should work:
Post.joins(:comments).group('posts.id').having('count(comments.id) > 10')

Rails Activerecord query selective include

I am having trouble optimizing a large activerecord query. I need to include an associated model in my request but due to the size of the return set I only want to include a couple of the associated columns. For example I have:
Post.includes(:user).large_set
While I am looking for something like:
Post.includes(:user.name, :user.profile_pic).large_set
I need to actually use the name and profile pic attributes so Post.joins(:user) is not an option as far as I understand.
select is what you are looking for:
Post.select("posts.*, users.name, users.profile_pic").large_set
http://guides.rubyonrails.org/active_record_querying.html#selecting-specific-fields
You'll have to use join to accomplish what you want, as includes does not have this functionality. Or you could white your own includes method :-)

Rails 3 ActiveRelation adding "is null" on a join... how do I stop it from doing that?

I am trying my first join and the sql that it's generating is very odd.
I have a Recipient belongs to a User. I am trying to query all the recipients by a user that are also not read and not deleted:
scope :unread, where(:is_read => false).where(:is_deleted => false)
scope :unread_by_user_id, lambda { |id| unread.joins(:user).merge(User.by_id(id)) }
This is the sql it generates:
SELECT `recipients`.* FROM `recipients` INNER JOIN `users` ON `users`.`id` IS NULL WHERE `recipients`.`is_read` = 0 AND `recipients`.`is_deleted` = 0 AND `users`.`id` = 475
Is there any way I can get rid of the "IS NULL"? That's not supposed to be there :(
I have tried searching google, and it's actually really amazing that 95% of examples out there do not talk about joins. The few examples of joins that I do find use the & syntax that has become depreciated. The documentation for this is actually quite bad compared to other things. Very odd indeed.
Anyway, I can't get this to work. It's definitely not a good day when you've been developing software for 19 years and can't get sql to join on a single table :( I can write queries in sql with 15 joins no problem manually. I guess that's the price you pay sometimes when you go through and learn new frameworks. It's not this weird though in Hibernate :/
Assuming you have your user fetched already:
user.recipients.unread
Recipients association already limits recipients to that user. Having a separate scope for that doesn't make sense to me.
--edit
This works if your User model has has_many :recipients association defined.