Postgresql – Sorting based on multiple-table columns in Postgres

performancepostgresqlpostgresql-9.6postgresql-performance

I have to query a table in my database in order to generate a feed of relevant items. We're talking in this case about listings and the most relevant are considered those with most votes and most photos.

Here is the structure of the relevant tables for this question:

                                 Table "public.companies"
     Column     |          Type          |                       Modifiers                        
----------------+------------------------+--------------------------------------------------------
 id             | integer                | not null default nextval('companies_id_seq'::regclass)
 name           | character varying(128) | not null
 description    | text                   | 
 tag_line       | character varying(64)  | 
 created_by     | integer                | 
 tsmeta         | tsvector               | 
 logo           | character varying(32)  | 
Indexes:
    "companies_pkey" PRIMARY KEY, btree (id)
    "companies_search_idx" gin (tsmeta)

                     Table "public.company_likes"
   Column   |              Type              |       Modifiers        
------------+--------------------------------+------------------------
 company_id | integer                        | not null
 user_id    | integer                        | not null
 created_at | timestamp(0) without time zone | not null default now()
Indexes:
    "company_likes_pkey" PRIMARY KEY, btree (company_id, user_id)
Foreign-key constraints:
    "company_likes_company_id_foreign" FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
    "company_likes_user_id_foreign" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE

                        Table "public.gallery_images"
   Column    |            Type            |                              Modifiers                              
-------------+----------------------------+---------------------------------------------------------------------
 id          | integer                    | not null default nextval('company_gallery_images_id_seq'::regclass)
 entity_id   | integer                    | not null
 filename    | character varying(32)      | not null
 description | character varying(140)     | 
 featured    | boolean                    | not null default false
 parent_type | gallery_parent_entity_type | not null
Indexes:
    "company_gallery_images_pkey" PRIMARY KEY, btree (id)

This is what I have tried and doesn't seem to work at all:

select
  c.id,
  count(cl.user_id) as likes_count,
  count(gi.id) as images_count
from companies c
  left join company_likes cl on cl.company_id = c.id
  left join gallery_images gi on gi.entity_id = c.id
where gi.parent_type = 'company'
group by c.id
order by likes_count desc, images_count desc;

This is the result of the query above:

 id  | likes_count | images_count 
-----+-------------+--------------
   1 |      195820 |       195820
   2 |      145968 |       145968
   3 |           1 |            1
 360 |           1 |            1
(4 rows)

This is obviously an untrue result because:

  • the likes_count cell for company id = 1 represents the actual total number of likes recorded in the company_likes table
  • the images_count has the same value with likes_count for some reason I don't understand

Also, this query only fetches companies with likes and images. However, since I need to generate a feed, I don't need filtering like that. I would need to have 0 instead in those cells.

Thinks that crossed my mind about optimizing the speed and making this query simpler include creating a VIEW that holds the likes_count and images_count as columns but since likes_count would change all the time (because there are about 50k users) regenerating the sorted view would be a huge hit on performance as there are about 20k companies in the companies table.

How can I make this work? What are you recommendations for my database structure? (I am aware it is far from the best out there)

Thank you very much!

P.S. This is the version of Postgres on my system:

PostgreSQL 9.6.6 on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609, 64-bit

Best Answer

Your given query will definitely return zero cells for companies with no likes as long as they have images. Either you are misinterpreting something or you have posted the wrong query. Companies with no images won't be returned because where gi.parent_type = 'company' will filter them out.

You have what is effectively an inner join between company_likes and gallery_images (slightly different due to the left join). So if a company has 3 likes and 2 images, you will get 6 rows returned for that company, and the tally will be 6 for both. Because the columns being counted over are both declared NOT NULL, the only way you will get different counts between the 2 count columns is if the LEFT part of the join kicks in.

It sounds like you want to move both counts into sub-selects, so they are decoupled from each other:

select
  c.id,
  (select count(cl.user_id) from company_likes cl 
     where cl.company_id = c.id ) as likes_count,
  (select count(gi.id) from gallery_images gi
     where gi.entity_id=c.id and gi.parent_type = 'company') as images_count  
from companies c
order by likes_count desc, images_count desc;