Converting PHP dependent SQL code to full SQL - php

the question will require a bit long of an answer to explain due to my ignorance on SQL.
I hope it will not be viewed as vague because I have tried doing it by parts, but then I wont know which part exactly is causing which problem.(It really shows my level of knowledge on SQL.)
I have a code that was originally written in a PHP file, but I have decided I want to create a view table in order for the page to load faster.
The reason was because it does a loop to list the ranking of students and was taking too long for the web page to load.
Anyways, here is the code :
SELECT
SUM(VCA.meritPoint) AS merit,
VCA.student_no AS student_no,
P.program_code AS education_level,
P.name AS name,
P.gender AS gender,
P.campus_id AS campus_id
FROM viewcardactivity VCA
JOIN pupil P ON P.student_no = VCA.student_no
JOIN semester S ON S.id = '{$id}' -- MAX() AND (MAX() - 1)
AND DATE(VCA.tarikh) BETWEEN DATE(s.tarikhStart) AND DATE(s.tarikhEnd)
WHERE P.campus_id = '{$campus}' -- 1, 2
AND P.gender= '{$gender}' -- M, F
AND VCA.level= '{$level}' -- Diploma, Degree
AND P.program_code = (CONVERT(IF((SUBSTR(REPLACE(`p`.`program_code`,' ',''),3,1) = 1),'Diploma','Degree')USING latin1))
GROUP BY student_no ORDER BY merit DESC
As the name of the columns suggests, I would like to display more than one instead of specific ids, gender and level provided from the PHP variables.
The example output I would like to have is such as(based on the SQL Fiddle mock data :
table 'viewrankingmerit'
| merit | student_no | education_level | name | gender | campus_id |
---------------------------------------------------------------------
| 99 | 111111111 | Diploma | Ash | M | 1 |
---------------------------------------------------------------------
| 87 | 222222222 | Diploma |Belle | F | 1 |
---------------------------------------------------------------------
| 85 | 333333333 | Degree | Carl | M | 1 |
---------------------------------------------------------------------
| 80 | 444444444 | Degree | Deli | F | 1 |
---------------------------------------------------------------------
| 75 | 555555555 | Diploma | Eddy | M | 2 |
---------------------------------------------------------------------
| 74 | 666666666 | Diploma |Foxxy | F | 2 |
---------------------------------------------------------------------
| 50 | 777777777 | Degree | Greg | M | 2 |
---------------------------------------------------------------------
| 20 | 888888888 | Degree |Haley | F | 2 |
---------------------------------------------------------------------
As for the semester id, I would like to get the latest 2 ids. Which is the highest and second highest, based on the auto-generated id that will keep on increasing..
I was immediately stuck at trying to get 2 ids from table semester. I've tried using :
JOIN semester S1 ON S1.id = (SELECT MAX(s1.id) FROM semester)
AND DATE(VCA.tarikh) BETWEEN DATE(s1.tarikhStart) AND DATE(s1.tarikhEnd)
JOIN semester S2 ON S2.id = (SELECT MAX(s2.id)-1 FROM semester)
AND DATE(VKA.tarikh) BETWEEN DATE(s2.tarikhStart) AND DATE(s2.tarikhEnd)
It was probably a bad reference, but that was the closest solution I got so far.
1) Is it possible to do a table to show all the info?
2) If yes, how to get both S.id, P.campus_id, P.gender and VCA.level. Hoping that the solution would be alike.
3) If no, what is the best solution?
Thanks a lot guys.
[Edit] I've added a demo data in an SQL Fiddle

