RAILS SQL Subquery optimization - ruby-on-rails

I have a query which looks like this:
#inventory = Pack.find_by_sql("SELECT Packs.id, "+
" (SELECT COUNT(*) FROM Stocks WHERE (Stocks.pack_id = Packs.id AND Stocks.status = 'online' AND Stocks.user_id = #{current_user.id})) AS online,"+
" (SELECT COUNT(*) FROM Stocks WHERE (Stocks.pack_id = Packs.id AND Stocks.status = 'offline' AND Stocks.user_id = #{current_user.id})) AS offline,"+
" (SELECT COUNT(*) FROM Stocks WHERE (Stocks.pack_id = Packs.id AND Stocks.status = 'depositing' AND Stocks.user_id = #{current_user.id})) AS depositing,"+
" (SELECT COUNT(*) FROM Stocks WHERE (Stocks.pack_id = Packs.id AND Stocks.status = 'withdrawing' AND Stocks.user_id = #{current_user.id})) AS withdrawing,"+
" (SELECT COUNT(*) FROM Stocks WHERE (Stocks.pack_id = Packs.id AND Stocks.status = 'selling' AND Stocks.user_id = #{current_user.id})) AS selling,"+
" (SELECT COUNT(*) FROM Transactions WHERE (Transactions.pack_id = Packs.id AND Transactions.status = 'buying' AND Transactions.buyer_id = #{current_user.id})) AS buying"+
" FROM Packs WHERE disabled = false")
I am thinking there's a way to make a new sub-query so that instead of
SELECT FROM Stocks
the query selects from a stored table
SELECT FROM (Stocks WHERE (Stocks.pack_id = Packs.id AND Stocks.user_id = #{current_user.id}))
which would only be queried once. Then the WHERE Stocks.status = ? stuff would be applied to that stored table.
Any help guys?

The best query depends on data distribution and other details.
This is very efficient as long as most pack_id from the subqueries are actually used in the join to packs (most packs are NOT disabled):
SELECT p.id
, s.online, s.offline, s.depositing, s.withdrawing, s.selling, t.buying
FROM packs p
LEFT JOIN (
SELECT pack_id
, count(status = 'online' OR NULL) AS online
, count(status = 'offline' OR NULL) AS offline
, count(status = 'depositing' OR NULL) AS depositing
, count(status = 'withdrawing' OR NULL) AS withdrawing
, count(status = 'selling' OR NULL) AS selling
FROM stocks
WHERE user_id = #{current_user.id}
AND status = ANY('{online,offline,depositing,withdrawing,selling}'::text[])
GROUP BY 1
) s ON s.pack_id = p.id
LEFT JOIN (
SELECT pack_id, count(*) AS buying
FROM transactions
WHERE status = 'buying'
AND buyer_id = #{current_user.id}
) t ON t.pack_id = p.id
WHERE NOT p.disabled;
In pg 9.4 you can use the aggregate FILTER clause:
SELECT pack_id
, count(*) FILTER (WHERE status = 'online') AS online
, count(*) FILTER (WHERE status = 'offline') AS offline
, count(*) FILTER (WHERE status = 'depositing') AS depositing
, count(*) FILTER (WHERE status = 'withdrawing') AS withdrawing
, count(*) FILTER (WHERE status = 'selling') AS selling
FROM stocks
WHERE ...
Details:
How can I simplify this game statistics query?
Use crosstab() for the pivot table to make that faster, yet:
SELECT p.id
, s.online, s.offline, s.depositing, s.withdrawing, s.selling, t.buying
FROM packs p
LEFT JOIN crosstab(
$$
SELECT pack_id, status, count(*)::int AS ct
FROM stocks
WHERE user_id = $$ || #{current_user.id} || $$
AND status = ANY('{online,offline,depositing,withdrawing,selling}'::text[])
GROUP BY 1, 2
ORDER BY 1, 2
$$
,$$SELECT unnest('{online,offline,depositing,withdrawing,selling}'::text[])$$
) s (pack_id int
, online int
, offline int
, depositing int
, withdrawing int
, selling int
) USING (pack_id)
LEFT JOIN (
SELECT pack_id, count(*) AS buying
FROM transactions
WHERE status = 'buying'
AND buyer_id = #{current_user.id}
) t ON t.pack_id = p.id
WHERE NOT p.disabled;
Details here:
PostgreSQL Crosstab Query
If most packs are disabled, LATERAL joins will be faster (requires pg 9.3 or later):
SELECT p.id
, s.online, s.offline, s.depositing, s.withdrawing, s.selling, t.buying
FROM packs p
LEFT JOIN LATERAL (
SELECT pack_id
, count(status = 'online' OR NULL) AS online
, count(status = 'offline' OR NULL) AS offline
, count(status = 'depositing' OR NULL) AS depositing
, count(status = 'withdrawing' OR NULL) AS withdrawing
, count(status = 'selling' OR NULL) AS selling
FROM stocks
WHERE user_id = #{current_user.id}
AND status = ANY('{online,offline,depositing,withdrawing,selling}'::text[])
AND pack_id = p.id
GROUP BY 1
) s ON TRUE
LEFT JOIN LATERAL (
SELECT pack_id, count(*) AS buying
FROM transactions
WHERE status = 'buying'
AND buyer_id = #{current_user.id}
AND pack_id = p.id
) t ON TRUE
WHERE NOT p.disabled;
Why LATERAL? And are there alternatives in pg 9.1?
Record returned from function has columns concatenated

If what you're after is a count of the various types, something like the following would be much less code and easier to read/maintain, IMO...
You could split them up into the different tables, so, for stocks, something like this:
#inventory = Pack.find_by_sql("SELECT status, count(*)
FROM stocks
WHERE user_id = ?
GROUP BY status
ORDER BY status", current_user.id)
Note the importance of using ? to prevent SQL injection. Also, Ruby supports multiline strings, so there's no need to quote and concatenate every line.
You can do something similar for the other tables.

Related

How to use ActiveRecord for a sql query with subquery in FROM clause

Do you know how to translate a SQL query with a subquery in a FROM clause ?
Here is an example :
SELECT
*
FROM
cars
WHERE
cars.id IN (
SELECT
cars.user_id
FROM
cars AS cf, (SELECT
cars.user_id, MAX(consumption) AS consumption FROM
cars
GROUP BY
user_id) AS t1
WHERE
cars.consumption = t1.consumption
AND
cars.user_id = 2
AND
t1.user_id = cars.user_id)
Car.where('id IN (SELECT cars.user_id FROM cars AS cf, (SELECT cars.user_id, MAX(consumption) AS consumption FROM cars GROUP BY user_id) AS t1 WHERE cars.consumption = t1.consumption AND cars.user_id = :user_id AND t1.user_id = cars.user_id)', user_id: 2)

how to make one from the the following two queries

i have two queries:
the first one query is:
SELECT sale.saleid,
sale.totalpaid,
item.itemname AS item,
stock.saleprice AS Price,
invoice.qty,
sale.discount,
invoice.saleprice AS [invoice saleprice],
cetegory.catname AS [Cateogory],
cetegory.subcat AS [Sub Catgry],
vehicle.vehicle_name AS [Vehicle],
vehicle.vehicle_model AS [Model],
item.model_number AS [Part No.],
sale.date,
stock.size,
sale.customerid
FROM invoice
JOIN item
ON invoice.itemid = item.itemid
JOIN sale
ON invoice.saleid = sale.saleid
JOIN stock
ON item.itemid = stock.itemid
JOIN cetegory
ON item.catid = cetegory.catid
JOIN vehicle
ON item.vehicleid = vehicle.vehicleid
WHERE sale.saleid = 5
and the second query is:
SELECT customer.customername,
customer.customercontact,
customer.customeraddress,
account.account_type
FROM account
JOIN customer
ON customer.customerid = account.customerid
Now i want to combine these two queries by customer id because i have "custome id" in sale table
You could use a CTE and then join both:
WITH CUST
AS
(SELECT customer.customername,
customer.customercontact,
customer.customeraddress,
account.account_type ,
customer.customerid
FROM account
JOIN customer
ON customer.customerid = account.customerid
),
SALES
AS
(
SELECT sale.saleid,
sale.totalpaid,
item.itemname AS item,
stock.saleprice AS Price,
invoice.qty,
sale.discount,
invoice.saleprice AS [invoice saleprice],
cetegory.catname AS [Cateogory],
cetegory.subcat AS [Sub Catgry],
vehicle.vehicle_name AS [Vehicle],
vehicle.vehicle_model AS [Model],
item.model_number AS [Part No.],
sale.date,
stock.size,
sale.customerid
FROM invoice
JOIN item
ON invoice.itemid = item.itemid
JOIN sale
ON invoice.saleid = sale.saleid
JOIN stock
ON item.itemid = stock.itemid
JOIN cetegory
ON item.catid = cetegory.catid
JOIN vehicle
ON item.vehicleid = vehicle.vehicleid
WHERE sale.saleid = 5
)
SELECT * FROM CUST
LEFT JOIN SALES
ON CUST.customerid = SALES.customerid

Rails find_by_SQL with Rails

I'm using a find_by_sql method to search users in my userstable.
is there a possibility to use rails code in the select statement?
User.find_by_sql ["SELECT DISTINCT
users.*
FROM
users
JOIN
clients_courses cc
ON
cc.client_id = users.client_id
LEFT JOIN
memberships m
ON
m.user_id = users.id AND m.course_id = cc.course_id
WHERE
cc.course_id = ?
AND
m.user_id IS NULL
AND
users.active = ?
AND
users.firstname LIKE ? or users.lastname LIKE ?
AND NOT IN ( RAILS CODE )", self.id, true, "#{search}%", "#{search}%"]
end
I Marked the position with RAILS CODE
I want to do someting linke this:
Membership.where("course_id = ?", self.id).users
is there a way to do this?
You can do this -
member_user_ids = []
Membership.where("course_id = ?", self.id).map{|membership| membership.users.map{|user| member_user_ids << user.id}}
# you might want to put a uniq! on member_user_ids
User.find_by_sql ["SELECT DISTINCT
users.*
FROM
users
JOIN
clients_courses cc
ON
cc.client_id = users.client_id
LEFT JOIN
memberships m
ON
m.user_id = users.id AND m.course_id = cc.course_id
WHERE
cc.course_id = ?
AND
m.user_id IS NULL
AND
users.active = ?
AND
users.firstname LIKE ? or users.lastname LIKE ?
AND users.id NOT IN ( #{member_user_ids.join(',')} )", self.id, true, "#{search}%", "#{search}%"]
You can also have a look at link which explains how to put array of strings in where clause.

Why I cannot use both ORDER BY and DISTINCT * in SQL?

I'm trying to do the following, and if I were to uncomment the distinct it will break. Also if I comment out the order and leave the distinct in, it will work.
Contestant.joins('INNER JOIN votes AS V ON V.contestant_id = contestants.id AND V.season_id = '+ season_number.to_s)
.joins('LEFT OUTER JOIN votes AS XV ON (XV.contestant_id = '+self.id.to_s+') AND (XV.tribal_council_key = V.tribal_council_key) AND XV.contestant_voted_for_id = V.contestant_voted_for_id')
.joins('INNER JOIN season_rosters ON season_rosters.season_id = V.season_id')
.where('V.is_jury_vote = (?) AND V.contestant_id <> (?) AND XV.tribal_council_key IS NOT NULL', :false, self.id)
.order('season_rosters.finished')
#.distinct
The error I get is below...
TinyTds::Error: Incorrect syntax near '*'.: EXEC sp_executesql N'SELECT DISTINCT *, __order FROM ( SELECT [contestants].*, DENSE_RANK() OVER (ORDER BY season_rosters.finished ASC) AS __order, ROW_NUMBER() OVER (PARTITION BY [contestants].* ORDER BY season_rosters.finished ASC) AS __joined_row_num FROM [contestants] INNER JOIN votes AS V ON V.contestant_id = contestants.id AND V.season_id = 6 LEFT OUTER JOIN votes AS XV ON (XV.contestant_id = 112) AND (XV.tribal_council_key = V.tribal_council_key) AND XV.contestant_voted_for_id = V.contestant_voted_for_id INNER JOIN season_rosters ON season_rosters.season_id = V.season_id WHERE (V.is_jury_vote = (''false'') AND V.contestant_id <> (112) AND XV.tribal_council_key IS NOT NULL) ) AS __sq WHERE __joined_row_num = 1 ORDER BY __order'
The issue is with this part:
SELECT DISTINCT *, __order
Try adding the required columns to your GROUP BY.
Contestant.joins('INNER JOIN votes AS V ON V.contestant_id = contestants.id AND V.season_id = '+ season_number.to_s)
.joins('LEFT OUTER JOIN votes AS XV ON (XV.contestant_id = '+self.id.to_s+') AND (XV.tribal_council_key = V.tribal_council_key) AND XV.contestant_voted_for_id = V.contestant_voted_for_id')
.joins('INNER JOIN season_rosters ON season_rosters.season_id = V.season_id')
.where('V.is_jury_vote = (?) AND V.contestant_id <> (?) AND XV.tribal_council_key IS NOT NULL', :false, self.id)
.order('season_rosters.finished')
.group('col1,col2,__order')
Also in your SQL error, order by is on a different column while in your code, it is on season_rosters.finished.

PostgreSQL - must appear in the GROUP BY clause or be used in an aggregate function

This query works locally, but doesn't work on Heroku for some reason.
2014-06-26T00:45:11.334388+00:00 app[web.1]: PG::GroupingError: ERROR: column
"conversations.updated_at" must appear in the GROUP BY clause or be used in an
aggregate
function
This is the SQL.
Conversation.joins("INNER JOIN notifications ON
notifications.conversation_id = conversations.id AND
notifications.type IN ('Message') INNER JOIN receipts
ON receipts.notification_id = notifications.id WHERE
notifications.type = 'Message' AND (receipts.receiver_id = #{a.id}
AND receipts.receiver_type = 'Profile') AND conversations.id
IN #{active_conversations_ids} ORDER BY conversations.updated_at DESC")
I tried doing distinct and it did not work, and I tried group_by('conversation_id')
the to_sql
SELECT "conversations".* FROM "conversations" INNER JOIN
notifications ON notifications.conversation_id = conversations.id
AND notifications.type IN ('Message') INNER JOIN receipts
ON receipts.notification_id = notifications.id WHERE
notifications.type = 'Message' AND (receipts.receiver_id = 104
AND receipts.receiver_type = 'Profile') AND conversations.id
IN (…) ORDER BY conversations.updated_at DESC"
You can see the SQL in your server window, if you are running the app in local machine.
It is a good practice to keep the joins where they belong to.
Conversation.where('id' => active_conversations_ids).joins(
"INNER JOIN notifications ON notifications.conversation_id = conversations.id
AND notifications.type IN ('Message')
INNER JOIN receipts ON receipts.notification_id = notifications.id
AND (receipts.receiver_id = #{a.id} AND receipts.receiver_type = 'Profile')"
).order('updated_at')
And for the better readability, you can use notifications with symbols.
Conversation
.find_all_by_id(active_conversations_ids)
.joins(:notifications, :receipts).where({
notifications: { type: 'Message' },
receipts: { receiver_id: a.id, receiver_type: 'Profile'}
})
.order('updated_at')
My final suggestion would be to google for named scopes.
http://zachholman.com/2010/01/simplifying-rails-controllers-with-named_scopes/
It was as simple as adding
GROUP BY conversations.id
which ends up looking like
Conversation.joins("INNER JOIN notifications ON
notifications.conversation_id = conversations.id AND
notifications.type IN ('Message') INNER JOIN receipts ON
receipts.notification_id = notifications.id WHERE
notifications.type = 'Message' AND (receipts.receiver_id = #{self.id}) AND
conversations.id IN #{active_conversation_ids} GROUP BY
conversations.id ORDER BY conversations.updated_at DESC")
.select('conversations.id, conversations.updated_at')

Resources