Can you select unique by count in Rails Active Record? - sql

If you can't do it, how would SQL do it?
Basically I want to select all my question objects where there is at least two with the same attribute. That attribute is called, lets say, word_id.
So how would I only select all the objects that that share only once a common attribute with another object?
If I have three objects :
# Question(id: 1, word_id: 1)
# Question(id: 2, word_id: 2)
# Question(id: 3, word_id: 2)
# Question(id: 4, word_id: 1)
# Question(id: 5, word_id: 1)
# Question(id: 6, word_id: 1)
I would want to return just id's 2 and 3 since they both share a common attribute twice.
Is that possible? I crudely do this by making two calls to the DB where first I call all the objects in question, add them to an array, and subtract from that array objects that match my requirements. I was just curious if there was a more elegant way to do it all at once.

Just SQL:
SELECT * FROM questions WHERE world_id IN (
SELECT world_id FROM questions GROUP BY world_id HAVING count(*) = 2
)
Rails:
Question.where("world_id IN (?)", Question.find(:all, select: "world_id",
group: "world_id HAVING count(*) = 2"))
I guess that's still two queries though...

Related

How to get id's from a list that are NOT in a table?

I am receiving a list of ID's. Most of these already exist in a table. I need to find which ID's are NOT in the table. This question has nothing to do with joins.
My API will receive a list of IDs, such as: [1, 2, 3, 4, 5]
Let's say there are three records in the table: [2, 3, 4]
The result I'm looking for is the array: [1, 5]
Our SQL brains jump quickly to something like the following, but clearly that's not what we need:
select * from widgets where id not in [list]
We don't need the records not in the list, we need the part of the list not in the records!
My fallback is to retrieve all records in the list and subtract from the list, something like this:
existing_ids = Widget.where(id: id_list).pluck(:id)
new_ids = id_list - existing_ids
That will work...but feels heavy-handed. Particularly if id_list has 100,000 records, and the table has 99,999 of those records.
I've searched around, and the only similar result is ID from list that is not in a table ... which did not find a viable solution.
Is there any way to do this in a single SQL query? (Bonus points for an ActiveRecord solution!)
To compare the lists to each other, either the input list needs to go into the database or the list of existing ids needs to come out of the database. The latter you already tried and didn't like, so here's an alternative
SELECT "id" FROM unnest('{1,2,3,4,5}'::integer[]) AS "id" WHERE "id" NOT IN (SELECT "id" FROM "widgets");
Not sure about performance.
Depending how many records are in your database, the simplest thing might just be to select all of the IDs and then drop the duplicates in Ruby.
from_api = [1,2,3,4,5]
existing = Widgets.pluck(:id) # => [2,3,4]
from_api.difference(existing) # => [1,5]
Obviously, if you have a substantial dataset, this will be less than optimal.
This should work.
from_api = [1,2,3,4,5]
existing = Widgets.order(:id).ids # => [2,3,4]
new_ids = []
from_api.each{ |n| new_ids << n unless existing.include? n }
new_ids # => [1,5]
or
from_api = [1,2,3,4,5]
existing = Widgets.order(:id).ids # => [2,3,4]
from_api.map{ |n| n == existing.first ? (nil if existing = existing.drop(1)) : n }.compac # => [1,5]
Balancing the complexity (to the current and future developers) of unset approach, I decided for my project that the simpler approach was warranted. While I didn't profile performance, I believe any gains would be minimal, if any.
Here is the solution I ended up with:
class Widget < ApplicationRecord
def self.absent(names)
uniq_names = names.uniq
uniq_names - where(name: uniq_names).pluck(:name)
end
end
And tests:
describe '.absent' do
subject { described_class.absent(names) }
let!(:widget1) { create(:widget, name: 'old-1') }
let!(:widget2) { create(:widget, name: 'old-2') }
let(:names) { %w[new-2 old-2 new-1 old-1 new-1 old-1] }
it { is_expected.to eq %w[new-2 new-1] }
end

