Formulating complicated Doctrine2 DQL query - php

Given I have an instance of Event ($event) that has many AttendancePerson, I need to get all of the AttendancePerson objects belonging to $event where the AttendancePerson.person attended more than one event that has a calendar_id matching $event->calendar_id and where the AttendancePerson.event.dateTo ends in the previous year.
The schema minus irrelevant column names:
event_attendance_person
- id
- event_id
- person_id
event
- id
- calendar_id
- dateTo
person
- id
event_calendar
- id
The purpose is to find old members of any given event. Any event attendance person who attended an event sharing the same calendar more than once in the previous year is an "old member" of the event.
I read through many relevant questions. None of them helped. Thank you to anyone who can help on this.

For your specific requirement of having persons from event_attendance_person who have attended more than 1 event in past year of same calendar to the calendar of provided event so in plain Mysql query you can join your tables get the count of distinct events per person id i.e COUNT(DISTINCT e.id) and a conditional count for the provided event id lets say i want to get the persons who have attended event with id 2228 so for this suing case in count you can do so COUNT(CASE WHEN e.id = 2228 THEN 1 END) this will give you the count 1 for the person who attended this event and 0 for persons who misses that event, reason for this conditional count is because i am not using where filter for event id i have overcome this one by using having clause and for the past year a simple where clause is WHERE e.dateTo < DATE_FORMAT(NOW() ,'%Y-01-01 00:00:00')
SELECT p.*,COUNT(DISTINCT e.id) total_events,
COUNT(CASE WHEN e.id = 2228 THEN 1 END) count_event
FROM `event_attendance_person` p
JOIN `event_event` e ON(p.`eventId` = e.id )
JOIN `event_calendar` c ON(e.`calendar` =c.`id`)
WHERE e.`dateTo` < DATE_FORMAT(NOW() ,'%Y-01-01 00:00:00')
GROUP BY p.`personId`
HAVING count_event = 1 AND total_events > 1
ORDER BY total_events DESC
You can test this query on your Mysql server
Now here comes the doctrine part you can replicate above query in DQL as
$DQL="SELECT p,COUNT(DISTINCT e.id) AS total_events,
COUNT(CASE WHEN e.id = 2228 THEN 1 END) AS count_event
FROM NamespaceYourBundle:EventAttendencePerson p
JOIN p.events e
JOIN e.calandar c
WHERE e.dateTo < :dateTo
GROUP BY p.personId
HAVING total_events = 1 AND count_event >1
ORDER BY c DESC
";
For above DQL i assume you have already mapped your relations among your entities like for above query below are the mandatory relations which must exist in your entities
JOIN p.events e Now p is alias for entityNamespaceYourBundle:EventAttendencePerson, EventAttendencePerson entity must point to your Event entity so that the on ON(p.eventId = e.id ) part can be achieved
JOIN e.calandar c Now Event entity must point to your Calendar entity in order to achieve ON(e.calendar =c.id)
And then you can run your DQL as below by using doctrine's paginator class
use Doctrine\ORM\Tools\Pagination\Paginator;
$query = $DM->createQuery($DQL)
->setParameter('dateTo', date("Y-01-01 00:00:00"))
->setFirstResult(0)->setMaxResults(100);
$Persons = new Paginator($query, $fetchJoinCollection = true);

Assuming that (person_id,event_id) is unique in event_attendance_person.
1 get all persons belonging to given event
2 for each person get all their other events having the same calendar id and some end date
3 group by person id
4 filter by persons having more than 1 other event
In SQL (updated column names to match example data)
select p.id
from event_event e
join event_attendance_person eap on eap.eventId = e.id
join person p on eap.personId = p.id
join event_attendance_person eap2 on eap2.personId = p.id
join event_event e2 on e2.id = eap2.eventId
where e.id = 2230
and e2.id <> 2230
and e2.calendar = e.calendar
and e2.dateTo between '2013-01-01' and '2014-12-31'
group by p.id
having count(e2.id) > 1
Using QueryBuilder
$qb->select('p')
->from('MyBundleNameSpace\Entity\Event', 'e')
->innerJoin('e.person','p')
->innerJoin('p.event','e2')
->where('IDENTITY(e) = :event_id')
->andWhere('IDENTITY(e2) != :event_id')
->andWhere('IDENTITY(e2.calendar) = IDENTITY(e.calendar)')
->andWhere('e2.dateTo BETWEEN :start AND :end')
->groupBy('p')
->having('count(e2.id) > 1')
->setParameter('event_id',$event->getId())
->setParameter('start','2012-01-1')
->setParameter('end','2013-12-31');

