To short version of this question is that I want to accomplish something along the lines of what's visible on Flickr's homepage once you're logged in. It shows the three latest photos of each of your friends sorted by date but grouped by friend.
Here's a longer explanation: For example I have 3 friends: John, George and Andrea. The list I want to extract should look like this:
George
Photo - 2010-05-18
Photo - 2010-05-18
Photo - 2010-05-12
John
Photo - 2010-05-17
Photo - 2010-05-14
Photo - 2010-05-12
Andrea
Photo - 2010-05-15
Photo - 2010-05-15
Photo - 2010-05-15
Friend with most recent photo uploaded is on top but his or her 2 next files follow.
I'd like to do this from MySQL, and for the time being I got here:
SELECT photos.user_id, photos.id, photos.date_uploaded
FROM photos
WHERE photos.user_id IN (SELECT user2_id
FROM user_relations
WHERE user1_id = 8)
ORDER BY date_uploaded DESC
Where user1_id = 8 is the currently logged in user and user2_id are the ids of friends. This query indeed returns the latest files uploaded by the contacts of the user with id = 8 sorted by date. However I'd like to accomplish the grouping and limiting mentioned above.
Hopefully this makes sense. Thank you in advance.
Sometimes, the only way to acheive an end is to create a piece of SQL so ugly and heinous, that the alternative of doing multiple queries becomes attractive :-)
I would just do one query to get a list of your friends then, for each friend, get the three most recent photos. Something like:
friend_list = sqlexec "select user2_id from relations where user1_id = "
+ current_user_id
photolist = []
for friend in friend_list:
photolist += sqlexec "select user_id, id, date_uploaded from photos"
+ " where user_id = "
+ friend.get("user2_id")
+ " order by date_uploaded desc fetch first 3 rows only"
# Now do something with photolist
You don't have to do it as one query any more than you're limited to one regular expression for matching a heinous pattern. Sure it'd be nice to be "clever" but it's rarely necessary. I prefer a pragmatic approach.
Related
In my rails app, I have Users and Listings. The Listings belong to a User. Listing has user_id and its filled with users id who is creating the listing.
A user can be a premium user, gold user or silver user.
What I want is for each premium user, select one random listing to show in premium listings.
I can do it in O(n**2) time or n+1 query as follow:
users_id = User.where(:role => "premium").pluck[:id]
final_array = Array.new
users_id.each do |id|
final_array << Listing.where(:user_id => id).sample(1)
end
final_array
Is there a better way of doing this?
You could try this:
listings = Listing.select(
<<~SQL
DISTINCT ON (users.id) users.id,
listings.*,
row_number() OVER (PARTITION BY users.id ORDER BY random())
SQL
)
.joins(:user)
.includes(:user)
.where(users: { role: :premium })
It gives a random Listing for every premium user.
It produces the only request to db and also it won't make an extra request for getting listing's user, so you are free to do something like this:
listings.each do |listing|
p listing.user
end
random_user_listings = []
User.includes(:listings).where(role: "premium").find_each do |user|
random_user_listings << user.listings.sample(1)
end
random_user_listings
To avoid N+1 query you need to combine them, perform query one time like this:
list = Listing.includes(:user).where(:role => "premium").sample(1)
Feel free to deal with list instead of Listing. Because now you're dealing with variable, not Query.
ids = list.pluck(:user_id).uniq
Getting array of ids like above and doing further steps as you did (but with list, not Listing)
Need to be noticed that, when you deal with Model you're dealing with QUERY. Avoiding doing that in loop statement.
I have a survey where users can post answers and since the answers are being saved in the db as a foreign key for each question, I'd like to know which answer got the highest rating.
So if the DB looks somewhat like this:
answer_id
1
1
2
how can I find that the answer with an id of 1 was selected more times than the one with an id of 2 ?
EDIT
So far I've done this:
#question = AnswerContainer.where(user_id: params[:user_id]) which lists the things a given user has voted for, but, obviously, that's not what I need.
you could try:
YourModel.group(:answer_id).count
for your example return something like: {1 => 2, 2 => 1}
You can do group by and then sort
Select answer_id, count(*) as maxsel
From poll
Group by answer_id
Order by maxsel desc
As stated in rails documentation (http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html) when you use group with count, active record "returns a Hash whose keys represent the aggregated column, and the values are the respective amounts"
Person.group(:city).count
# => { 'Rome' => 5, 'Paris' => 3 }
Suppose I have many tagged entities (e.g. blog posts with tags) to store in a SQL database. For example:
post1: work
post2: work, programming, java, work
post3: work, programming, sql
post4: vacation, photo
post5: vacation
post6: photo
Suppose also I have a list of tags
work, vacation
Now I'd like to get a posts sample of size 2, i.e. two posts with tags from the list. For example
sample1: post1 and post2
sample2: post1 and post4
sample3: post2 and post5
In addition I'd like the sample to contain all tags in the list. Note that sample1 does not meet this requirement since the set of tags of the sample entities does not contain tag vacation from the list.
I would like also all tags occurrences to be equal. Let's consider 2 samples of size 4.
sample1: post1, post2, post3, post6
sample2: post1, post3, post4, post5
Note that sample1 does not meet this requirement since tag work occurs 3 times in it and vacation occurs only once.
My question is: how to design a relational database and SQL query to retrieve samples of given size?
If you want to get all posts that have tags in a comma delimited list:
select postid
from post_tags
where find_in_set(tagid, #LIST) > 0
group by postid
having count(distinct tagid) = 1+length(#LIST) - length(replace(',', #LIST, ''));
If you want just a "sample" of them:
select postid
from (select postid
from post_tags
where find_in_set(tagid, #LIST) > 0
group by postid
having count(distinct tagid) = 1+length(#LIST) - length(replace(',', #LIST, ''))
) t
order by rand()
limit 5
I have 2 models - Restaurant and Feature. They are connected via has_and_belongs_to_many relationship. The gist of it is that you have restaurants with many features like delivery, pizza, sandwiches, salad bar, vegetarian option,… So now when the user wants to filter the restaurants and lets say he checks pizza and delivery, I want to display all the restaurants that have both features; pizza, delivery and maybe some more, but it HAS TO HAVE pizza AND delivery.
If I do a simple .where('features IN (?)', params[:features]) I (of course) get the restaurants that have either - so or pizza or delivery or both - which is not at all what I want.
My SQL/Rails knowledge is kinda limited since I'm new to this but I asked a friend and now I have this huuuge SQL that gets the job done:
Restaurant.find_by_sql(['SELECT restaurant_id FROM (
SELECT features_restaurants.*, ROW_NUMBER() OVER(PARTITION BY restaurants.id ORDER BY features.id) AS rn FROM restaurants
JOIN features_restaurants ON restaurants.id = features_restaurants.restaurant_id
JOIN features ON features_restaurants.feature_id = features.id
WHERE features.id in (?)
) t
WHERE rn = ?', params[:features], params[:features].count])
So my question is: is there a better - more Rails even - way of doing this? How would you do it?
Oh BTW I'm using Rails 4 on Heroku so it's a Postgres DB.
This is an example of a set-iwthin-sets query. I advocate solving these with group by and having, because this provides a general framework.
Here is how this works in your case:
select fr.restaurant_id
from features_restaurants fr join
features f
on fr.feature_id = f.feature_id
group by fr.restaurant_id
having sum(case when f.feature_name = 'pizza' then 1 else 0 end) > 0 and
sum(case when f.feature_name = 'delivery' then 1 else 0 end) > 0
Each condition in the having clause is counting for the presence of one of the features -- "pizza" and "delivery". If both features are present, then you get the restaurant_id.
How much data is in your features table? Is it just a table of ids and names?
If so, and you're willing to do a little denormalization, you can do this much more easily by encoding the features as a text array on restaurant.
With this scheme your queries boil down to
select * from restaurants where restaurants.features #> ARRAY['pizza', 'delivery']
If you want to maintain your features table because it contains useful data, you can store the array of feature ids on the restaurant and do a query like this:
select * from restaurants where restaurants.feature_ids #> ARRAY[5, 17]
If you don't know the ids up front, and want it all in one query, you should be able to do something along these lines:
select * from restaurants where restaurants.feature_ids #> (
select id from features where name in ('pizza', 'delivery')
) as matched_features
That last query might need some more consideration...
Anyways, I've actually got a pretty detailed article written up about Tagging in Postgres and ActiveRecord if you want some more details.
This is not "copy and paste" solution but if you consider following steps you will have fast working query.
index feature_name column (I'm assuming that column feature_id is indexed on both tables)
place each feature_name param in exists():
select fr.restaurant_id
from
features_restaurants fr
where
exists(select true from features f where fr.feature_id = f.feature_id and f.feature_name = 'pizza')
and
exists(select true from features f where fr.feature_id = f.feature_id and f.feature_name = 'delivery')
group by
fr.restaurant_id
Maybe you're looking at it backwards?
Maybe try merging the restaurants returned by each feature.
Simplified:
pizza_restaurants = Feature.find_by_name('pizza').restaurants
delivery_restaurants = Feature.find_by_name('delivery').restaurants
pizza_delivery_restaurants = pizza_restaurants & delivery_restaurants
Obviously, this is a single instance solution. But it illustrates the idea.
UPDATE
Here's a dynamic method to pull in all filters without writing SQL (i.e. the "Railsy" way)
def get_restaurants_by_feature_names(features)
# accepts an array of feature names
restaurants = Restaurant.all
features.each do |f|
feature_restaurants = Feature.find_by_name(f).restaurants
restaurants = feature_restaurants & restaurants
end
return restaurants
end
Since its an AND condition (the OR conditions get dicey with AREL). I reread your stated problem and ignoring the SQL. I think this is what you want.
# in Restaurant
has_many :features
# in Feature
has_many :restaurants
# this is a contrived example. you may be doing something like
# where(name: 'pizza'). I'm just making this condition up. You
# could also make this more DRY by just passing in the name if
# that's what you're doing.
def self.pizza
where(pizza: true)
end
def self.delivery
where(delivery: true)
end
# query
Restaurant.features.pizza.delivery
Basically you call the association with ".features" and then you use the self methods defined on features. Hopefully I didn't misunderstand the original problem.
Cheers!
Restaurant
.joins(:features)
.where(features: {name: ['pizza','delivery']})
.group(:id)
.having('count(features.name) = ?', 2)
This seems to work for me. I tried it with SQLite though.
I have some problems with this fql.query method:
select music
from user
where uid = ......
and music in (select music
from user
where uid = ......
)
I want to obtain common music interest between two users; it works with queries like this
select uid
from user
where uid = ......
and uid in (select uid
from user
where uid = ......
)
I think the problem is that the second query returns an integer and the first one returns a string array. Can anyone help me with this?
(Excuse my bad English! I'm from Spain ;) )
Does not the first query already gives you your answer?
I mean it does the following :
-- Display only music field
select music
from user
where uid = <first_USER> -- Selection of your first user
and music in ( -- We want to macth the string with another
select music -- Selects only the music field
from user
where uid = <second_USER> -- Selection of your second user
)
So you gets a music name that match in two user records.
Your second query seems a bit odd to me :
select uid
from user
where uid = <first_USER?>
and uid in (select uid
from user
where uid = <second_USER?>
)
Which basically translates to find the user_id matching user one and user two. Thing that does never happen since each user has it's own uid.
By the way, I don't know how facebook handles music string, but it may appear that user don't write their favorite music name in a common spelling and/or format. That may impede your matching system.
I tried this:
select music
from user
where uid=UID1
AND music IN (SELECT music from user where uid=UID2)
And it works fine when the UID1 music string matches exactly the UID2's one.