Reputation: 3499
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!
Upvotes: 0
Views: 273
Reputation: 51990
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)
Upvotes: 1