Rails select by number of associated records - sql

I have following models in my rails app:
class Student < ApplicationRecord
has_many :tickets, dependent: :destroy
has_and_belongs_to_many :articles, dependent: :destroy
class Article < ApplicationRecord
has_and_belongs_to_many :students, dependent: :destroy
class Ticket < ApplicationRecord
belongs_to :student, touch: true
I need to extract all Students who has less than articles and I need to extract all Students who's last ticket title is 'Something'.
Everything I tried so far takes a lot of time. I tried mapping and looping through all Students. But I guess what I need is a joined request. I am looking for the most efficient way to do it, as database I am working with is quite large.

go with #MCI's answer for your first question. But a filter/select/find_all or whatever (although I havn't heared about filter method in ruby) through students record takes n queries where n is the number of student records (called an n+1 query).
studs = Student.find_by_sql(%{select tmp.id from (
select student_id as id from tickets where name='Something' order by tickets.created_at desc
) tmp group by tmp.id})

You asked
"I need to extract all Students who has less than articles". I'll presume you meant "I need to extract all Students who have less than X articles". In that case, you want group and having https://guides.rubyonrails.org/active_record_querying.html#group.
For example, Article.group(:student_id).having('count(articles.id) > X').pluck(:student_id).
To address your second question, you can use eager loading https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations to speed up your code.
result = students.filter do |student|
students.tickets.last.name == 'Something'
end

Here association is HABTM so below query should work
x = 10
Student.joins(:articles).group("articles_students.student_id").having("count(articles.id) < ?",x)

Related

How to remove duplicates before aggregating sql

I'm struggling to get the correct points return value from a many to many through relationship. I have the tables seasons, teams, drivers, results and driver_teams with the relationship below
class Season < ApplicationRecord
has_many :driver_teams
has_many :drivers, through: :driver_teams
has_many :teams, through: :driver_teams
end
class DriverTeam < ApplicationRecord
belongs_to :season
belongs_to :driver
belongs_to :team
has_many :results
end
class Team < ApplicationRecord
has_many :driver_teams
has_many :results, through: :driver_teams
end
class Driver < ApplicationRecord
has_many :driver_teams
has_many :results, through: :driver_teams
end
class Result < ApplicationRecord
belongs_to :driver_team
has_one :driver, though: :driver_team
has_one :team, though: :driver_team
end
The results table has a points attribute that is just a simple interger field
I'm trying to get the sum of all points for each team within a seaon like below
season.teams.joins(:results).select('teams.*, SUM(results.points) AS points').group('teams.id')
But because a team can have multiple drivers using the Driverteam through table, these points are being duplicated by the number of drivers per team, since referencing teams from a season will return multiple teams within the through table.
The ideal result is to have season.teams return only the single instances of each team for a season.
Is there a way to prevent season.teams from returning duplicates of the teams before running an aggregate SQL function?
I've tried simply using season.teams.distinct but the distinct statement appears to be run after the group by so its still including the duplicates during the calulation.
Maybe try to select the distinct before and don't use the function .distinct of ruby. Do something like (Select distinct seasons FROM..). It should leave you without duplicates.
I ended up solving the issue by just simply adding driver_teams.id into the group by clause
season.teams.joins(:results).select('teams.*, SUM(results.points) AS points').group('teams.id, driver_teams.id')

Find all records which have a count of an association of zero and none-zero

class Gallery < ApplicationRecord
has_many :associated_images, as: :imageable
end
class Event < ApplicationRecord
has_many :associated_images, as: :imageable
end
class Image < ApplicationRecord
has_many :associated_images
end
class AssociatedImage < ApplicationRecord
belongs_to :imageable, polymorphic: true
belongs_to :image
end
I'd like to get all the images which are being used, and a different query to get all the images that are not being used (based on AssociatedImage).
I tried Image.joins(:associated_images).group('images.id').having('count(image_id) > 0') and it returns the correct result. But when I run Image.joins(:associated_images).group('images.id').having('count(image_id) = 0'), it returns an empty #<ActiveRecord::Relation []> and I'm not sure why is that.
My query is based off Find all records which have a count of an association greater than zero's discussion
The reason is that in SQL a count of zero happens when there are no rows. So if there are no rows, even group, there is no result.
What you want is
Image.left_joins(:associated_images).where(associated_images: {id: nil}).group('images.id')
When SQL does a left join, for an image which does not have an associated image, it fills in NULL for all the columns in the associated_images table. So the ones where the associated_images.id is nil, are the ones we want.

Ordering records by an association presence

Lets say I have an instructions table which is associated to a surveys table through survey_instructions join table.
What I need to achieve is to fetch all instruction records, but ordered by an association presence with a given survey.
So instructions associated with a given survey will go first, and then all other instructions which have no association with this survey.
class Instruction < ApplicationRecord
has_many :survey_instructions, dependent: :destroy
has_many :surveys, through: :survey_instructions
and
class Survey < ApplicationRecord
has_many :survey_instructions, dependent: :destroy
has_many :instructions, through: :survey_instructions
and
class SurveyInstruction < ApplicationRecord
belongs_to :survey
belongs_to :instruction
and
Could this be achieved by chaining active record queries somehow? Would appreciate any thoughts on this
Yes you can achieve this by ActiveRecord query. Try this:
survay_id = 10
#instructions = Instruction.includes(:survey_instructions).order("(CASE WHEN survey_instructions.survay_id = #{survay_id} THEN 1 ELSE 2 END) ASC NULLS LAST")
Happy coding :)

