Complex ARel query - sql

I've got a complicated query that I can't wrap my head around (using either sql or ActiveRecord) Here are my models:
class Contact
has_many :profile_answers
end
class ProfileAnswer
belongs_to :contact
belongs_to :profile_question
end
class ProfileQuestion
has_many :profile_answers
end
I'm trying to find the number of ProfileAnswers for two contacts that have the same value for a particular ProfileQuestion. In other words:
Get the total number of profile answers that two contacts have answered with the same value for a particular profile_question
I don't want to make multiple queries and filter as I know this is possible with Sql only, i just don't know how to do it
I had considered a self join of profile_answers on profile_question_id then filtering by value being equal, but i still can't wrap my head around that. Any help is greatly appreciated.

I think this will do:
SELECT COUNT(DISTINCT profile_question_id)
FROM
( SELECT profile_question_id
FROM ProfileAnswer an
JOIN ProfileQuestion qu
ON qu.id = an.profile_question_id
WHERE contact_id IN ( id1, id2 )
GROUP BY profile_question_id
, value
HAVING COUNT(*) = 2
) AS grp
And the JOIN seems not be used. So, if ProfileAnswer.profile_question_id is NOT NULL, this will suffice:
SELECT COUNT(*)
FROM
( SELECT profile_question_id
FROM ProfileAnswer
WHERE contact_id IN ( id1, id2 )
GROUP BY profile_question_id
, value
HAVING COUNT(*) = 2
) AS grp
EDITED for two specific contacts (with ids id1 and id2).
Added the WHERE and changed the COUNT (DINSTINCT ) to COUNT(*).
Perhaps this version with JOIN can be more easily adapted to ActiveRecord.
Using JOIN
SELECT COUNT(*)
FROM ProfileAnswer a
JOIN ProfileAnswer b
ON a.profile_question_id = b.profile_question_id
AND a.value = b.value
WHERE a.contact_id = id1
AND b.contact_id = id2

Here's how I ended up doing it, thanks again #ypercube:
class ProfileAnswer < ActiveRecord::Base
def self.for_contacts(*contacts)
where :contact_id => contacts.collect(&:id)
end
def self.common_for_contacts(*contacts)
select(:profile_question_id).for_contacts(*contacts).group(:profile_question_id, :value).having("count(*) = #{contacts.length}")
end
def self.common_count_for_contacts(*contacts)
find_by_sql("select count(*) as answer_count from (#{common_for_contacts(*contacts).to_sql})").first.answer_count
end
end
# Usage
ProfileAnswer.common_count_for_contacts(contact1, contact2[, contact3...])
Still had to use a find_by_sql in the end for the nested select... not sure if there's any way around that ??
Also annoying that find_by_sql returns an array, so I had to use .first which then gives me the object that has my answer_count property on it.

Related

mysql select from table if row exists otherwise select from a different table

