10

I have a Property model that has_many :photos. I want to count the number of properties that have one or more photo.

How do I do that?

I have tried the simple:

> Property.where('properties.photos.count > ?', 0).count

   (3.1ms)  SELECT COUNT(*) FROM "properties" WHERE (properties.photos.count > 1)
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR:  missing FROM-clause entry for table "photos"
LINE 1: SELECT COUNT(*) FROM "properties"  WHERE (properties.photos....
                                                  ^
: SELECT COUNT(*) FROM "properties"  WHERE (properties.photos.count > 0)
from /ruby-2.3.0@myproject/gems/activerecord-3.2.22.5/lib/active_record/connection_adapters/postgresql_adapter.rb:1163:in `async_exec'
Caused by PG::UndefinedTable: ERROR:  missing FROM-clause entry for table "photos"
LINE 1: SELECT COUNT(*) FROM "properties"  WHERE (properties.photos....

to:

> Property.joins(:photos).where('photos.count > ?', 0).count

   (3.7ms)  SELECT COUNT(*) FROM "properties" INNER JOIN "photos" ON "photos"."property_id" = "properties"."id" WHERE (photos.count > 0)
ActiveRecord::StatementInvalid: PG::GroupingError: ERROR:  aggregate functions are not allowed in WHERE
LINE 1: ..."photos"."property_id" = "properties"."id" WHERE (photos.cou...
                                                             ^
: SELECT COUNT(*) FROM "properties" INNER JOIN "photos" ON "photos"."property_id" = "properties"."id" WHERE (photos.count > 0)
from ruby-2.3.0@myproject/gems/activerecord-3.2.22.5/lib/active_record/connection_adapters/postgresql_adapter.rb:1163:in `async_exec'
Caused by PG::GroupingError: ERROR:  aggregate functions are not allowed in WHERE
LINE 1: ..."photos"."property_id" = "properties"."id" WHERE (photos.cou...

to the more advanced:

>Property.includes(:photos).group(['property.id', 'photos.id']).order('COUNT(photos.id) DESC').count

(0.6ms)  SELECT COUNT(DISTINCT "properties"."id") AS count_id, property.id AS property_id, photos.id AS photos_id FROM "properties" LEFT OUTER JOIN "photos" ON "photos"."property_id" = "properties"."id" GROUP BY property.id, photos.id ORDER BY COUNT(photos.id) DESC
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR:  missing FROM-clause entry for table "property"
LINE 1: ...CT COUNT(DISTINCT "properties"."id") AS count_id, property.i...
                                                             ^
: SELECT COUNT(DISTINCT "properties"."id") AS count_id, property.id AS property_id, photos.id AS photos_id FROM "properties" LEFT OUTER JOIN "photos" ON "photos"."property_id" = "properties"."id" GROUP BY property.id, photos.id ORDER BY COUNT(photos.id) DESC
from ruby-2.3.0@myproject/gems/activerecord-3.2.22.5/lib/active_record/connection_adapters/postgresql_adapter.rb:1163:in `async_exec'
Caused by PG::UndefinedTable: ERROR:  missing FROM-clause entry for table "property"
LINE 1: ...CT COUNT(DISTINCT "properties"."id") AS count_id, property.i...

and a few other variations, and they all produce similar errors.

What am I doing wrong?

Note: All I want is the count of properties that have photos.count > 0. I don't want a hash of all the properties and the count of photos. In other words, if there are 5000 properties in my db, I want to build a scope that returns just the properties that actually have photos.

marcamillion
  • 32,933
  • 55
  • 189
  • 380
  • Please clarify via edits, not comments. Please in code questions give a [mcve]--cut & paste & runnable code; example input with desired & actual output (including verbatim error messages); clear specification & explanation. That includes the least code you can give that is code that you show is OK extended by code that you show is not OK. (Debugging fundamental.) PS Your problem is a code extension not returning what you expect. Wait until that's solved to ask re your overall goal. PS "having". Read the manual re aggregating. – philipxy May 28 '19 at 08:34

6 Answers6

8

Since all you want is the Propertys with Photos then an INNER JOIN is all you need.

Property.joins(:photos) 

That is it. If you want a scope then

class Property < ActiveRecord::Base
  scope :with_photos, -> {joins(:photos)} 
end 

To get the count using rails 3.2

Property.with_photos.count(distinct: true)  

You could also use: in rails 3.2

Property.count(joins: :photos, distinct: true) 

ActiveRecord::Calculations#count Doc

This will execute

SELECT 
  COUNT(DISTINCT properties.id) 
FROM 
  properties
  INNER JOIN photos ON photos.property_id = properties.id
engineersmnky
  • 25,495
  • 2
  • 36
  • 52
3

EDIT:

Property.joins(:photos).group('photos.property_id').having('count(photos.property_id) > 1').count

#=> {1234=>2}  # 1234 is property id 2 is count of photos 

You will get the property_ids with the number of associated photos with it.

Old Answer:

You can get the properties with atleast one photos associated with it

Property.includes(:photos).where.not(photos: { property_id: nil })

As you are using rails 3.2 .not will not work you have to use

Property.includes(:photos).where("property_id IS NOT null")
Deepak Mahakale
  • 22,834
  • 10
  • 68
  • 88
  • "At least one" and "more than one" are different. – sawa Jan 17 '19 at 07:53
  • I get this error when I try this: `PG::UndefinedColumn: ERROR: column "property_id" does not exist LINE 1: SELECT "properties".* FROM "properties" WHERE (property_id ...` – marcamillion Jan 17 '19 at 08:29
  • @sawa didn't notice the > 1. Made the changes according to it now – Deepak Mahakale Jan 17 '19 at 09:11
  • @DeepakMahakale when I tried your update, I get an empty hash as the result: `Property.joins(:photos).group('photos.property_id').having('count(photos.property_id) > 1').count (74.8ms) SELECT COUNT(*) AS count_all, photos.property_id AS photos_property_id FROM "properties" INNER JOIN "photos" ON "photos"."property_id" = "properties"."id" GROUP BY photos.property_id HAVING count(photos.property_id) > 1 => {}` – marcamillion Jan 18 '19 at 00:14
  • That means you probably don't have any properties with more than one photo. Can you please check manually by adding 2 photos to a property and verify if you get the result – Deepak Mahakale Jan 18 '19 at 06:58
  • @DeepakMahakale This solution returns a hash of hashes with all the properties that have more than 1 photo and the count of photos for that property. I don't want that. All I want is 1) A simple count of the number of properties that have 1 or more photo, and 2) I would like that set of properties returned so I can create a scope of just those properties. Also, I do have lots of properties with more than 1 photo. – marcamillion May 28 '19 at 08:20
3
Property.includes(:photos).where("SELECT count(photos.id) > 0 FROM photos WHERE property_id = properties.id")

As a scope:

scope :with_photos, -> { where("SELECT count(photos.id) > 0 FROM photos WHERE property_id = properties.id") }
FanaHOVA
  • 113
  • 1
  • 1
  • 7
  • This will not work for 2 reasons 1) `includes` will not automatically join in this case so `photos` has no reference 2) the where clause has no condition it is just a subquery which will be a syntax error – engineersmnky Jun 04 '19 at 21:39
2

You can try like this, I have done in my projects,

Photo.group(:property_id).count

You will get property id with photos count

results = { 3314=>3, 2033=>3, 3532=>2, 3565=>6, 3510=>1, 3022=>7, 648=>2, 570=>3, 4678=>3, 3540=>1, 3489=>4, 536=>1, 1715=>4 }
  • This is what I had initially, but it didn't give me what I need -- which is a simple integer that returns the number of Property records that have at least 1 photo associated with them. – marcamillion Jun 10 '19 at 01:47
1

Give this a go:

class Property < ApplicationRecord
  has_many :photos

  def self.with_photos
    self.all.reject { |p| p.photos.empty? }
  end
end

Property.with_photos.count

Source

More Efficient (Rails 4+):

Property.joins(:photos).uniq.count

Source

More Efficient (Rails 5.1+):

Property.joins(:photos).distinct.count

Source

engineersmnky
  • 25,495
  • 2
  • 36
  • 52
blnc
  • 4,384
  • 1
  • 28
  • 42
1

According to your requirement you can try this

1) A simple count of the number of properties that have 1 or more photo

To just get the number of properties which have one or more photo you can do this

Property.joins(:photos).distinct.count

As we are not using group the distinct or uniq is necessary. distinct will return ActiveRecord_Relation and uniq will return Array.

2) I would like that set of properties returned so I can create a scope of just those properties. Also, I do have lots of properties with more than 1 photo.

To get all the property objects which have one or more than one photo you can use the same query:

Property.joins(:photos).distinct

or you can use the group_by clause:

Property.joins(:photos).group('properties.id')

The difference will be that when you will use size method on the group query it will return a hash with the property_id as key and number of photos on the property as value.

Performance Update:

If you always require the count of associated object and you want to fetch it efficiently you may use counter_cache like this:

class Photo < ApplicationRecord
  belongs_to :property, counter_cache: true
end

class Property < ApplicationRecord
  has_many :photos
end

And then you will have to add a column in the properties table named photos_count and Rails will automatically update the column each time a new photo record is added or a exisisting photo record is removed. And then you can also query directly:

Property.where('photos_count > ?', 1)

This is optional and you can do this if you are facing performance issues with the data fetch.

Deepesh
  • 6,138
  • 1
  • 24
  • 41