Why does event_attendance_person need to be it's own object? Wouldn't a many to many relationship with a join table like event_person suffice?
Anycase assuming you have your doctrine entities set up correctly, you'd probably want to split this up into two separate DQL queries, the first query is where you get the list of people who attended the event and then you pass the id list of those people into your second query which does a where person_id IN (person_ids) and WHERE event_id != event.id AND calendar.id = event.calendar.id AND event.dateTo > calculated_date
the DQL for those two separate queries should be easy enough to write.

Related

SQL - Get AVG value from other table

My question sounds really easy, but I'm stuck.
Sample Data:
Listing:
id title State
1 Hotel with nice view Arizona
2 Hotel to stay Arizona
Review:
id listing_id rating mail_approved
1 1 4(stars) 1
2 1 4(stars) 0
3 1 3(stars) 1
4 2 5(stars) 1
So now I get the AVG value of the listings, but I want to get only the value of each listing when the review is mail_approved = 1. But when there is none review or no review with mail_approved = 1 it should give me the listing back just with 0.0 review points. So I would like to get all listing back if they have a review just calculate the AVG of those reviews with mail_approved = 1
How can I do this?
Do I have to rewrite the whole query?
Here is my query:
SELECT
ls.id,
title,
state,
ROUND(AVG(rating),2) avg_rating
FROM listing ls
JOIN review rv
ON ls.id = rv.listing_id
WHERE ls.state = '$get_state'
GROUP BY ls.id,
title,
state
ORDER BY avg_rating DESC
You used join, which is short for inner join. This type of join only gives results if a matching record exists in both tables. Change it to left join (short for left outer join), to also include listings without reviews.
You will need to move the state check and any other check to the join condition too, otherwise those listings without review will be dropped from the result again.
Lastly, you can coalesce the average value to get 0 instead of null for those records.
SELECT
ls.id,
title,
state,
COALESCE(ROUND(AVG(rating),2), 0) avg_rating
FROM listing ls
LEFT JOIN review rv
ON ls.id = rv.listing_id
AND ls.state = '$get_state'
AND ls.mail_approved = 1
GROUP BY ls.id,
title,
state
ORDER BY avg_rating DESC
As a side note, please check prepared statements (for PDO or MySQLi) for the proper way to pass input parameters to your query instead of concatenating with variables like $get_state. Concatting is error prone, and makes you more vulnerable for SQL injection.
Outer join the avarage ratings to the hotels:
select
l.id,
l.title,
l.state,
coalesce(r.avg_rating, 0)
from listing l
left join
(
select
listing_id,
round(avg(rating), 2) as avg_rating
from review
where mail_approved = 1
group by listing_id
) r on r.listing_id = l.id
where l.state = '$get_state'
order by avg_rating desc;

combine two tables and sum mysql

i want to join two tables but i can't do it as i want to sum column and get the result between two dates
first table named : vip_allotment_details
allotment_id qty
2 3
2 5
1 2
1 4
the second table name : vip_allotment
id date_from date_to
1 2017-10-1 2017-10-5
2 2017-10-6 2017-10-10
what i want from the query to get me this result
id qty date_from date_to
1 6 2017-10-1 2017-10-5
2 8 2017-10-6 2017-10-10
i will explain the result :
first allotment_id field is linked with id field in second table , the result i want that we can make sum of qty by the two fields (id , allotment_id ) between the date_from and date_to
and here is my try :
$query1 = "
SELECT SUM(qyt) as total
FROM vip_allotment_details
where allotment_id IN ( SELECT id from vip_allotment where date_from >= '$date_1' AND date_to <= '$date_2')
";
In my query the result gets all the sum of qty field with no filter ..
I hope I have explained my problem well .
thanks/.
I'm not try yet, but maybe you can try like this:
SELECT a.id AS id, SUM(qyt) AS qty, date_from, date_to
FROM vip_allotment AS a
LEFT JOIN vip_allotment_details AS b on b.allotment_id = a.id
WHERE a.date_from >= '{thedatestart}' AND a.date_to <= '{thedateend}'
GROUP BY a.id
ORDER BY a.id ASC;
You need to use JOIN. I see you are using IN keyword, which won't work. There can be many ways to solve your problem. One of them is,
select allotment_id, qty, date_from, date_to
from
(select allotment_id, SUM(qty) as qty
from vip_allotment_details group by allotment_id
) at
INNER JOIN
vip_allotment va
ON va.id= at.allotment_id;
I think the following should do what you ask.
SELECT
va.id,
SUM(vad.qyt) AS total,
va.date_from,
va.date_to
FROM vip_allotment_details AS vad
LEFT JOIN vip_allotment AS va ON va.id = vad.allotment_id
GROUP BY vad.allotment_id
Try below.i think you will get your desired result.
select va.id, temp.qty , va.date_from,va.date_to from vip_allotment as va
inner join (select sum(qty) as qty , allotment_id from vip_allotment_details group by `allotment_id`) as temp
ON temp.allotment_id=va.id
where va.date_from >= '$date_1' AND va.date_to <= '$date_2';
If you want more then one result form an aggregate function (SUM, COUNT, AVG, ...) you'll need to use a GROUP BY. Your query isn't that hard, this should do the trick:
SELECT va.id, va.date_from, va.date_to, SUM(vad.qyt) AS qyt
FROM vip_allotment AS va
LEFT JOIN vip_allotment_details AS vad ON vad.allotment_id = va.id
GROUP BY va.id
And as you can see here, this produces the expected result: http://sqlfiddle.com/#!9/707a8/2
If you now want to start adding extra filters (like filter by date), you can just do so by adding a WHERE to the query. Something like this:
...
LEFT JOIN ...
WHERE va.date_from >= "2017-10-06" and va.date_to <= "2018-10-06"
GROUP BY ...
http://sqlfiddle.com/#!9/707a8/6
On a side note, I noticed you are not binding your params in the php part of your code . Do note that this can pose serious security issues, especially if these dates come directly from the user input. I would suggest looking in to PDO to do the actual querying in PHP.
Try this..change your table name and run the query..hopefully it should give the result as your requirement..if not let me know...
select a.id
, sum(b.qty)
, a.date_from
, a.date_to
from table1 a
, table2 b
where a.id = b.allotment_id
group
by b.allotment_id

Relational division SQL

I programmed a filter which generates a Query to show special employees.
I have table employees and a lot of 1:1, 1:n and n:m relationships e.g. for skills and languages for the employees like this:
Employees
id name
1 John
2 Mike
Skills
id skill experience
1 PHP 3
2 SQL 1
Employee_Skills
eid sid
1 1
1 2
Now I want to filter employees which have at least 2 years experience in using PHP and 1 year SQL.
My filter always generates a correct working Query for every table, relationship and field.
But now my problem is when I would like to filter the same field in a related table multiple times with a and it does not work.
e.g.
John PHP 3
John SQL 1
PHP and SQL are different rows so AND can not work.
I tried using group_concat and find_in_set but I have the problem that I can not filter experience over 2 years with find_in_set and find_in_set does not know PHP is 3 and SQL is 1.
I also tried
WHERE emp.id IN (SELECT eid FROM Employee_Skills WHERE sid IN (SELECT id FROM Skills WHERE skill = 'PHP' AND experience > 1)) AND emp.id IN (SELECT eid FROM Employee_Skills WHERE sid IN (SELECT id FROM Skills WHERE skill = 'SQL' AND experience > 0))
which works for this example, but it only works for n:m and it too complex to know the relationship type.
I have the final Query with
ski.skill = 'PHP' AND ski.experience > 1 AND ski.skill = 'SQL' AND ski.experience > 0
and I would like to manipulate the Query to make it work.
How does a Query have to look like to deal with relational division.
you can try next approach:
select * from Employees
where id in (
select eid
from Employee_Skills as a
inner join
Skills as ski
on (a.sid = ski.id)
where
(ski.skill = 'PHP' AND a.experience > 2) OR
(ski.skill = 'SQL' AND a.experience > 1)
group by eid
having count(*) = 2
)
so, for every filter you will add OR statement, having will filter employees with all filters passed, just pass appropriate number
You could make a kind of pivot query, where you put the experience in each of all of the known skills in columns. This could be a long query, but you could build it dynamically in php, so it would add all skills as columns to the final query, which would look like this:
SELECT e.*, php_exp, sql_exp
FROM Employee e
INNER JOIN (
SELECT es.eid,
SUM(CASE s.skill WHEN 'PHP' THEN s.experience END) php_exp,
SUM(CASE s.skill WHEN 'SQL' THEN s.experience END) sql_exp,
SUM(CASE s.skill WHEN 'JS' THEN s.experience END) js_exp
-- do the same for other skills here --
FROM Employee_Skills es
INNER JOIN Skills s ON es.sid = s.id
GROUP BY es.eid
) pivot ON pivot.eid = e.id
WHERE php_exp > 2 AND sql_exp > 0;
The WHERE clause is then very concise and intuitive: you use the logical operators like in other circumstances.
If the set of skills is rather static, you could even create a view for the sub-query. Then the final SQL is quite concise.
Here is a fiddle.
Alternative
Using the same principle, but using the SUM in the HAVING clause, you can avoid gathering all skill's experiences:
SELECT e.*
FROM Employee e
INNER JOIN (
SELECT es.eid
FROM Employee_Skills es
INNER JOIN Skills s ON es.sid = s.id
GROUP BY es.eid
HAVING SUM(CASE s.skill WHEN 'PHP' THEN s.experience END) > 2
AND SUM(CASE s.skill WHEN 'SQL' THEN s.experience END) > 0
) pivot ON pivot.eid = e.id;
Here is a fiddle.
You can also replace the CASE construct by the IF function, like this:
HAVING SUM(IF(s.skill='PHP', s.experience, 0)) > 2
... etc.
But it comes down to the same.
The straightforward way would be to repeatedly JOIN the skills:
SELECT e.*
FROM Employees AS e
JOIN Employee_Skills AS j1 ON (e.id = j1.eid)
JOIN Skills AS s1 ON (j1.sid = s1.id AND s1.skill = 'PHP' AND s1.experience > 3)
JOIN Employee_Skills AS j2 ON (e.id = j2.eid)
JOIN Skills AS s2 ON (j2.sid = s2.id AND s2.skill = 'SQL' AND s2.experience > 1)
...
Since all the clauses are required this translated to a straight JOIN.
You will need to add two JOINs for each clause, but they're quite fast joins.
A more hackish way would be to compress the skills into a code in a 1:1 relation with the employees. If experience never exceeds, say, 30, then you can multiply the first condition's experience by 1, the second by 30, the third by 30*30, the fourth by 30*30*30... and never get an overflow.
SELECT eid, SUM(CASE skill
WHEN 'PHP' THEN 30*experience
WHEN 'SQL' THEN 1*experience) AS code
FROM Employees_Skills JOIN Skills ON (Skills.id = Employees_Skills.sid)
GROUP BY eid HAVING code > 0;
Actually since you want 3 years PHP, you can HAVE code > 91. If you had three conditions with experiences 2, 3 and 5, you would request more than x = 2*30*30 + 3*30 + 5. This only serves to whittle the results, since 3*30*30 + 2*30 + 4 still passes the filter but is of no use to you. But since you want a restriction on code, and "> x" costs the same as "> 0" and gives better results... (if you needed more complex filtering than a series of AND, > 0 is safer, though).
The table above you join with Employees, then on the result you perform the true filtering, requiring
((code / 30*30) % 30) > 7 // for instance :-)
AND
((code / 30) % 30) > 3 // for PHP
AND
((code / 1) % 30) > 1 // for SQL
(the *1 and /1 are superfluous, and only inserted to clarify)
This solution requires a full table scan on Skills, with no real possibility of automatic optimizations. So it is slower than the other solution. On the other hand, its cost grows much more slowly, so if you have complex queries, or need OR operators or conditional expressions instead of ANDs, it may be more convenient to implement the "hackish" solution.

MySQL Join and create new column value

I have an instrument list and teachers instrument list.
I would like to get a full instrument list with id and name.
Then check the teachers_instrument table for their instruments and if a specific teacher has the instrument add NULL or 1 value in a new column.
I can then take this to loop over some instrument checkboxes in Codeigniter, it just seems to make more sense to pull the data as I need it from the DB but am struggling to write the query.
teaching_instrument_list
- id
- instrument_name
teachers_instruments
- id
- teacher_id
- teacher_instrument_id
SELECT
a.instrument,
a.id
FROM
teaching_instrument_list a
LEFT JOIN
(
SELECT teachers_instruments.teacher_instrument_id
FROM teachers_instruments
WHERE teacher_id = 170
) b ON a.id = b.teacher_instrument_id
my query would look like this:
instrument name id value
--------------- -- -----
woodwinds 1 if the teacher has this instrument, set 1
brass 2 0
strings 3 1
One possible approach:
SELECT i.instrument_name, COUNT(ti.teacher_id) AS used_by
FROM teaching_instrument_list AS i
LEFT JOIN teachers_instruments AS ti
ON ti.teacher_instrument_id = i.id
GROUP BY ti.teacher_instrument_id
ORDER BY i.id;
Here's SQL Fiddle (tables' naming is a bit different).
Explanation: with LEFT JOIN on instrument_id we'll get as many teacher_id values for each instrument as teachers using it are - or just a single NULL value, if none uses it. The next step is to use GROUP BY and COUNT() to, well, group the result set by instruments and count their users (excluding NULL-valued rows).
If what you want is to show all the instruments and some flag showing whether or now a teacher uses it, you need another LEFT JOIN:
SELECT i.instrument_name, NOT ISNULL(teacher_id) AS in_use
FROM teaching_instrument_list AS i
LEFT JOIN teachers_instruments AS ti
ON ti.teacher_instrument_id = i.id
AND ti.teacher_id = :teacher_id;
Demo.
Well this can be achieved like this
SELECT
id,
instrument_name,
if(ti.teacher_instrument_id IS NULL,0,1) as `Value`
from teaching_instrument_list as til
LEFT JOIN teachers_instruments as ti
on ti.teacher_instrument_id = til.id
Add a column and check for teacher_instrument_id. If found set Value to 1 else 0.

SQL: How to show event beneath all regions depending on flag (in absence of many-to-many join)

I'm displaying a page listing events across different cities:
Los Angeles Soap Festival 13 Feb Cookie Contest 8 May
New York
Star Trek Convention 5 May
Miami
Swimsuit Contest
Events can only be linked to a single city (region_id is on the event table) or flagged as being in 'all cities' ('show_in_all_cities' flag). In the absence of a many-to-many joining table i.e. event_city, is there a way I could get an event that's flagged with 'show_in_all_cities' to appear beneath each of the cities?
My current query below (not taking into account the 'all cities' flag):
SELECT
event.*,
region.name
FROM event
INNER JOIN region
ON region.region_id = event.region_id
WHERE event.`date` >= CURDATE()
ORDER BY region.sort_order, event.date ASC
I'm then looping through with PHP to output and display the city headings.
Add the flag to the order by clause:
SELECT event.*, region.name
FROM event
INNER JOIN region
ON region.region_id = event.region_id
WHERE event.`date` >= CURDATE()
ORDER BY event.show_in_all_cities, region.sort_order, event.date ASC
(assuming that the flag is set to a higher value for the all-cities events, otherwise:
ORDER BY event.show_in_all_cities DESC, region.sort_order, event.date ASC
)
Just add or (event.SHOW_IN_ALL_CITIES=1 ) (if it is 0/1 int flag for instance) to your JOIN condition. In this case for each event with this flag ALL regions are added to the output set.
SELECT
event.*,
region.name
FROM event
INNER JOIN region
ON (region.region_id = event.region_id) or (event.SHOW_IN_ALL_CITIES=1 )
WHERE event.`date` >= CURDATE()
ORDER BY region.sort_order, event.date ASC

Categories