Rails order by range first - ruby-on-rails

I have model named Group which has many users,
Group contains the fields for min_age and max_age describing the user's with minimum age and maximum age.
Each user has its settings where sets preferred age group like 18 to 25
When a user searches for groups than I have order groups with age b/w 18 to 25 first and than rest
I am doing it with 2 queries like
groups = Group.where("min_age >=? AND max_age <=?", setting.min_age, setting.max_age)
+ Group.where("min_age <? OR max_age >?", setting.min_age, setting.max_age)
It worked but thing is I have too many other filters and I want cut short number of queries.
Is it possible to do this in single query?

You can do that by ordering matching records before records that do not match:
Group.order("CASE WHEN min_age >= #{setting.min_age} AND max_age <= #{settings.max_age} then 1 else 2 end")
See: SQL CASE statement.

Related

Find all records having less value then sum of two columns with associated activerecord

I have a Room model and it is associated with ReservationRoom. (room => has_many :reservations_rooms)
Now I want to find Rooms with below condition
ReservationRooms has two column named number_of_adults and number_of_child
andĀ 
Room has column named min_occupancy
I need all the room recordĀ 
if sum of the number_of_adults and number_of_child of reservation_rooms, less then min_occupancy of room table.
something link below.
Room.joins(:reservations_rooms).where(id: [22, 19]).having('sum(reservations_rooms.number_of_adult + reservations_rooms.number_of_child) < rooms.min_occupancy')
can someone help me out with this?
Try this, I think you don't need grouping
Room.joins(:reservation_rooms).
where(rooms: { id: [22, 19] }).
where('reservation_rooms.number_of_adults + reservation_rooms.number_of_child <= rooms.min_occupancy')
I left the condition about ids because I don't know if you need that too
What you are looking for is a where clause. You can combine multiple where in ActiveRecord and the will be combined with AND:
Room # the model you are interested in
.joins(:reservation_rooms) # combine the reservations
.where(rooms: {id: [22, 19]}) # only for those two rooms
.where(
"number_of_adults + number_of_child <= min_occupancy"
) # add adults/child and compare to occupancy
This should generate SQL similar to this.
select * from rooms
joins reservation_rooms on roomws.id = reservation_rooms.room_id
where
rooms.id in (22, 19)
and number_of_adults + number_of_child <= max_occupancy
Some notes:
You can see what SQL gets generated by appending .to_sql to your ActiveRecord::Association (the query you've built)
Assuming the column names are unique you don't need to prefix them with the table name (e.g. if bot tables have a created_at column you would need to specify what column you are interested in like rooms.id).
I'd rename number_of_child to number_of_children to be consistent (it's plural on number_of_adults)
sum is an aggregate function (like avg, count and others). Those functions are used to group multiple columns into one. (in your example you want to combine multiple columns of a row, hence you can use +)
having is also used with group s. It is similar to a where clause but for filtering grouped rows

Duplicates in the result of a subquery

I am trying to count distinct sessionIds from a measurement. sessionId being a tag, I count the distinct entries in a "parent" query, since distinct() doesn't works on tags.
In the subquery, I use a group by sessionId limit 1 to still benefit from the index (if there is a more efficient technique, I have ears wide open but I'd still like to understand what's going on).
I have those two variants:
> select count(distinct(sessionId)) from (select * from UserSession group by sessionId limit 1)
name: UserSession
time count
---- -----
0 3757
> select count(sessionId) from (select * from UserSession group by sessionId limit 1)
name: UserSession
time count
---- -----
0 4206
To my understanding, those should return the same number, since group by sessionId limit 1 already returns distinct sessionIds (in the form of groups).
And indeed, if I execute:
select * from UserSession group by sessionId limit 1
I have 3757 results (groups), not 4206.
In fact, as soon as I put this in a subquery and re-select fields in a parent query, some sessionIds have multiple occurrences in the final result. Not always, since there is 17549 rows in total, but some are.
This is the sign that the limit 1 is somewhat working, but some sessionId still get multiple entries when re-selected. Maybe some kind of undefined behaviour?
I can confirm that I get the same result.
In my experience using nested queries does not always deliver what you expect/want.
Depending on how you use this you could retrieve a list of all values for a tag with:
SHOW TAG VALUES FROM UserSession WITH KEY=sessionId
Or to get the cardinality (number of distinct values for a tag):
SHOW TAG VALUES EXACT CARDINALITY FROM UserSession WITH KEY=sessionId.
Which will return a single row with a single column count, containing a number. You can remove the EXACT modifier if you don't need to be exact about the result: SHOW TAG VALUES CARDINALITY on Influx Documentation.

Summary Count by Group with ActiveRecord

