Advanced MySQL Joining. Speeding up query - php

I have a list of users on my php application (using codeigniter). Each user may have completed a form with about 1000 or so total fields. The structure looks similar to this:
users
id|username|...
completed_form_fields
id|formid|userid|fieldkey|data
where field key is just a unique key for that particular form field, ie: "first_name"
I have a user search page where people can filter out specific users by the fields they chose (eye color, race, gender...) Then I need to display these fields so I would love (and currently have) an output like this:
$filteredmembers = array(
[0] = Object(
[id] => 1
[username] => kilrizzy
...
[fields] => Array(
[fname] => Jeff
[gender] => Male
...
Currently my script is obviously taking forever since I query all the members who filled out this form, then loop through each one to query all of their fields. THEN filter those out based on criteria + page / offset.
I know there needs to be a way to join these together in one query I am not familiar with
Simplified version of my very slow code:
function get_members(){
$this->db->select('u.*');
$this->db->from('users AS u');
$query = $this->db->get();
if ($query->num_rows() > 0){
$members = $query->result();
//Get fields from each user
foreach($members as $mk => $mv){
$fields = $this->get_form_fields($mv->id,1,true);
$members[$mk]->fields = $fields;
}
return $members;
}else{
return false;
}
}
function get_form_fields($uid,$form,$values=false){
$this->db->where('user', $uid);
$this->db->where('form', $form);
$query = $this->db->get('form_fields');
if ($query->num_rows() > 0){
$result = $query->result();
return $result;
}else{
return false;
}
}

There is a way but it gets over expensive the more you add fields. The same thing occurs with many CMS that choose to store additionnal user data in that form.
You can get a working search SQL using this:
SELECT
users.*,
firstname.data AS firstname,
lastname.data AS lastname,
eyecolor.data AS eyecolor,
FROM
users
LEFT JOIN completed_form_fields AS firstname ON firstname.userid = users.id AND firstname.fieldkey = "firstname"
LEFT JOIN completed_form_fields AS lastname ON lastname.userid = users.id AND lastname.fieldkey = "lastname"
LEFT JOIN completed_form_fields AS eyecolor ON eyecolor.userid = users.id AND eyecolor.fieldkey = "eyecolor"
WHERE
firstname.data LIKE '%searchdata%'
OR lastname.data LIKE '%searchdata%'
OR eyecolor.data LIKE '%searchdata%'
This method gets very big and expensive for the MySQL server the more you add tables. Therefore, i would recommend not to go more than 10-15 joins like that and then again, i'd profile it to make sure.

SELECT u.username, f.formid, f.userid, f.fieldkey, f.data FROM user AS u
LEFT JOIN completed_form_fields AS f
ON f.userid = u.id
you should look at indexing your userid, index(userid), via phpmyadmin or sql file
mysql-indexes

I'm not sure I completely understand the problem. You can use a single query to get all users, and all of their fields (basically, this is what Philip suggested):
SELECT u.username, f.*
FROM user AS u
INNER JOIN completed_form_fields AS f
ON f.userid = u.id
ORDER BY u.id, f.fieldkey
To filter the results, add a WHERE clause with the conditions. For example, to only get data from fieldkeys 'k1', 'k2' and 'k3':
SELECT u.username, f.*
FROM user AS u
INNER JOIN completed_form_fields AS f
ON f.userid = u.id
WHERE f.fieldkey IN ('k1', 'k2', 'k3')
ORDER BY u.id, f.fieldkey
Is this what you're looking for?

Related

Yii2 translating findBySql query to QueryBuilder query

I have the following query using findbysql:
$query = Users::findBySql('select a.user_id, a.last_name,a.first_name, a.emp_id, ar.role_id from auth_users a, auth_user_roles AR, AUTH_USER_DEPTS AD, DEPARTMENTS D
where AD.DEPT_ID = D.DEPT_ID AND AR.USER_ID = AD.USER_ID and a.user_id = ar.user_id
AND D.DEPT_GROUP_ID = :dept_group_id AND (ACCESS_END_DATE > SYSDATE OR ACCESS_END_DATE IS NULL)
UNION
SELECT DISTINCT a.user_id, a.last_name, a.first_name, a.emp_id, NULL AS role_id FROM auth_users a, AUTH_USER_ROLES AR, AUTH_USER_DEPTS AD, DEPARTMENTS D
WHERE AD.DEPT_ID = D.DEPT_ID AND AR.USER_ID = AD.USER_ID and a.user_id = ar.user_id
AND D.DEPT_GROUP_ID = :dept_group_id AND
AR.ACCESS_END_DATE < SYSDATE AND AR.USER_ID NOT IN (select USER_ID from auth_user_roles where ACCESS_END_DATE > SYSDATE OR ACCESS_END_DATE IS NULL)', [':dept_group_id' => $dept_group_id ]);
This query does exactly what I want it to, but the problem is when I try to put it into a gridview it does not sort. According to Sort and search column when I'm querying with findbysql in yii2 it seems like I need to use query builder instead.
So I was trying to do that with the first part of my query (before the union), and it looks like so:
$query1 = (new \yii\db\Query())
->select(['user_id', 'last_name', 'first_name', 'emp_id'])
->from('AUTH_USERS');
$query2 = (new \yii\db\Query())
->select('USER_ID')
->from('AUTH_USER_ROLES')
->where('ACCESS_END_DATE>SYSDATE OR ACCESS_END_DATE IS NULL');
$query = $query1->innerJoin('AUTH_USER_DEPTS', 'AUTH_USER_DEPTS.user_id = AUTH_USERS.user_id')->innerJoin('DEPARTMENTS', 'AUTH_USER_DEPTS.dept_id = DEPARTMENTS.dept_id');
$query->innerJoin('AUTH_USER_ROLES', 'AUTH_USER_ROLES.USER_ID = auth_users.USER_ID')->where('ACCESS_END_DATE>SYSDATE OR ACCESS_END_DATE IS NULL');
However, my query comes out like this in yii and apparently oracle is not accepting the double quotes around the column names:
SELECT "user_id", "last_name", "first_name", "emp_id" FROM "AUTH_USERS"
INNER JOIN "AUTH_USER_DEPTS" ON AUTH_USER_DEPTS.user_id = AUTH_USERS.user_id
INNER JOIN "DEPARTMENTS" ON AUTH_USER_DEPTS.dept_id = DEPARTMENTS.dept_id
INNER JOIN "AUTH_USER_ROLES" ON AUTH_USER_ROLES.USER_ID = auth_users.USER_ID
WHERE ACCESS_END_DATE>SYSDATE OR ACCESS_END_DATE IS NULL
I know the query might be incorrect here already but I cant even get the double quotes to go away. Tried defining the select statements multiple ways suggested by the yii docs already with no success:
select(['user_id', 'last_name', 'first_name', 'emp_id'])
select('user_id', 'last_name', 'first_name', 'emp_id')
select("user_id, last_name,first_name,emp_id")
I have also tried joining the queries like this from the docs: http://www.yiiframework.com/doc-2.0/guide-db-query-builder.html
$query = $query1->innerJoin(['u' => $query2], 'u.user_id = user_id');
but it also complains that it doesnèt recognize u and the query instead comes out like so in yii:
SELECT COUNT(*) FROM "AUTH_USERS" INNER JOIN "AUTH_USER_DEPTS" ON AUTH_USER_DEPTS.user_id = AUTH_USERS.user_id INNER JOIN "DEPARTMENTS" ON AUTH_USER_DEPTS.dept_id = DEPARTMENTS.dept_id INNER JOIN (SELECT "USER_ID" FROM "AUTH_USER_ROLES" WHERE ACCESS_END_DATE>SYSDATE OR ACCESS_END_DATE IS NULL) "u" ON u.user_id = auth_users.user_id
At this point im just looking for the easiest way to build this query (whether it be using querybuilder or some other way) so that I can pass the query to my gridview and sort it.
I would recommend you first create all the data models you need from the tables you need for the query, using Gii it should be easy and it even creates the relationships you will need.
After that, you can do something like the following:
$query = Users::find()
->joinWith('theRelation1Name')
->joinWith('theRelation2Name')
->joinWith('theRelation3Name')
...
This way you don't need to give tables aliases or add the conditions needed for the relations to work.

Inner/Left join with two different where clauses

i'm in the process of joining two tables together under two different conditions. For primary example, lets say I have the following nested query:
$Query = $DB->prepare("SELECT ID, Name FROM modifications
WHERE TYPE =1 & WFAbility = '0'");
$Query->execute();
$Query->bind_result($Mod_ID,$Mod_Name);
and this query:
$Query= $DB->prepare("SELECT `ModID` from `wfabilities` WHERE `WFID`=?");
$Query->bind_param();
$Query->execute();
$Query->bind_result();
while ($Query->fetch()){ }
Basically, I want to select all the elements where type is equal to one and Ability is equal to 0, this is to be selected from the modifications table.
I further need to select all the IDs from wfabilities, but transform them into the names located in modifications where WFID is equal to the results from another query.
Here is my current semi-working code.
$Get_ID = $DB->prepare("SELECT ID FROM warframes WHERE Name=?");
$Get_ID->bind_param('s',$_GET['Frame']);
$Get_ID->execute();
$Get_ID->bind_result($FrameID);
$Get_ID->fetch();
$Get_ID->close();
echo $FrameID;
$WF_Abilties = $DB->prepare("SELECT ModID FROM `wfabilities` WHERE WFID=?");
$WF_Abilties->bind_param('i',$FrameID);
$WF_Abilties->execute();
$WF_Abilties->bind_result($ModID);
$Mod_IDArr = array();
while ($WF_Abilties->fetch()){
$Mod_IDArr[] = $ModID;
}
print_r($Mod_IDArr);
$Ability_Name = array();
foreach ($Mod_IDArr AS $AbilityMods){
$WF_AbName = $DB->prepare("SELECT `Name` FROM `modifications` WHERE ID=?");
$WF_AbName->bind_param('i',$AbilityMods);
$WF_AbName->execute();
$WF_AbName->bind_result($Mod_Name);
$WF_AbName->fetch();
$Ability_Name[] = $Mod_Name;
}
print_r($Ability_Name);
See below:
SELECT ModID,
ID,
Name
FROM modifications M
LEFT JOIN wfabilities WF
ON WF.ModID = M.ID
WHERE TYPE =1 & WFAbility = '0'
To do this, you need to join your tables, I'm not quite sure what you are trying to do so you might have to give me more info, but here is my guess.
SELECT ID, Name, ModID
FROM modifications
JOIN wfabilities
ON WFID = ID
WHERE TYPE = '1'
AND WFAbility = '0'
In this version I am connecting the tables when WFID is equal if ID. You will have to tell me exactly what is supposed to be hooking to what in your requirements.
To learn more about joins and what they do, check this page out: MySQL Join
Edit:
After looking at your larger structure, I can see that you can do this:
SELECT modifications.Name FROM modifications
JOIN wfabilities on wfabilities.ModID = modifications.ID
JOIN warframes on warframes.ID = wfabilities.WFID
WHERE warframes.Name = 'the name you want'
This query will get you an array of the ability_names from the warframes name.
This is the query:
"SELECT A.ID, A.Name,B.ModID,C.Name
FROM modifications as A
LEFT JOIN wfabilities as B ON A.ID = B.WFID
LEFT JOIN warframes as C ON C.ID = B.WFID
WHERE A.TYPE =1 AND A.WFAbility = '0' AND C.Name = ?"

Mysql Join two Id's to get there Usernames

Im trying to get in Array that contains the results from a MYSQL query.
I have 2 ids stored in the table hitlist user_id and mark_id
they need to join in the table users to retrieve there usernames that match there id's and in the future other variables.
i have this working in a weird way and was hopeing to get this working in a more efficent simple way similar to this
$Hitlists = $db->query("SELECT * FROM hitlist JOIN users ON hitlist.user_id = users.id AND hitlist.mark_id = users.id")->fetchAll();
This is the code i have that is working...for now it looks like it might give me problems later on.
<?php
$index = 0;
$Hitlists = array();
$st = $db->query("SELECT * FROM hitlist JOIN users ON hitlist.user_id = users.id")->fetchAll();
$sth = $db->query("SELECT * FROM hitlist JOIN users ON hitlist.mark_id = users.id")->fetchAll();
foreach($st as $id)
{
$Hitlists[] = $id;
}
foreach($sth as $id)
{
$Hitlists[$index]['markedby'] = $id['username'];
$Hitlists[$index]['mark_id'] = $id['mark_id'];
$index++;
}
The way you are joining the table is wrong. You can get the exact records you want, you need to join users table twice to get the username of each ID
SELECT a.*,
b.username User_name,
c.username mark_name
FROM hitlist a
INNER JOIN users b
ON a.user_id = b.id
INNER JOIN users c
ON a.mark_id = c.id
and you can access
$result['User_name']
$result['mark_name']

Create a Multidimensional Array with PHP and MySQL

I am new to PHP and am looking at efficient ways to return data from the database. Lets say I have a UserProfile table that has a one to many relationship with UserInterest and UserContact:
Select p.Id, p.FirstName, p.LastName, i.Name as Interests, c.Email, c.Phone
from UserProfile p
left join UserInterest i on p.Id = i.UserProfileId
left join UserContact c on p.Id = c.UserProfileId
where p.Id = 1
An efficient way to retrieve data would be to create a multidimensional array such as:
$user = array( "FirstName" => "John",
"LastName" => "Doe",
"Gender" => "Male",
"Interests" => array(
"Hiking",
"Cooking"),
"Contact" => array(
"Email" => "john.doe#gmail.com",
"Phone" => "(555) 555-5555"));
I can't seem to get my head around how this would be constructed in PHP. For simple data like interests I could use group_concat(i.Name) as Interests in the query to return interests back as a comma separated list in a single row, however, for an associative array such as Contact, I'd like to be able to get a handle on each key in the array using $user['Contact']['Email'].
From a "Best Practices" standpoint, I would assume that constructing an array like this in one query is a lot better than hitting the database multiple times to retrieve this data.
Thanks!
Neil
You can construct this array in one pass through the data returned by your query. In pseudo-code:
for each row
$user["FirstName"] = row->FirstName;
$user["LastName"] = row->LastName;
$user["Interests"][] = row->Interests;
$user["Contact"]["Email"] = row->Email;
$user["Contact"]["Phone"] = row->Phone;
next
The syntax $user["Interests"][] = $data is valid PHP code. It is equivalent to array_push($user["Interests"], $data).
My guess is that you would have to run separate queries and manually construct the array yourself. I don't personally know of any MySQL database resources in PHP that return rowsets like that in a multidimensional array the way you described.
A better way of doing this could be:
$query = "SELECT p.Id, p.FirstName, p.LastName, i.Name as Interests,
c.Email, c.Phone
FROM UserProfile p
LEFT JOIN UserInterest i ON p.Id = i.UserProfileId
LEFT JOIN UserContact c ON p.Id = c.UserProfileId
WHERE p.Id = 1";
$res = GIDb::pg_query($query);
if(pg_num_rows($res) > 0)
{
while($data = pg_fetch_assoc($res))
{
$id = $data['Id'];
$f_name = $data['FirstName'];
$l_name = $data['LastName'];
$interests = $data['Interests'];
$email = $data['Email'];
$phone = $data['Phone'];
}
}
//You can also directly use $data['Id'] etc.
Also don't forget to include in the beginning of the php code : include_once(dirname(__FILE__)."/../../class.GIDb.php");
EDIT :
This is for Postgres.
For MySQL use: mysql_query(), mysql_fetch_assoc(), mysql_num_rows()

remove results of 1 query that appear in another

I have a query that retrieves the name of each friend a user has by joining that of friends and users tables. I have another table that stores active users. I need to retrieve friends that are active and not active but for some reason I am drawing a blank. If I have a list of all friends and a list of active friends, can I subtract active from all to be left with offline? All I Want to do basically is have two tabs. Under one will be offline friends. Under the other will be online friends. If anyone has any useful suggestions, I would appreciate it.
$sql = 'SELECT * FROM users
LEFT JOIN friendships
ON friendships.friend_id = users.id
WHERE friendships.user_id = ?';
$stmt5 = $conn->prepare($sql);
$result=$stmt5->execute(array($userid));
$count=$stmt5->rowCount();
//user has more than 0 friends
if ($count>0){
while ($row = $stmt5->fetch(PDO::FETCH_ASSOC)) {
$online=htmlspecialchars( $row['username'], ENT_NOQUOTES, 'UTF-8' );
//check whos online
$sql = 'SELECT * FROM active_users
WHERE username=?';
$stmt7 = $conn->prepare($sql);
$result=$stmt7->execute(array($online));
$count=$stmt7->rowCount();
while ($row = $stmt7->fetch(PDO::FETCH_ASSOC)) {
$activeuser=$row['username'];
}
}
This code just retrieves active users but hopefully gives an idea of structure.
Could you do a "not in" clause? Without knowing the layout of your database, I'm thinking something like this:
SELECT * FROM users
LEFT JOIN friendships
ON friendships.friend_id = users.id
WHERE friendships.user_id = ?
AND users.id NOT IN (
SELECT user_id FROM active_users
)
Using SQL to do this 'not in' is probably the best solution.
You could also do this in code if you really want to if the results are ordered. Just loop through the all users list, grab the first result from the active users list, and whenever there's a match, put that on the active users list and grab the next active user. Put every non-match into the inactive users list and only fetch from the all users list.
Something like this might tell you both lists in one shot:
SELECT users.username, active_users.username AS active FROM users
LEFT JOIN friendships
ON friendships.friend_id = users.id
LEFT JOIN active_users ON users.username = active_users.username
WHERE friendships.user_id = ?
Inactive users would return NULL in the active columns, where active would not.

Categories