I have two tables, and I want to select from tablea if the row exists but if none does exist then I want to select a row from tableb (if it exists in that) otherwise if it exists in none it returns null.
This is what I have so far:
SELECT CASE
WHEN (SELECT COUNT(*) FROM users WHERE users.domainid = :domainid AND users.user = :user LIMIT :limit) > 0
THEN (SELECT id,receiving,greylistingdisable FROM users WHERE users.domainid = :domainid AND users.user = :user LIMIT :limit)
ELSE (SELECT id,blockexpires,recordexpires FROM auto_deny WHERE auto_deny.domainid = :domainid AND auto_deny.user = :user AND auto_deny.blockexpires >= :blockexpires LIMIT :limit)
END
I have tried so may different combination of things but I can never get it to work. This one seems the closest I have gotten tho but it returns an error:
"ER_OPERAND_COLUMNS: Operand should contain 1 column(s)"
Assuming the queries themselves are working properly and you just the conditional selection to work as you describe I would try using exists and not exists subqueries on each side and connecting them with a union. One should return rows and the other not, as appropriate, achieving what you want in the end.
( select id,
receiving,
greylistingdisable,
'x' as src -- to distinguish rows as being sourced from x or y
from users
where users.domainid = :domainid
and exists ( select 1
from users
where users.domainid = :domainid
and users.user = :user )
and users.user = :user
limit :limit
)
union all
( select id,
blockexpires,
recordexpires,
'y'
from auto_deny
where auto_deny.domainid = :domainid
and auto_deny.user = :user
and not exists ( select 1
from users
where users.domainid = :domainid
and users.user = :user )
and auto_deny.blockexpires >= :blockexpires
limit :limit
)
I think you want a prioritization query using union all:
(SELECT id, receiving, greylistingdisable, 1 as isuser
FROM users u
WHERE u.domainid = :domainid AND u.user = :user
LIMIT :limit
) UNION ALL
(SELECT id, blockexpires, recordexpires, 0 as isuser
FROM auto_deny ad
WHERE ad.domainid = :domainid AND ad.user = :user AND
ad.blockexpires >= :blockexpires AND
NOT EXISTS (SELECT 1 FROM users u WHERE u.domainid = :domainid and u.user = :user)
LIMIT :limit
)
Note: The use of limit without an order by is considered bad practice. You should usually include an order by to guarantee that the query returns the same results each time it is called.

activerecord sql difficult query

I need to make a query with two options: first - select DISTINCT ON, secondly - order by (and order by other fields). BTW, having by don't work
At one sql forum I find a solution
WITH d AS (
SELECT DISTINCT ON ({Dlist}) {slist}
FROM {flist}
....
)
SELECT * FROM d ORDER BY {order fields}
So, how I can make this via ActiveRecord method and get back ActiveRecord::Relation
My full query seems something like that:
WITH d AS (
SELECT DISTINCT ON(item_info_id, volume) items.item_info_id, items.volume, items.*
FROM "items" INNER JOIN "item_info" ON "item_info"."id" = "items"."item_info_id" WHERE "items"."type" IN ('Product')
AND "items"."published" = 't'
AND ("items"."item_info_id" IS NOT NULL)
AND ("items"."price" BETWEEN 2 AND 823489)\
)
SELECT * FROM d ORDER_BY 'price'
Below might work for you or give you some hints
class Item < ActiveRecord::Base
def self.what_you_want_to_achieve
item_ids = where("item_info_id IS NOT NULL")
.select(" DISTINCT on(item_info_id, volume) items.item_info_id, items.volume, items.id ")
.map(&:id)
where(:id => item_ids).published.products.price_between(2,823489).order(:price)
end
I assume you know how to define scope e.g. published

Providing Language FallBack In A SQL Select Statement

