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)
I am trying to use a custom sql query to display different attributes for a product i.e. Size and Price. The query I have when running in console displays as it should
SELECT products.id, products.name, variant_properties.description, LEFT(variant_properties.description,1) as short_desc, variants.price FROM products
INNER JOIN product_properties ON product_properties.product_id = products.id
INNER JOIN variant_properties on product_properties.property_id = variant_properties.property_id AND variant_properties."primary" = true
INNER JOIN properties ON properties.id = product_properties.property_id AND properties.id = variant_properties.property_id AND properties.display_name = 'Size'
INNER JOIN variants on variants.product_id = products.id AND variants.id = variant_properties.variant_id
In my HAML template I have done the following
- #products.each_with_index do |product, i|
.product-list.grid-block
.small-8.grid-content.text-center
%h4= product.name.titlecase
- #sizes.each do |size|
= link_to size.short_desc, product, class: 'hollow button tiny'
%small= size.price
and in the controller
products = Product.active
# products = Product.active.includes(:variants)
product_types = nil
if params[:product_type_id].present? && product_type = ProductType.find_by_id(params[:product_type_id])
product_types = product_type.self_and_descendants.map(&:id)
end
if product_types
#products = products.where(product_type_id: product_types)
else
#products = products
end
#sizes = Product.find_by_sql("SELECT products.id, LEFT(variant_properties.description,1) as short_desc, variants.price FROM products
INNER JOIN product_properties ON product_properties.product_id = products.id
INNER JOIN variant_properties on product_properties.property_id = variant_properties.property_id
INNER JOIN properties ON properties.id = product_properties.property_id AND properties.id = variant_properties.property_id AND properties.display_name = 'Size'
INNER JOIN variants on variants.product_id = products.id AND variants.id = variant_properties.variant_id")
Ideally I am trying to get it to look something like below, though I am having issues achieving this
The first thing is notice is that Model.find_by_sql will return a list of models and nothing more no matter what you select in your sql query.
So the solution I suggest is trying to convert to ActiveRecord::Relation like this:
Product.joins(:product_properties)
.joins('INNER JOIN variant_properties on product_properties.property_id = variant_properties.property_id')
.joins('INNER JOIN properties ON properties.id = product_properties.property_id AND properties.id = variant_properties.property_id AND properties.display_name = \'Size\'')
.joins('INNER JOIN variants on variants.product_id = products.id AND variants.id = variant_properties.variant_id')
.pluck('products.id', 'LEFT(variant_properties.description,1)', 'variants.price')
I haven't tried yet but I think it could produce an array of arrays contains the value you need.
I have the following query:
select source_names.id as source_id, source_names.name as source_name, source_types.id as source_type_id,source_types.name type_name,event_sources.id
from source_names
left join source_types
on source_type_id = source_types.id
left join event_sources
on source_names.id = event_sources.source_id and event_id = 1
result = SourceName.joins('
LEFT JOIN source_types ON source_type_id = source_types.id
LEFT JOIN event_sources ON source_names.id = event_sources.source_id
').where('event_id =?',event_id).select('source_names.id as source_id, source_names.name as source_name, source_types.id as source_type_id,source_types.name type_name,event_sources.id').all
yields:
select source_names.id as source_id, source_names.name as source_name, source_types.id as source_type_id,source_types.name type_name,event_sources.id
from source_names
left join source_types
on source_type_id = source_types.id
left join event_sources
on source_names.id = event_sources.source_id WHERE event_id = 1
But I want and event_id = 1
How'd I accomplish that?
If you don't want a WHERE, don't use .where
Just add it to your joins:
SourceName.joins('
LEFT JOIN source_types ON source_type_id = source_types.id
LEFT JOIN event_sources ON source_names.id = event_sources.source_id and event_sources.event_id = 1')
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.
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.