I have a Laravel API in which I want to provide a list of hashtags with the count of posts and comments in which a hashtag is used and order the list by the total count of posts and comments.
Here are my tables: posts, comments, hashtags, hashtag_post, comment_hashtag. Also, proper relationship methods are defined in my models.
After reading the Laravel's documentation and searching the web, I wrote this code:
Hashtag::withCount(['posts', 'comments'])
->orderByRaw('posts_count + comments_count desc')
->paginate(15);
If I use orderBy with only posts_count or comments_count alone, the query works and I get a list, but with the above code, I get this error:
Illuminate/Database/QueryException with message 'SQLSTATE[42703]: Undefined column: 7 ERROR: column "posts_count" does not exist ...
I've used tinker to see if those columns are defined properly on the SQL query, and here's what I've got:
select "hashtags".*, (select count(*) from "posts" inner join "hashtag_post" on "posts"."id" = "hashtag_post"."post_id" where "hashtags"."id" = "hashtag_post"."hashtag_id" and "posts"."deleted_at" is null) as "posts_count", (select count(*) from "comments" inner join "comment_hashtag" on "comments"."id" = "comment_hashtag"."comment_id" where "hashtags"."id" = "comment_hashtag"."hashtag_id" and "comments"."deleted_at" is null) as "comments_count" from "hashtags" where "hashtags"."deleted_at" is null order by posts_count + comments_count desc
I've searched the web about the error, but I found nothing useful.
Edit 1: Based on #Ersoy comment I've changed my query as follow:
Hashtag::selectRaw('Count(posts.id) + Count(comments.id) as total_count')
->withCount(['posts', 'comments'])
->groupBy('hashtags.id')
->orderByRaw('(Count(posts.id) + Count(comments.id)) desc')
->paginate(15);
And as I expected, it didn't work. It says:
Illuminate/Database/QueryException with message 'SQLSTATE[42P01]: Undefined table: 7 ERROR: missing FROM-clause entry for table "posts"
The thing is, likes and comments in the other question, as I understand from the query, are 1-N relationships while my relationships are N-M. Also, I think using the Count function like that won't work without a join. Correct me if I'm wrong.
Edit 2: One of my friends gave me this SQL query which gives the result I want:
select
h.*,
count(distinct hp.post_id) as posts_count,
count(distinct ch.comment_id) as comments_count
from
hashtags as h
left join hashtag_post as hp on
h.id = hp.hashtag_id
left join posts p on
hp.post_id = p.id
left join comment_hashtag ch on
h.id = ch.hashtag_id
left join comments c on
ch.comment_id = c.id
where
h.deleted_at is null
and p.deleted_at is null
and c.deleted_at is null
group by
h.id
order by
( count(distinct hp.post_id) + count(distinct ch.comment_id) ) desc;
But I don't know how to convert it properly to an Eloquent query.
DB::table('hashtags as h')
->leftJoin('hashtag_post as hp', 'h.id', '=' ,'hp.hashtag_id')
->leftJoin('posts as p', 'hp.post_id', '=' ,'p.id')
->leftJoin('comment_hashtag as ch', 'h.id', '=', 'ch.hashtag_id')
->leftJoin('comments as c', 'ch.comment_id', '=' ,'c.id')
->whereNull('h.deleted_at')
->whereNull('p.deleted_at')
->whereNull('c.deleted_at')
->selectRaw("*, count(distinct hp.post_id) + count(distinct ch.comment_id) as total_count")
->groupBy('h.id')
// try
->orderByRaw('count(distinct hp.post_id) + count(distinct ch.comment_id) desc')
// or
->orderByRaw('total_count desc')
->get();
Both the orderByRaw statement are similar, you can try any of the above. Hopefully, it should work.
The following code works perfectly if the Laravel version is 6+:
Hashtag::withCount(['posts', 'comments'])
->orderByRaw('posts_count + comments_count desc')
->paginate(15);
However, I'm currently using Laravel 5.7 and I can't upgrade it now. Thus I should use an alternative to get the desired result:
Hashtag::leftJoin('hashtag_post as hp', 'hashtags.id', '=', 'hp.hashtag_id')
->leftJoin('posts as p', 'hp.post_id', '=', 'p.id')
->leftJoin('comment_hashtag as ch', 'hashtags.id', '=', 'ch.hashtag_id')
->leftJoin('comments as c', 'ch.comment_id', '=', 'c.id')
->whereNull('p.deleted_at')
->whereNull('c.deleted_at')
->selectRaw('hashtags.*, count(distinct hp.post_id) as posts_count, count(distinct ch.comment_id) as comments_count')
->groupBy('hashtags.id')
->orderByRaw('count(distinct hp.post_id) + count(distinct ch.comment_id) desc')
->paginate(15)
Thanks to #tarun-jain
Related
I need to add a column with the content of this query :
SELECT COUNT(*) FROM account_subscriptiongroups WHERE account_subscriptiongroups.active = true AND account_subscriptiongroups.user_id = account_user.id
to this query :
SELECT
account_user.id as user_id, account_user.email, account_user.first_name, account_user.last_name, account_user.phone,
account_subscriptiongroup.id as sub_group_id,
account_adminaction.description,
account_adminaction.id as admin_action_id,
account_adminaction.created_on as subscription_ended_on
FROM
account_adminaction
LEFT JOIN
account_user ON account_user.id = account_adminaction.user_id
LEFT JOIN
account_subscriptiongroup ON account_adminaction.sub_group_id = account_subscriptiongroup.id
WHERE
account_adminaction.created_on >= '2021-04-07' AND account_adminaction.created_on <= '2021-04-13' AND
((account_adminaction.description LIKE 'Arrêt de l''abonnement%') OR (account_adminaction.description LIKE 'L''utilisateur a arrêté%'))
ORDER BY
subscription_ended_on
I tried adding a LEFT JOIN like that:
LEFT JOIN
account_subscriptiongroup all_sg ON account_user.id = account_subscriptiongroup.user_id
with this line in my WHERE statement :
AND all_sg.active = true
and this line in my SELECT :
COUNT(all_sg.id)
but I get an error :
ERROR: column "account_user.id" must appear in the GROUP BY clause or be used in an aggregate function
LINE 2: account_user.id as user_id, account_user.email, account_us...
^
I don't understand how I could perform this action properly
To count something, you need to specify a group where that count applies.
So every column that you select (and is not used in an aggregate function, like COUNT or SUM), you need to mention in the GROUP BY clause.
Or to put it the other way around: the non-aggregate columns must apply to all rows that are contained in that particular COUNT.
So between the WHERE and ORDER BY clauses, add a GROUP BY clause:
GROUP BY account_user.id, account_user.email, account_user.first_name, account_user.last_name, account_user.phone,
account_subscriptiongroup.id,
account_adminaction.description,
account_adminaction.id,
account_adminaction.created_on
If, on the other hand, you want a count from a different table, you can add a sub-select:
SELECT
account_user.id as user_id, account_user.email, account_user.first_name, account_user.last_name, account_user.phone,
account_subscriptiongroup.id as sub_group_id,
account_adminaction.description,
account_adminaction.id as admin_action_id,
account_adminaction.created_on as subscription_ended_on,
(SELECT COUNT(*)
FROM account_subscriptiongroups
WHERE account_subscriptiongroups.active = true
AND account_subscriptiongroups.user_id = account_user.id) AS groupcount
FROM
account_adminaction
LEFT JOIN
account_user ON account_user.id = account_adminaction.user_id
You can left join to to a derived table that does the grouping and counting:
SELECT au.id as user_id, au.email, au.first_name, au.last_name, au.phone,
asg.id as sub_group_id,
ad.description,
ad.id as admin_action_id,
ad.created_on as subscription_ended_on,
asgc.num_groups
FROM account_adminaction ad
LEFT JOIN account_user au ON au.id = ad.user_id
LEFT JOIN account_subscriptiongroups asg on ON ad.sub_group_id = asg.id
LEFT JOIN (
SELECT user_id, count(*) as num_groups
FROM account_subscriptiongroups ag
WHERE ag.active
GROUP by user_id
) asgc on asgc.user_id = au.id
WHERE ad.created_on >= '2021-04-07'
AND ad.created_on <= '2021-04-13'
AND ((ad.description LIKE 'Arrêt de l''abonnement%') OR (ad.description LIKE 'L''utilisateur a arrêté%'))
ORDER BY subscription_ended_on
It's not entirely clear to me, what you are trying to count, but another option (most probably slower) could be to use a window function combined with a filter clause:
count(*) filter (where asg.active) over (partition by asg.user_id) as num_groups
EDIT: my answer is the same as submitted by a_horse_with_no_name
Two answers, a literal one just solving the problem you posed, and then another one questioning whether what you asked for is really what you want.
Simple answer: modify your desired query to add user_id to the Select and remove user_id from the Where clause. Now you have a table that can be left-joined to the rest of your larger query.
...
Left Join (Select user_id, count(*) as total_count
From account_subscriptiongroup
Where account_subscriptiongroups.active = true
Group By user_id) Z
On Z.user_id=account_user.id
I question whether this count is what you really want here. This counts every account_subscriptiongroup entry for all time but only the active ones. Your larger query brings back inactive as well as active records, so if your goal is to create a denominator for a percentage, you are mixing 'apples and oranges' here.
If you decide you want a total by user of the records in your query instead, then you can add one more element to your larger query without adding any more tables. Use a windowing function like this:
Select ..., Sum(Case When account_subscriptiongroup.active Then 1 else 0 End) Over (Group By account_user.id) as total count
This just counts the records within the date range and having the desired actions.
I have this query:
$query = $this->createQueryBuilder('l')
->select('l')
->leftJoin('l.processedLeads', 'pl')
->where('pl.company <> :company')
->andWhere('pl.company IS NULL')
->setParameters(array('company' => $company))
->getQuery();
But i need it formed like the following:
SELECT * FROM leads WHERE NOT IN
( SELECT * FROM processed_leads WHERE lead = :lead AND company = :company)
Can i do a sub-select in a join where 2 parameters of the join (lead_id and company) do not exist?
As in, only select the leads that do not exist in processedLeads with the specific company?
I'm not familiar with the QueryBuilder, but I would recommend something like this in DQL:
$query = $em->createQuery('SELECT l FROM Project\model\leads l WHERE l NOT IN
(SELECT p.lead FROM Project\model\processed_leads p WHERE p.lead = ? AND company = ?)')
->setParameters(array($lead, $company))
->getResult;
Hopefully, this helps you. I would use the NOT INstatement. In case the sub query returns a null value, there is just no lead in the processed_leads for this company and you'll get all leads - which should be ok in this case.
I was thinking it might be an issue with the joins? I've tried Group by to no avail...
Any advice would be appreicated! I've placed the query below:
*Sorry for the lack of details- Med_Prof_Record_No is the unique value, and I know the code is messy- but this is what was here when I got here ;-) Also this is a sql 2000 box, so new syntax won't work... I've cleaned up the joins abit but don't want to stray too far from the original queries-
SELECT DISTINCT
P.Last_name + ', ' + P.First_name AS Full_name,
P.Degree,
F.Med_Prof_Record_No,
F.Current_status,
F.Status_category,
F.Department_name,
F.Section,
F.SPHAffiliatedPhysiciansSurgeons AS Affiliated,
S.Board_Name,
S.Specialty_Name,
O.Office_name,
O.Address_1,
O.Address_2,
O.City,
O.State,
O.Zip_Code,
O.Phone_number_1,
O.Fax_number
FROM
Med_Prof P, Med_Prof_Facilities F, Med_Prof_Specialties S, Med_Prof_Offices O
WHERE
(F.Med_Prof_Record_No = P.Med_Prof_Record_No) AND
(F.Med_Prof_Record_No = S.Med_Prof_Record_No) AND
(F.Med_Prof_Record_No = O.Med_Prof_Record_No) AND
<cfif URL.LastName is NOT "">(P.Last_name LIKE '#URL.LastName#%') AND</cfif>
<cfif URL.Specialty is NOT "">(F.Section = '#URL.Specialty#') AND</cfif>
<cfif URL.Group is NOT "">(O.Office_name LIKE '#URL.Group#%') AND</cfif>
(F.Status_category = 'active')
ORDER by Full_name
Here is a stab. This makes assumptions about how you want to decide which office etc. to show (in this case it is as good as arbitrary), that P.Med_Prof_Record_No is unique and only represents one person (at first I thought Last_name + First_name is unique, but that seems a very dangerous assumption), and also that you are using SQL Server 2005 or better. Finally, please use properly qualified object names and please, please, please stop using lazy implicit joins of the FROM foo, bar, blat, splunge variety.
;WITH x AS
(
SELECT
P.Last_name + ', ' + P.First_name AS Full_name,
P.Degree,
F.Med_Prof_Record_No,
-- other columns from F,
S.Board_Name,
S.Specialty_Name,
O.Office_name,
-- other columns from O,
rn = ROW_NUMBER() OVER (PARTITION BY P.Med_Prof_Record_No
ORDER BY F.Current_status, S.Board_name, O.Office_name)
FROM
dbo.Med_Prof AS P
INNER JOIN
dbo.Med_Prof_Facilities AS F
ON P.Med_Prof_Record_No = F.Med_Prof_Record_No
INNER JOIN
dbo.Med_Prof_Specialties AS S
ON F.Med_Prof_Record_No = S.Med_Prof_Record_No
INNER JOIN
dbo.Med_Prof_Offices AS O
ON F.Med_Prof_Record_No = O.Med_Prof_Record_No
WHERE
<cfif ... AND</cfif>
-- other <cfif> clauses
(F.Status_category = 'active')
)
SELECT * FROM x WHERE rn = 1
ORDER BY Full_name;
I am facing an issue while working with Zend Framework. Basically, I am trying to add an AND operator in a sub query using Left join. Please see the following query that my code is producing:
SELECT `d`.*, count(status) FROM `deal` AS `d` LEFT JOIN `payments` ON `payments`.deal_id = `d`.deal_id GROUP BY `d`.`deal_id` ORDER BY `created_date` desc
Code is:
$select = $this->_db->select()
->from(array('d'=>'deal'))
->joinLeftUsing('payments', 'deal_id', array('count(status)'))
->order("created_date $sortOrder")
->group("d.deal_id");
However, I want to add one more condition in my sub query i.e. Status = 1, please see the following output that I am willing to get.
SELECT `d`.*, count(status) FROM `deal` AS `d` LEFT JOIN `payments` ON `payments`.deal_id = `d`.deal_id AND status = 1 GROUP BY `d`.`deal_id` ORDER BY `created_date` desc
Let me know if someone have an idea about how I can achieve the same.
Thanks,
Gagz
I would recommend using joinLeft instead of joinLeftUsing
$select = $this->_db->select()
->from(array('d'=>'deal'))
->joinLeft('payments', 'payments.deal_id = d.deal_id and status = 1',
array('count(status)'))
->order("created_date $sortOrder")
->group("d.deal_id");
gives me
SELECT `d`.*, count(status) FROM `deal` AS `d`
LEFT JOIN `payments` ON payments.deal_id = d.deal_id and status = 1
GROUP BY `d`.`deal_id` ORDER BY `created_date ` ASC
We can use expression in Zend 3:
$join = new \Zend\Db\Sql\Expression('contractor_jobs.contractor_id = contractor_info.contractor_id AND
contractor_jobs.job_trade_id = '.$variableName.' ');
$select->join(
'contractor_jobs',
$join,
array('job_trade_id'),
$select::JOIN_LEFT
);
It works for me. You can also check as per your conditions. Thanks for asking this question.
I'm using Informix version 11.50.FC6 via iSql
I'm giving the result of a CASE block a virtual name, att_hrs
SELECT c.id,
CASE WHEN ( c.prog = 'UNDG'
AND (c.grd IN (SELECT DISTINCT grd FROM grd_table WHERE att_fctr = 1) OR (c.grd IN ('TR','W','LAB','WC')))
AND c.grd NOT IN ('WM')
AND c.stat NOT IN ('X','D'))
THEN CAST(SUM(c.hrs) AS CHAR(4))
ELSE 'ELSED (att)'
END att_hrs
FROM cw_rec c
WHERE c.id IN (SELECT DISTINCT id FROM stu_ids)
GROUP BY c.id
INTO TEMP cheese
WITH NO LOG;
This gives me an error:
294: The column (att_hrs) must be in the GROUP BY list.
Trying to fix the error as suggested:
SELECT c.id,
CASE WHEN ( c.prog = 'UNDG'
AND (c.grd IN (SELECT DISTINCT grd FROM grd_table WHERE att_fctr = 1) OR (c.grd IN ('TR','W','LAB','WC')))
AND c.grd NOT IN ('WM')
AND c.stat NOT IN ('X','D'))
THEN CAST(SUM(c.hrs) AS CHAR(4))
ELSE 'ELSED (att)'
END att_hrs
FROM cw_rec c
WHERE c.id IN (SELECT DISTINCT id FROM stu_ids)
GROUP BY c.id,
att_hrs
INTO TEMP cheese
WITH NO LOG;
Then gives me this error:
217: Column (att_hrs) not found in any table in the query (or SLV is undefined).
They kind of found att_hrs pretty easily when it wasn't in the GROUP BY party, but now all of a sudden, att_hrs is lost in the sauce...
Can you get around this?
What are the real errors &| solutions to what is going on here and what I need to do to fix it?
EDIT
I tried RET's solution to GROUP BY 1,2,3... and got the following error:
321: Cannot group by aggregate column.
You can't use your labels for derived columns in your group by list. You need to list the grouping columns by their ordinal position under these circumstances.
Change your SQL to read GROUP BY 1, 2
Here's the relevant entry from the manual. Figure 269, specifically.
UPDATE: I didn't examine the CASE statement closely enough - I don't think what you're attempting is possible, because each row can differ in whether it's treated as a grouping or aggregate column.
Perhaps you need to try something like this:
SELECT c.id,
NVL(SUM(CASE
WHEN ( c.prog = 'UNDG'
AND (c.grd IN (SELECT DISTINCT grd FROM grd_table WHERE att_fctr = 1) OR (c.grd IN ('TR','W','LAB','WC')))
AND c.grd NOT IN ('WM')
AND c.stat NOT IN ('X','D'))
THEN c.hrs
ELSE NULL)::CHAR(4)
END), 'ELSED (att)') AS att_hrs
FROM cw_rec c
WHERE c.id IN (SELECT DISTINCT id FROM stu_ids)
GROUP BY c.id
INTO TEMP cheese
WITH NO LOG;
That's untested, but hopefully gives you the idea - there's always aggregation going on, and the result is cast into text.
Although the following link is a different kind of problem, it may shed some light into solving your problem with the GROUP BY clause. Error compiling ACE report with multiple SELECT INTO statements
The problem is with the columns c.prog, c.grd, c.stat, which appear in the SELECT list without being either included in GROUP BY or aggregated.
You seem to be calculating att_hrs based on the values of some columns, but the question is: what if the columns' values match the condition in the CASE expression in some rows and do not match in some others for the same id? Is that possible? If yes, what should become the value of att_hrs for that id?
There are two answers I can expect (I'm not pretending they are the only two possible answers, maybe I've missed something):
It should be 'ELSED (att)' if all the rows for that id do not match the CASE condition, otherwise it should be the sum for the rows that match the condition.
It should be 'ELSED (att)' if some (one or more) rows for that id do not match the CASE condition. The sum should only be calculated if all the rows match the condition.
So, to summarise, presently I've only answered the question about the actual problem with your query. I'll be happy to extend my answer with a solution after you elaborate on my questions.
I took everyone's advice that it isn't possible and rewrote it using UNION blocks.
Table & field name(s) may have varied, but here's the idea:
SELECT s.id,
SUM(c.hrs) hrs,
'ATT' type
FROM expected_contacts s,
OUTER stu_crs c
WHERE s.id = c.id
AND c.prog = 'UNDG'
AND c.grd NOT IN ('WM')
AND c.stat NOT IN ('X','D')
AND (c.grd IN (SELECT DISTINCT grd
FROM grd_table
WHERE att_fctr = 1)
OR (c.grd IN ('TR','W','LAB','WC')))
AND c.crs_no <> 'PARM101'
GROUP BY s.id
UNION
SELECT s.id,
SUM(c.hrs) hrs,
'EARN' type
FROM expected_contacts s,
OUTER stu_crs c
WHERE s.id = c.id
AND c.prog = 'UNDG'
AND (c.grd <= 'DI' or c.grd like 'S%' or c.grd IN ('P','LAB','TR'))
AND c.stat NOT IN ('D','W','X')
GROUP BY s.id
UNION
SELECT s.id,
SUM(c.hrs) hrs,
'DEV' type
FROM expected_contacts s,
OUTER stu_crs c
WHERE s.id = c.id
AND c.prog = 'UNDG'
AND ( c.crs_no LIKE 'ENGL0%'
OR c.crs_no LIKE 'MATH0%'
OR c.crs_no LIKE 'ENGSL0%'
OR c.crs_no LIKE 'ESOL0%')
AND c.stat IN ('C','R','W')
AND c.grd <> 'IP'
GROUP BY s.id
INTO TEMP stu_acad
WITH NO LOG;