简体   繁体   中英

Use the count of a has_many relation to select - using having?

Order has_many deliveries

Order has a column number_of_deliveries_paid_for

How do I select all the orders where the number of deliveries (COUNT(deliveries.id)) is less than number_of_deliveries_paid_for

At the moment, I'm doing:

Order.joins('LEFT OUTER JOIN deliveries ON deliveries.order_id = orders.id')
     .select('orders.*, COUNT(deliveries.id) AS delivery_count')
     .select { |o| o if o.number_of_deliveries_paid_for > o.delivery_count }

But this returns an array, it'd be nice to return an ActiveRecord::Relation, and I imagine it would be more efficient to do it in sql.

Update _ __ _ __ _ __ _ __ _ __

This seems to work

Order.joins('LEFT OUTER JOIN deliveries ON deliveries.order_id = orders.id')
     .group('orders.id')
     .having('COUNT(deliveries.id) < orders.number_of_deliveries_paid_for')

I can chain other scopes on the end of it etc. But if I add .count to the end, I get the error

ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column
'orders.number_of_deliveries_paid_for' in 'having clause':
SELECT COUNT(*) AS count_all, orders.id AS orders_id FROM `orders`
LEFT OUTER JOIN deliveries ON deliveries.order_id = orders.id GROUP BY orders.id 
HAVING COUNT(deliveries.id) < orders.number_of_deliveries_paid_for

which makes me anxious!

I don't know how to write that in Ruby, but from an SQL point of view, I feel like you need a sub-query to aggregate the number of deliveries by order.

Something like that (I type directly to SO -- please forgive typos or stupid syntax errors):

SELECT orders.* FROM orders 
                LEFT OUTER JOIN (SELECT COUNT(*) AS C, deliveries.order_id AS ID
                                        FROM deliveries 
                                        GROUP BY deliveries.order_id) AS S
                ON orders.id = S.ID
                WHERE S.C < orders.number_of_deliveries_paid_for
                   OR (deliveries.id IS NULL AND orders.number_of_deliveries_paid_for > 0);

EDIT: As a matter of fact, this is much more simple using ORDER BY ... HAVING :

SELECT *, count(*) AS C FROM orders 
                        LEFT OUTER JOIN deliveries 
                        ON deliveries.order_id = orders.id
       GROUP BY orders.id 
       HAVING C < orders.number_of_deliveries_paid_for
           OR (deliveries.id IS NULL AND orders.number_of_deliveries_paid_for > 0);

And for the curious, here is my test case:

mysql> select * from orders;
+------+---------+-------------------------------+
| id   | name    | number_of_deliveries_paid_for |
+------+---------+-------------------------------+
|    1 | Order 1 |                             0 |
|    2 | Order 2 |                             2 |
|    3 | Order 3 |                             3 |
+------+---------+-------------------------------+
3 rows in set (0.00 sec)

mysql> select * from deliveries;
+----+----------+
| id | order_id |
+----+----------+
|  1 |        2 |
|  2 |        2 |
|  3 |        3 |
|  4 |        3 |
+----+----------+
4 rows in set (0.00 sec)

mysql> select * from orders JOIN deliveries ON deliveries.order_id = orders.id    -> ;
+------+---------+-------------------------------+----+----------+
| id   | name    | number_of_deliveries_paid_for | id | order_id |
+------+---------+-------------------------------+----+----------+
|    2 | Order 2 |                             2 |  1 |        2 |
|    2 | Order 2 |                             2 |  2 |        2 |
|    3 | Order 3 |                             3 |  3 |        3 |
|    3 | Order 3 |                             3 |  4 |        3 |
+------+---------+-------------------------------+----+----------+
4 rows in set (0.00 sec)

As a side note, see the difference between JOIN and LEFT JOIN :

mysql> select *, COUNT(*) from orders LEFT JOIN deliveries ON deliveries.order_id = orders.id group by orders.id;
+------+---------+-------------------------------+------+----------+----------+
| id   | name    | number_of_deliveries_paid_for | id   | order_id | COUNT(*) |
+------+---------+-------------------------------+------+----------+----------+
|    1 | Order 1 |                             0 | NULL |     NULL |        1 |
|    2 | Order 2 |                             2 |    1 |        2 |        2 |
|    3 | Order 3 |                             3 |    3 |        3 |        2 |
+------+---------+-------------------------------+------+----------+----------+
3 rows in set (0.00 sec)

mysql> select *, COUNT(*) from orders JOIN deliveries ON deliveries.order_id = orders.id group by orders.id;
+------+---------+-------------------------------+----+----------+----------+
| id   | name    | number_of_deliveries_paid_for | id | order_id | COUNT(*) |
+------+---------+-------------------------------+----+----------+----------+
|    2 | Order 2 |                             2 |  1 |        2 |        2 |
|    3 | Order 3 |                             3 |  3 |        3 |        2 |
+------+---------+-------------------------------+----+----------+----------+
2 rows in set (0.01 sec)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM