Why I cannot use both ORDER BY and DISTINCT * in SQL? - ruby-on-rails

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.

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 do I change this query to return an association instead of an array in Rails and Postgres?

Suppose I have this query:
t.pool_tournament_matches
.where(status: "unstarted")
.joins("INNER JOIN pool_tournament_match_users ON pool_tournament_match_users.pool_tournament_match_id = pool_tournament_matches.id")
.joins("INNER JOIN users ON users.id = pool_tournament_match_users.user_id")
.group("pool_tournament_matches.id")
.select("pool_tournament_matches.*, COUNT(users.id) AS user_count")
.select {|m| m.user_count == 2}
The result does return the right matches but it's an array which does not work well with RABL template engine. What converts the query to an array is
.select {|m| m.user_count == 2}` I think.
How would I do this using SQL so that it will return an association and I can chain it further if needed.
I've tried:
t.pool_tournament_matches
.where(status: "unstarted")
.joins("INNER JOIN pool_tournament_match_users ON pool_tournament_match_users.pool_tournament_match_id = pool_tournament_matches.id")
.joins("INNER JOIN users ON users.id = pool_tournament_match_users.user_id")
.group("pool_tournament_matches.id")
.select("pool_tournament_matches.*, COUNT(users.id) = 2)
but it does not work.
t.pool_tournament_matches
.where(status: "unstarted")
.joins("INNER JOIN pool_tournament_match_users ON pool_tournament_match_users.pool_tournament_match_id = pool_tournament_matches.id")
.joins("INNER JOIN users ON users.id = pool_tournament_match_users.user_id")
.group("pool_tournament_matches.id")
.having("COUNT(users.id) = 2")
.select("pool_tournament_matches.*, COUNT(users.id) AS user_count")

Multi-dimension instance loop

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.

How to implement AND clause in Rails Active Record?

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')

RAILS SQL Subquery optimization

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.

Resources