I have a Rails application that holds user data (in an aptly named user_data object). I want to display a summary table that shows me the count of total users and the count of users who are still active (status = 'Active'), created each month for the past 12 months.
In SQL against my Postgres database, I can get the result I want with the following query (the date I use in there is calculated by the application, so you can ignore that aspect):
SELECT total.creation_month,
total.user_count AS total_count,
active.user_count AS active_count
FROM
(SELECT date_trunc('month',"creationDate") AS creation_month,
COUNT("userId") AS user_count
FROM user_data
WHERE "creationDate" >= to_date('2015 12 21', 'YYYY MM DD')
GROUP BY creation_month) AS total
LEFT JOIN
(SELECT date_trunc('month',"creationDate") AS creation_month,
COUNT("userId") AS user_count
FROM user_data
WHERE "creationDate" >= to_date('2015 12 21', 'YYYY MM DD')
AND status = 'Active'
GROUP BY creation_month) AS active
ON total.creation_month = active.creation_month
ORDER BY creation_month ASC
How do I write this query with ActiveRecord?
I previously had just the total user count grouped by month in my display, but I am struggling with how to add in the additional column of active user counts.
My application is on Ruby 2.1.4 and Rails 4.1.6.
I gave up on trying to do this the ActiveRecord way. Instead I just constructed my query into a string and passed the string into
ActiveRecord::Base.connection.execute(sql_string)
This had the side effect that my result set came out as a array instead of a set of objects. So getting at the values went from a syntax (where user_data is the name assigned to a single record from the result set) like
user_data.total_count
to
user_data['total_count']
But that's a minor issue. Not worth the hassle.

Rails: Find biggest number out of val.size

user = SkillUser.find_all_by_skill_id(skill_id)
user.size
gives me: 1 2 2 1 3 1 3 1 3 2 1 1 3
How can I get the biggest value (in this case 3) out of this row of numbers?
Thanks for help
You can use the maximum scope on your ActiveRelation:
SkillUser.maximum(:rating)
If you want the maximum of an attribute called rating.
If you want to count the number of users per skill id, try:
SkillUser.count(:group => :skill_id).max_by { |skill_id,count| count }
This gives you both the skill_id and the number of users for the skill with most users.
For a more efficient way (by doing the whole calculation in SQL), try:
SkillUser.limit(1).reverse_order.count(:group => :skill_id, :order => :count)
# Giving the SQL:
# => SELECT COUNT(*) AS count_all, "skill_users"."skill_id" AS skill_id
# FROM "skill_users" GROUP BY "skill_users"."skill_id"
# ORDER BY "skill_users"."count" DESC LIMIT 1
Be aware that count must be called last because it doesn't return an ActiveRelation for you to further scope the query.
You should use ActiveRecord::Calculations
http://ar.rubyonrails.org/classes/ActiveRecord/Calculations/ClassMethods.html
for performance reasons
1.9.3-194 (main):0 > User.maximum(:id)
(1.6ms) SELECT MAX("users"."id") AS max_id FROM "users"
=> 3
Fastest way to find a single maximum value in an unsorted list of
integer is to scan the list from left to right and memorize the
largest value so far.
If you sort the list first, you get the
additional benefit of easily finding the 2nd, 3rd etc. largest
values easily as well.
If you take one of the "maximum" methods hidden in ruby ... you should check what the implementors are doing to pick the max and compare it to 1. and 2. above :-)
Explanations:
to 1. Doing it this way, you just have to pick each value in the list exactly once and compare it once to the maximum so-far.
to 2. Sorting costs O(n*log n) ops in the average if you got a list with n entries. Obviously this is more than the O(n) in solution 1., but you get a bit more
to 3. Well.. I prefer knowing what happens, but your preferences might vary

Ranking position

I have a Rails application with the following models:
User
Bet
User has many_bets and Bets belongs_to User. Every Bet has a Profitloss value, which states how much the User has won/lost on that Bet.
So to calculate how much a specific User has won overall I cycle through his bets in the following way:
User.bets.sum(:profitloss)
I would like to show the User his ranking compared to all the other Users, which could look something like this:
"Your overall ranking: 37th place"
To do so I need to sum up the overall winnings per User, and find out in which position the current user is.
How do I do that and how to do it, so it don't overload the server :)
Thanks!
You can try something similar to
User.join(:bets).
select("users.id, sum(bets.profitloss) as accumulated").
group("users.id").
order("accumulated DESC")
and then search in the resulting list of "users" (not real users, they have only two meaningful attributes, their ID and a accumulated attribute with the sum), for the one corresponding to the current one.
In any case to get a single user's position, you have to calculate all users' accumulated, but at least this is only one query. Even better, you can store in the user model the accumulated value, and query just it for ranking.
If you have a large number of Users and Bets, you won't be able to compute and sort the global profitloss of each user "on demand", so I suggest that you use a rake task that you schedule regularly (once a day, every hour, etc...)
Add a column position in the User model, get the list of all Users, compute their global profitloss, sort the list of Users with their profitloss, and finally update the position attribute of each User with their position in the list.
Best way to do it is to keep a pre calculated total in your database either on user model itself or on a separate model that has 1:1 relation to user. If you don't do this, you will have to calculate sum for all users at all times in order to get their rating, which means full table operation on bets table. This said, this query will give you desired results, if more than 1 person has the same total, it will count both as rating X:
select id, (select count(h.id) from users u inner join
(select user_id, sum(profitloss) as `total` from bets group by user_id) b2
on b2.user_id = u.id, (select id from users) h inner join
(select user_id, sum(profitloss) as `total` from bets group by user_id) b
on b.user_id = h.id where u.id = 1 and (b.total > b2.total))
as `rating` from users where id = 1;
You will need to plug user.id into query in where id = X
if you add a column to user table to keep track of their total, query is a little simpler, in this example column name is total_profit_loss:
select id, total_profit_loss, (select count(h.username)+1 from users u,
(select username, score from users) h
where id = 1 and (h.total_profit_loss > u.total_profit_loss))
as `rating` from users where id = 1;

Resources