After some discussion in coments, this is the final result. I think.
select sum(vca.meritPoint) as merit,
vca.student_no AS student_no,
vca.type AS education_level,
p.name AS name,
p.gender AS gender,
p.campus_id AS campus_id
from
viewcardactiviti vca
inner join pupil p ON p.student_no = vca.student_no
inner join (select * from semester order by id desc limit 2) s
ON (vca.tarikh between s.tarikhStart and s.tarikhEnd
AND vca.type = s.level)
group by vca.student_no, vca.type, p.name, p.gender, p.campus_id
order by merit desc, p.campus_id;
See it here on SQLFiddle
If you need to filter for specific configurations like the parameters on your original query just add a WHERE clause.
This subquery (select * from semester order by id desc limit 2) will get the last to semesters based on the ID. And since there is no direct link (foreign key) between semester and viewcardactiviti you can use there join conditions ON (vca.tarikh between s.tarikhStart and s.tarikhEnd AND vca.type = s.level)
If you think that it still need to change anything let me know!

Related

How to GROUP GROUP_CONCAT()?

I am looking for a way to get groups of the GROUP_CONCAT() function in a single query, for example.
My current code
SELECT
SUBSTRING_INDEX(GROUP_CONCAT(service_info.ip_address SEPARATOR ','),',',service_plans.aggregation) AS ip_address
FROM
services
LEFT JOIN
service_info
ON
service_info.service_id = services.id
LEFT JOIN
service_plans
ON
service_plans.id = services.service_plan_id
WHERE
service_plans.id = '2'
I want to group the IP addresses by a specific number(the $group_by variable if you see in the query) but then separate by a different character such as ":" or something.
Essentially I want my output to look like:
If $group_by=2: 10.1.1.2,10.1.1.3:10.1.1.4,10.1.1.5
If $group_by=3: 10.1.1.2,10.1.1.3,10.1.1.4:10.1.1.5
Is this possible to implement into my current query?
UPDATE: table structure
Table service_plans
id | name | aggregation
-----------------------------------------
1 | Uncapped 10Mbps 20:1 | 20
2 | Uncapped 20Mbps 10:1 | 10
3 | Capped 30Mbps | 0
Table services
id | service_plan_id | description
------------------------------------
1 | 2 | Phone
2 | 2 | Laptop
3 | 2 | PC
4 | 2 | TV
5 | 2 | Test
Table service_info
id | service_id | ip_address
------------------------------
1 | 1 | 10.1.1.2
2 | 2 | 10.1.1.3
3 | 3 | 10.1.1.4
4 | 4 | 10.1.1.5
5 | 5 | 10.1.1.6
I am trying to get an array of ip_address's concatenated and separated by a comma but the in groups of however much the service_plans.aggregation value is.
If aggregation is 2, then my output should be:
10.1.1.2,10.1.1.3:10.1.1.4,10.1.1.5
As you can see they are in groups of 2 and then the next group is separated by a colon(:)
If aggregation is 3, then my output should be:
10.1.1.2,10.1.1.3,10.1.1.4:10.1.1.5
As you can see they are in groups of 3 and then the next group is separated by a colon(:) and so on
Your post is a little confusing. What would be helpful is if you posted sample data, and then posted what you want your query to return. I'll give you an answer to what I think you're asking, based on the subject of your post.
ServicePlanIPs
service_plan_id | ip_address
-------------------------------
1 | 192.168.70.1
1 | 192.168.70.2
1 | 192.168.70.3
2 | 192.168.70.4
2 | 192.168.70.5
2 | 192.168.70.6
If you run this query against ServicePlanIPs:
SELECT service_plan_id, GROUP_CONCAT(ip_address) as ip_addresses
FROM ServicePlanIPs
GROUP BY service_plan_id
You will get:
service_plan_id | ip_addresses
-------------------------------
1 | 192.168.70.1, 192.168.70.2, 192.168.70.3
2 | 192.168.70.4, 192.168.70.5, 192.168.70.6
I don't guarantee this will run out of the box, but it should get you on the right track. Hope it helps. Note - if you're using a version of mysql which supports window functions, you can do something similar to the below and use the natively supported RANK function instead of doing it manually with variables.
SET #curRank := 0;
SET #concatIps := '';
SELECT
sp.id,
#curRank := #curRank + 1 AS rank,
IF(MOD(#curRank, (SELECT aggregation FROM service_plans WHERE id = {service_plan_id}) = 0, #concatIps := CONCAT(#concatIps, ':', s.ip_address), #concatIps := CONCAT(#concatIps, ',', s.ip_address))
FROM service_plans sp
JOIN services s
ON sp.id = s.service_plan_id
JOIN service_info si
ON si.service_id = s.id
WHERE sp.id = {service_plan_id}
ORDER BY service_info_id

MySQL and PHP One to Many Return Data in Single Row, Multiple Columns

I have been learning PHP and MySQL for a project and I am struggling with this part. For simplicity sake, I will only list the relevant fields (actually many more in real db) let's say I have 3 tables.
Table1
---------------------
Index | Name | email
1 | Rob | rob#email.com
2 | Kevin| kevin#email.com
3 | Amy | amy#email.com
Table2
------------------------
id | Info | Submitted
1 | Blah | 0
2 | Yada | 1
Table 3
-------------------------
id | Goal |Submitted
1 | 1 | 1
1 | 2 | 1
1 | 3 | 1
1 | 4 | 0
1 | 5 | 0
3 | 1 | 0
3 | 3 | 1
3 | 4 | 1
So, Table1 holds user information and is kinda the main table
Table2 the user inputs some data in a field and then submits for approval when ready. (I will be using the value of Submitted for functions later)
If the user has not submitted the info, there is not record. This is a 1 to 1 with Table1
Table3 The user inputs information for 5 goals. At any given time there could be 0 to max 5 goals entered for a user. The Submitted is the same for later processing. This table is Many to one with Table1. The Goal field literally shows the number 1 through 5, there is a separate field that holds the goal text, just not needed in this example.
Desired output is HTML table
Name | email | info |Goal1|Goal2|Goal3|Goal4|Goal5|
Rob | rob#email.com | Blah | 1 | 2 | 3 | 4 | 5 |
Kevin | kevin#email.com | Yada | | | | | |
Amy | amy#email.com | | 1 | | 3 | 4 | |
Not sure if the blanks are considered NULL or something else as they do not exist in the DB. I would like to put something in the field like an *. Basically the Submitted will be used for code to make the fields hyper links, so they need to be part of the query, just not in the table display, but if it would help, it can be displayed.
Name | email | info |Goal1|Goal2|Goal3|Goal4|Goal5|
Rob | rob#email.com | Blah | 1 | 2 | 3 | 4 | 5 |
Kevin | kevin#email.com | Yada | * | * | * | * | * |
Amy | amy#email.com | * | 1 | * | 3 | 4 | * |
I am using a query with left joins and group_concat, but that is not working well with the non existent data, and I cannot figure out how to include the Submitted field without doing some crazy concatenation, then pulling in all apart to put in the HTML fields.
I can include some code, but it might be hard to follow as there are lots of variables being used.
The best I have gotten out using only table1 and table3:
Rob 1,2,3,4,5
Kevin
Amy 1,3,4
With the records that have not been entered not be accounted for, it makes it near impossible to turn the data string into a table. If I can get something showing for every position, even if it does not exist yet, I do know how to make it into the html table.
I hope this makes sense and someone can help me with this.
As you stated that you have at most 5 goals. The best possible option is to use following query.
SELECT t1.Name,t1.email
,CASE WHEN t2.Info IS NULL THEN * ELSE t2.Info END as Info
,CASE WHEN g1.Goal IS NULL THEN * ELSE g1.Goal END as Goal1
,CASE WHEN g2.Goal IS NULL THEN * ELSE g2.Goal END as Goal2
,CASE WHEN g3.Goal IS NULL THEN * ELSE g3.Goal END as Goal3
,CASE WHEN g4.Goal IS NULL THEN * ELSE g4.Goal END as Goal4
,CASE WHEN g5.Goal IS NULL THEN * ELSE g5.Goal END as Goal5
FROM Table1 as t1
LEFT JOIN Table2 as t2 ON t2.id = t1.Index
LEFT JOIN Table3 as g1 ON g1.id = t1.Index AND g1.Goal=1
LEFT JOIN Table3 as g2 ON g2.id = t1.Index AND g2.Goal=2
LEFT JOIN Table3 as g3 ON g3.id = t1.Index AND g3.Goal=3
LEFT JOIN Table3 as g4 ON g4.id = t1.Index AND g4.Goal=4
LEFT JOIN Table3 as g5 ON g5.id = t1.Index AND g5.Goal=5
You will want to JOIN each of the tables together, and GROUP BY the user.
One way of handling the goals would be to display it as one column, containing all goals.
This should help you get started:
SELECT name,
email,
info,
<LOGIC> as goals
FROM table1
JOIN table2 ON table2.user_id = table1.id
JOIN table3 ON table3.user_id = table1.id
GROUP BY name
The logic that you use for the goals column could be created with a mixture of CASE and CONCAT (if a goal is defined, concatenate it into a string, and display that string as the final value of goals).

MySQL Multiple Conditions on Group By / Having Clause

I have three tables that are all inter-related with the following structure.
ModuleCategory Table:
+------------------+----------------+------------+
| ModuleCategoryID | ModuleCategory | RequireAll |
+------------------+----------------+------------+
| 90 | Cat A | YES |
| 91 | Cat B | NO |
+------------------+----------------+------------+
ModuleCategorySkill Table:
+------------------+---------+
| ModuleCategoryID | SkillID |
+------------------+---------+
| 90 | 1439 |
| 90 | 3016 |
| 91 | 1440 |
| 91 | 3016 |
+------------------+---------+
EmployeeSkill Table:
+---------+---------+
| EmpName | SkillID |
+---------+---------+
| Emp1 | 1439 |
| Emp1 | 3016 |
| Emp2 | 1440 |
| Emp2 | 3016 |
| Emp3 | 1439 |
| Emp4 | 3016 |
+---------+---------+
Desired Output:
+------------------+-------+
| ModuleCategory | Count |
+------------------+-------+
| Cat A | 1 |
| Cat B | 3 |
+------------------+-------+
I am trying to group by ModuleCategoryID's and get the count of employees which have the skills being tracked.
Normally, I can do the following query to obtain the numbers:
select mc.ModuleCategory, Count(*) as Count from ModuleCategory as mc
join ModuleCategorySkill as mcs on mc.ModuleCategoryID = mcs.ModuleCategoryID join EmployeeSkill as es on es.SkillID= mcs.SkillID
group by mc.ModuleCategoryID
However, I have a column RequireAll in the ModuleCategory table which if it is set to 'YES' should only count employees as 1 only if they have all the skills in the category. If it is set to NO then it can count each row normally and increase the count by the number of rows it groups by.
I can achieve this by writing separate queries for each modulecategoryID and using a having Count() > 1 (which will find me anyone that has all the skills for ModuleCategoryID 90). If there were 3 skills than I would have to change it to Having Count() > 2. If there isn't anyone that has all the skills specified, the count should be 0.
I need a dynamic way of being able to do this since there is a lot of data and writing one query for each ModuleCategoryID isn't the proper approach.
Also, I am using PHP so I can loop through and create a sql string that can help me achieve this. But I know I will run into performance issues on big tables with a lot of skills and modulecategoryID's.
Any guidance on how to achieve this is much appreciated.
You can do it by joining on the total category counts, and then using conditional aggregation:
select modulecategory,
count(case when requireall = 'yes'
then if(s = t, 1, null)
else s
end)
from (
select modulecategory,empname, requireall, count(*) s, min(q.total) t
from employeeskill e
inner join modulecategoryskill mcs
on e.skillid = mcs.skillid
inner join modulecategory mc
on mcs.modulecategoryid = mc.modulecategoryid
inner join (
select modulecategoryid, count(*) total
from modulecategoryskill
group by modulecategoryid
) q
on mc.modulecategoryid = q.modulecategoryid
group by modulecategory, empname
) qq
group by modulecategory;
demo here
This operates under the assumption an employee isn't going to be allocated the same skill twice, if that is something that may happen, this query is alterable to support it, but it seems like a broken scenario to me.
What we have here is an inner query that collates all the information we need (category name, employee name, whether or not all skills are required, how many skills are in the group per employee, and how many there in the group total), with an outer query that uses a conditional count to change how the rows are tallied, based on the value of requireall.

SQL query from two tables ordering results by both tables columns

I have two tables for registering out-going phone calls.
First table is people. The scheme is:
-----------------------------------
id | fname | lname | phone_number |
-----------------------------------
1 | John | Black | 132333312 |
2 | Marry | White | 172777441 |
-----------------------------------
Second tables called calls. The scheme is:
----------------------------------------------------------------------
id | scr_phone | dst_phone | dst_public_id | date | notes |
----------------------------------------------------------------------
1 | 555 | 132333312 | 1 | 1.12.2013 | chit-chat |
2 | 555 | 172777441 | 2 | 1.12.2013 | serios talk |
3 | 555 | 172777441 | 2 | 2.12.2013 | conversation|
----------------------------------------------------------------------
I'm displaying list of phones for users not in an alphabetical order but by frequency of calls. So whoever calls from local phone 555 sees at the top the people who he/she calls more often. Here is the MySQL query I use for it:
SELECT p.fname, p.lname, c.notes, COUNT(c.dst_public_id) AS amount
FROM people p
JOIN calls c ON c.dst_public_id = p.id
WHERE phones != ''
GROUP BY p.id
ORDER BY amount DESC, p.lname ASC
As a result I get person number 2 at the top of the phones list and person number 1 is on second place (I count how many calls are for each individual public and order it by that amount). Here is the query result:
--------------------------------
fname | lname | notes | amount |
--------------------------------
Marry | White | | 2 |
John | Black | | 1 |
--------------------------------
This is all good. But I want always to display last note made on last conersation with that person. I can probably make a totally separated MySQL query for each person requesting last record of each and getting the data from that, something like:
SELECT notes FROM calls WHERE dst_public_id = 2 ORDER BY date DESC LIMIT 1
... and then add the result of the first query inside my PHP script, but is there any way to do it with one single query?
You can get the last note with the following MySQL trick, using group_concat() and substring_index():
SELECT p.fname, p.lname, c.notes, COUNT(c.dst_public_id) AS amount,
substring_index(group_concat(notes order by id desc separator '^'), '^', 1) as last_notes
FROM people p JOIN
calls c
ON c.dst_public_id = p.id
WHERE phones != ''
GROUP BY p.id
ORDER BY amount DESC, p.lname ASC;
For this to work, you need a separator character that will never appear in the notes. I have chosen '^' for this purpose.
Can you try this? I haven't tested it as I don't have
SQL in front of me but you should get the idea.
SELECT tbl.notes, tbl.amount, p.fname, p.lname
FROM people p
join
(
select t.dst_public_id, c1.notes, t.cnt as amount
from calls c1 join
(
SELECT c2.dst_public_id, count(c2.id) as cnt, max(c2.id) as id
FROM
calls c2
group by c2.dst_public_id
) t on c1.id = t.id
) tbl on p.id = tbl.dst_public_id
WHERE p.phones != ''
GROUP BY p.id
ORDER BY tbl.amount DESC, p.lname ASC

GROUP_CONCAT With Nested Set Model

I have an application that uses a nested set model class to organise my data, however I'm trying to write a query that will group_concat my results. I know I need to put some sub select statements somewhere but I can't figure it out!
Here's my structure at the moment:
table: person
-----------+------------+-----------
|Person_ID | Name | Age |
-----------+------------+-----------
| 1 | Mark Vance | 19 |
| 2 | Michael Tsu| 22 |
| 3 | Mark Jones | 29 |
| 4 | Sara Young | 25 |
-----------+------------+-----------
table: person_to_group
----+------------+-----------
|ID | Person_ID | Group_ID |
----+------------+-----------
| 1 | 3 | 1 |
| 2 | 3 | 2 |
| 3 | 1 | 2 |
| 4 | 4 | 3 |
----+------------+-----------
table: groups
----------+--------------+--------------+-------------
|Group_ID | Group_Name | Group_Left | Group_Right |
----------+--------------+--------------+-------------
| 1 | Root | 1 | 6 |
| 2 | Node | 2 | 5 |
| 3 | Sub Node | 3 | 4 |
----------+--------------+--------------+-------------
I need to render something like this with my results:
//Grab the group_IDs for this person and put them in the class tag...
<li class="2 3">Sara Young is in the Sub Node Group</li>
Notice that although Sara is in the Sub Node group, she is still being given the id for Node aswell because she is a child of Node.
The following is the query that I am working with as a starting point.
SELECT *, GROUP_CONCAT( CAST( gg.Group_ID AS CHAR ) SEPARATOR ' ' ) Group_IDs
FROM groups gg
LEFT JOIN person_to_group AS t1 ON gg.Group_ID = t1.Group_ID
LEFT JOIN person AS t2 ON t2.Person_ID = t1.Person_ID
GROUP BY t2.per_ID
ORDER BY t2.Name ASC
Any help would be much appreciated!
Here's how I'd write the query:
SELECT p.Name,
GROUP_CONCAT( g.Group_Name ) AS Group_List,
GROUP_CONCAT( CAST( gg.Group_ID AS CHAR ) SEPARATOR ' ' ) AS Group_ID_List
FROM person AS p
INNER JOIN person_to_group AS pg ON p.Person_ID = pg.Person_ID
INNER JOIN groups AS g ON pg.Group_ID = g.Group_ID
INNER JOIN groups AS gg ON g.Group_Left BETWEEN gg.Group_Left AND gg.Group_Right
GROUP BY p.Name
ORDER BY p.Name ASC
Note that if you group by person name, you also need to GROUP_CONCAT the list of group names. According to your schema, a person could belong to multiple groups, because of the many-to-many relationship.
I also recommend against using SELECT * in general. Just specify the columns you need.
This was little bit interesting as I do programming in both MsSQL and MySql. In SQL I have used function called STUFF. In MySQL you can use a function called INSERT. I tried out the below query in MsSQL. Don't have a MySQL handy to try out my query. If I have time I will post the MySQL version of the query.
DECLARE #person TABLE (Person_ID INT, Name VARCHAR(50), Age INT)
INSERT INTO #person VALUES
(1,'Mark Vance',19),
(2,'Michael Tsu',22),
(3,'Mark Jones',29),
(4,'Sara Young',25)
DECLARE #groups TABLE (Group_ID INT, Group_Name VARCHAR(50), Group_Left INT, Group_Right INT)
INSERT INTO #groups VALUES
(1,'Root',1,6),
(2,'Node',2,5),
(3,'Sub Node',3,4)
DECLARE #person_to_group TABLE (ID INT, Person_ID INT, Group_ID INT)
INSERT INTO #person_to_group VALUES
(1,3,1),
(2,3,2),
(3,1,1),
(4,4,1),
(4,1,1)
SELECT *,STUFF((SELECT ',' + CAST(g.Group_ID AS VARCHAR) FROM #groups g
JOIN #person_to_group pg ON g.Group_ID = pg.Group_ID AND pg.Person_ID = a.Person_ID FOR XML PATH('')) , 1, 1, '' ) FROM #person a
Function: INSERT(str,pos,len,newstr)
Documentation: http://dev.mysql.com/doc/refman/5.0/en/string-functions.html#function_insert

Categories