Rails 4 ActiveRecord: Order recrods by attribute and association if it exists

I have three models that I am having trouble ordering:
User(:id, :name, :email)
Capsule(:id, :name)
Outfit(:id, :name, :capsule_id, :likes_count)
Like(:id, :outfit_id, :user_id)
I want to get all the Outfits that belong to a Capsule and order them by the likes_count.
This is fairly trivial and I can get them like this:
Outfit.where(capsule_id: capsule.id).includes(:likes).order(likes_count: :desc)
However, I then want to also order the outfits so that if a given user has liked it, it appears higher in the list.
Example if I have the following outfit records:
Outfit(id: 1, capsule_id: 2, likes_count: 1)
Outfit(id: 2, capsule_id: 2, likes_count: 2)
Outfit(id: 3, capsule_id: 2, likes_count: 2)
And the given user has only liked outfit with id 3, the returned order should be IDs: 3, 2, 1
I'm sure this is fairly easy, but I can't seem to get it. Any help would be greatly appreciated :)
Postgres SQL with a subquery
SELECT outfits.*
FROM outfits
LEFT OUTER JOIN (SELECT likes.outfit_id, 1 AS weight
FROM likes
WHERE likes.user_id = #user_id) AS user_likes
ON user_likes.outfit_id = outfits.id
WHERE outfits.capsule_id = #capsule_id
ORDER BY user_likes.weight ASC, outfits.likes_count DESC;
Postgres gives NULL values bigger weight when ordering. I am not sure how this would look in Arel query. You can try converting it using this cheatsheets.

Selecting in rails with an array and a element-related-condition

I have an array of products ids, which I would like to use for filtering a list of buying sessions. So far so good:
Session.where('product_id': [23, 32, 17])
I also have an array of dates, which specify for each product (differently) from which date should I even consider their sessions: ['12/2/17', '1/1/17', '20/3/17']
So in other words: product id 23, should only be considered for sessions created after 12/2/17, product id 32, should only be considered for sessions created after 1/1/17, and so on...
What is the right rails command to also consider the product dates when selecting sessions about the wanted product ids?
Something like this, chaining your where clauses to your needs.
Session.where('product_id = ? AND date > ?', 23, Time.new(2017, 2, 12))
.or(Session.where('product_id = ? AND date > ?', 32, Time.new(2017, 1, 17)))

Query that finds objects with ALL association ids (Rails 4)

BACKGROUND: Posts have many Communities through CommunityPosts. I understand the following query returns posts associated with ANY ONE of these community_ids.
Post.joins(:communities).where(communities: { id: [1,2,3] })
OBJECTIVE: I'd like to query for posts associated with ALL THREE community_ids in the array. Posts having communities 1, 2, and 3 as associations
EDIT: Please assume that length of the array is unknown. Used this array for explanation purposes.
Try this,
ids=[...]
Post.joins(:communities).select(“count(communities.id) AS cnt”).where(id: ids).group(‘post.id’).having(cnt: ids.size)
ids = [1, 2, 3] # and etc
Post.joins(:communities).where("communities.id IN ?", ids)
Wish it helps .

AND multiple JOIN statements in Rails

I have a table for users and roles. I'm using a has many through relationship. I am trying to create a query that will find users that have all of the roles in an array.
ex.
role_ids = [2, 4, 6]
User.filter(role_ids) would return all users that have roles with ids 2, 4, 6.
This is what I have so far.
def self.filter(role_ids)
results = User.joins(:roles).where(roles: {id: role_ids} )
end
The problem with this statement is it returns all users who have at least one of the roles in role_ids.
How do I make this statement give me an intersection, not a union?
I think you are asking for only unique instances of users who meet the role filter criteria. If so, then this should work.
def self.filter(role_ids)
results = User.joins(:roles).where(roles: {id: role_ids} ).uniq
end