Active Record query to find records that match all conditions in Rails has_many through relationship

I have two models, Apartments and Amenities, which are associated through ApartmentAmenities. I am trying to implement a filter where I only show apartments that have all of the amenities specified.
class Amenity < ActiveRecord::Base
has_many :apartment_amenities
has_many :apartments, through: :apartment_amenities
end
class ApartmentAmenity < ActiveRecord::Base
belongs_to :apartment
belongs_to :amenity
end
class Apartment < ActiveRecord::Base
has_many :apartment_amenities
has_many :amenities, through: :apartment_amenities
end
I've got a query working that will return all apartments that match at least one of the amenities of given set like so:
Apartment.joins(:apartment_amenities).where('apartment_amenities.amenity_id IN (?)', [1,2,3])
but this isn't quite what I'm going for.
Alright, after giving up for a few days then getting back to it, I finally found this question: How to find records, whose has_many through objects include all objects of some list?
Which led me to the answer that works properly:
def self.with_amenities(amenity_ids)
where("NOT EXISTS (SELECT * FROM amenities
WHERE NOT EXISTS (SELECT * FROM apartment_amenities
WHERE apartment_amenities.amenity_id = amenities.id
AND apartment_amenities.apartment_id = apartments.id)
AND amenities.id IN (?))", amenity_ids)
end

How to optimise this ActiveRecord query?

I'm still learning ruby, rails and ActiveRecord everyday. Right now I'm learning SQL through a new small app I'm building but the problem is that the main view of my app currently does ~2000 queries per page refresh, oouuuppps.
So now that I know I have all the required information in my DB and that I can display them correctly, it is time for me to optimise them but I just don't know where to start to be honest.
These are my models associations
class League < ActiveRecord::Base
belongs_to :user
has_many :league_teams
has_many :teams, :through => :league_teams
end
class Team < ActiveRecord::Base
has_many :gameweeks
has_many :league_teams
has_many :leagues, :through => :league_teams
end
class Gameweek < ActiveRecord::Base
belongs_to :team
has_and_belongs_to_many :players
has_and_belongs_to_many :substitutes, class_name: "Player", join_table: "gameweeks_substitutes"
belongs_to :captain, class_name: "Player"
belongs_to :vice_captain, class_name: "Player"
end
class Player < ActiveRecord::Base
serialize :event_explain
serialize :fixtures
serialize :fixture_history
has_many :gameweeks, class_name: "captain"
has_many :gameweeks, class_name: "vice_captain"
has_and_belongs_to_many :gameweeks
has_many :player_fixtures
end
So this is my controller:
#league = League.includes(teams: [{gameweeks: [{players: :player_fixtures} , :captain]}]).find_by(fpl_id:params[:fpl_id])
#teams = #league.teams
#defense_widget_leaderboard = #league.position_based_leaderboard_stats(#teams, ['Defender', 'Goalkeeper'])
And this is one of the method in my League Model:
def position_based_leaderboard_stats(teams,positions_array)
leaderboard = []
teams.each do |team|
position_points = 0
gameweeks = team.gameweeks
gameweeks.each do |gameweek|
defense = gameweek.players.where(type_name:positions_array)
defense.each do |player|
player.player_fixtures.where(gw_number: gameweek.number).each do |p|
position_points += p.points
end
end
end
leaderboard << [team.team_name,position_points]
end
return leaderboard.sort_by {|team| team[1]}.reverse
end
I have 4 methods that look more or less the same thing as the one above. Each are doing between 300 and 600 queries.
As far as I read it only, it is a typical case of N+1 queries. I tried to reduce with the includes in the #league but it got me down from 2000 to 1800 queries.
I looked into group_by, joins and sum but I couldn't make it work.
The closest thing I got to working was this
players = PlayerFixture.group("player_id").sum(:points)
Where I could then query by doing players[player.id] but that doesn't give me the right results anyway because it doesn't take into account the Gameweeks > Players > Player_fixtures relationship.
How can I reduce the numbers of queries I'm doing? I went on #RubyOnRails on freenode and people told me it can be done in 1 query but wouldn't point me in any directions or help me...
Thanks
In your position_based_leaderboard_stats N+1 problem appears, too. So you can preload all your associations before each cycles:
def position_based_leaderboard_stats(teams,positions_array)
leaderboard = []
Team.preload(gameweeks: players).where('players.type_name=?', positions_array )
your code
Also, you could add player_fixtures to preload statement, but I can't understand dependencies of those associations, sorry.
Spend some time with SQL. Finally found the query that can help me with that. I also discovered SQL Views and how to use them through Activerecord which is pretty neat.
Final successful query
CREATE VIEW team_position_points AS
select teams.id as team_id, teams.team_name, players.type_name, sum(points) as points
from teams
inner join gameweeks on teams.id = gameweeks.team_id
inner join gameweeks_players on gameweeks.id = gameweeks_players.gameweek_id
inner join players on gameweeks_players.player_id = players.id
inner join player_fixtures on players.id = player_fixtures.player_id AND player_fixtures.gw_number = gameweeks.number
group by teams.id, players.type_name