I have a table that represents an Object. It has many columns but also fields that require language support.
For simplicity let's say I have 3 tables:
MainObjectTable
LanguageDependantField1
LanguageDependantField2.
MainObjectTable has a PK int called ID, and both LanguageDependantTables have a foreign key link back to the MainObjectTable along with a language code and the date they were added.
I've created a stored procedure that accepts the MainObjectTable ID and a Language. It will return a single row containing the most recent items from the language tables. The select statement looks like
SELECT
MainObjectTable.VariousColumns,
LanguageDependantField1.Description,
LanguageDependantField2.SomeOtherText
FROM
MainObjectTable
OUTER APPLY
(SELECT TOP 1 LanguageDependantField1.Description
FROM LanguageDependantField1
WHERE LanguageDependantField1.MainObjectTable_ID = MainObjectTable.ID
AND LanguageDependantField1.Language_ID = #language
ORDER BY
LanguageDependantField1.[Default], LanguageDependantField1.CreatedDate DESC) LanguageDependantField1
OUTER APPLY
(SELECT TOP 1 LanguageDependantField2.SomeOtherText
FROM LanguageDependantField2
WHERE LanguageDependantField2.MainObjectTable_ID = MainObjectTable.ID
AND LanguageDependantField2.Language_ID = #language
ORDER BY
LanguageDependantField2.[Default] DESC, LanguageDependantField2.CreatedDate DESC) LanguageDependantField2
WHERE
MainObjectTable.ID = #MainObjectTableID
What I want to add is the ability to fallback to a default language if a row isn't found in the specified language. Let's say we use "German" as the selected language. Is it possible to return an English row from LanguageDependantField1 if the German does not exist presuming we have #fallbackLanguageID
Also am I right to use OUTER APPLY in this scenario or should I be using JOIN?
Many thanks for your help.
Try this:
SELECT MainObjectTable.VariousColumns,
COALESCE(PrefLang.Description,Fallback.Description,'Not Found Desc')
as Description,
COALESCE(PrefLang.SomeOtherText,FallBack.SomeOtherText,'Not found')
as SomeOtherText
FROM MainObjectTable
LEFT JOIN
(SELECT TOP 1 pl.Description,pl.SomeOtherText
FROM LanguageDependantField1 pl
WHERE pl.MainObjectTable_ID = MainObjectTable.ID
AND pl.Language_ID = #language
ORDER BY
pl.[Default], pl.CreatedDate DESC)
PrefLang ON 1=1
LEFT JOIN
(SELECT TOP 1 fb.Description,fb.SomeOtherText
FROM LanguageDependantField1 fb
WHERE fb.MainObjectTable_ID = MainObjectTable.ID
AND fb.Language_ID = #fallbackLanguageID
ORDER BY
fb.[Default], fb.CreatedDate DESC)
Fallback ON 1=1
WHERE
MainObjectTable.ID = #MainObjectTableID
Basically, make two queries, one to the preferred language and one to English (Default). Use the LEFT JOIN, so if the first one isn't found, the second query is used...
I don't have your actual tables, so there might be a syntax error in above, but hope it gives you the concept you want to try...
Yes, the use of Outer Apply is correct if you want to correlate the MainObjectTable table rows to the inner queries. You cannot use Joins with references in the derived table to the outer table. If you wanted to use Joins, you would need to include the joining column(s) and in this case pre-filter the results. Here is what that might look like:
With RankedLanguages As
(
Select LDF1.MainObjectTable_ID, LDF1.Language_ID, LDF1.Description, LDF1.SomeOtherText, ...
, Row_Number() Over ( Partition By LDF1.MainObjectTable_ID, LDF1.Language_ID
Order By LDF1.[Default] Desc, LDF1.CreatedDate Desc ) As Rnk
From LanguageDependantField1 As LDF1
Where LDF1.Language_ID In( #languageId, #defaultLanguageId )
)
Select M.VariousColumns
, Coalesce( SpecificLDF.Description, DefaultLDF.Description ) As Description
, Coalesce( SpecificLDF.SomeOtherText, DefaultLDF.SomeOtherText ) As SomeOtherText
, ...
From MainObjectTable As M
Left Join RankedLanguages As SpecificLDF
On SpecificLDF.MainObjectTable_ID = M.ID
And SpecifcLDF.Language_ID = #languageId
And SpecifcLDF.Rnk = 1
Left Join RankedLanguages As DefaultLDF
On DefaultLDF.MainObjectTable_ID = M.ID
And DefaultLDF.Language_ID = #defaultLanguageId
And DefaultLDF.Rnk = 1
Where M.ID = #MainObjectTableID

How to rewrite this SQL query in Rails 3?

Suppose I have two models, submission has_many submissionstate
and table submissionstates has the following columns:
id | submission_id | state_id | created_at
and the query is
SELECT submission_id, state_id
FROM submissionstates ss
JOIN (
SELECT MAX(created_at) as created_at
FROM submissionstates ss
GROUP BY submission_id
) x
ON ss.created_at = x.created_at
WHERE state_id = 0
like the link Saurabh gave, you end up putting your sql fragments into the rails query methods; something like this
list = SubmissionState.select("submission_id, state_id")
.where(:state_id => 0)
.joins("
JOIN (
SELECT MAX(created_at) as created_at
FROM submissionstates ss
GROUP BY submission_id
) x
ON ss.created_at = x.created_at
")
puts list.length
to be honest at this point you might be better off just using find_by_sql
sql = "
SELECT submission_id, state_id
FROM submissionstates ss
JOIN (
SELECT MAX(created_at) as created_at
FROM submissionstates ss
GROUP BY submission_id
) x
ON ss.created_at = x.created_at
WHERE state_id = ?
AND some_other_value = ?
"
list = SubmissionState.find_by_sql([sql, 0, 'something-else'])
puts list.length
NOTE: once you start using joins or find_by_sql rails acts like it gives you objects back but really they will contain any attributes defined in the select clause and find_by_sql returns all attributes as strings which can be annoying

PostgreSQL - how to query "result IN ALL OF"?

I am new to PostgreSQL and I have a problem with the following query:
WITH relevant_einsatz AS (
SELECT einsatz.fahrzeug,einsatz.mannschaft
FROM einsatz
INNER JOIN bergefahrzeug ON einsatz.fahrzeug = bergefahrzeug.id
),
relevant_mannschaften AS (
SELECT DISTINCT relevant_einsatz.mannschaft
FROM relevant_einsatz
WHERE relevant_einsatz.fahrzeug IN (SELECT id FROM bergefahrzeug)
)
SELECT mannschaft.id,mannschaft.rufname,person.id,person.nachname
FROM mannschaft,person,relevant_mannschaften WHERE mannschaft.leiter = person.id AND relevant_mannschaften.mannschaft=mannschaft.id;
This query is working basically - but in "relevant_mannschaften" I am currently selecting each mannschaft, which has been to an relevant_einsatz with at least 1 bergefahrzeug.
Instead of this, I want to select into "relevant_mannschaften" each mannschaft, which has been to an relevant_einsatz WITH EACH from bergefahrzeug.
Does anybody know how to formulate this change?
The information you provide is rather rudimentary. But tuning into my mentalist skills, going out on a limb, I would guess this untangled version of the query does the job much faster:
SELECT m.id, m.rufname, p.id, p.nachname
FROM person p
JOIN mannschaft m ON m.leiter = p.id
JOIN (
SELECT e.mannschaft
FROM einsatz e
JOIN bergefahrzeug b ON b.id = e.fahrzeug -- may be redundant
GROUP BY e.mannschaft
HAVING count(DISTINCT e.fahrzeug)
= (SELECT count(*) FROM bergefahrzeug)
) e ON e.mannschaft = m.id
Explain:
In the subquery e I count how many DISTINCT mountain-vehicles (bergfahrzeug) have been used by a team (mannschaft) in all their deployments (einsatz): count(DISTINCT e.fahrzeug)
If that number matches the count in table bergfahrzeug: (SELECT count(*) FROM bergefahrzeug) - the team qualifies according to your description.
The rest of the query just fetches details from matching rows in mannschaft and person.
You don't need this line at all, if there are no other vehicles in play than bergfahrzeuge:
JOIN bergefahrzeug b ON b.id = e.fahrzeug
Basically, this is a special application of relational division. A lot more on the topic under this related question:
How to filter SQL results in a has-many-through relation
Do not know how to explain it, but here is an example how I solved this problem, just in case somebody has the some question one day.
WITH dfz AS (
SELECT DISTINCT fahrzeug,mannschaft FROM einsatz WHERE einsatz.fahrzeug IN (SELECT id FROM bergefahrzeug)
), abc AS (
SELECT DISTINCT mannschaft FROM dfz
), einsatzmannschaften AS (
SELECT abc.mannschaft FROM abc WHERE (SELECT sum(dfz.fahrzeug) FROM dfz WHERE dfz.mannschaft = abc.mannschaft) = (SELECT sum(bergefahrzeug.id) FROM bergefahrzeug)
)
SELECT mannschaft.id,mannschaft.rufname,person.id,person.nachname
FROM mannschaft,person,einsatzmannschaften WHERE mannschaft.leiter = person.id AND einsatzmannschaften.mannschaft=mannschaft.id;