Clean result from multiple left joins in mysql - php

Let's say I have these 3 tables:
Person table
id | name
1 | Sam
Dress table
id | person_id |name
1 | 1 |shorts
2 | 1 |tshirt
Interest table
id | person_id | interest
1 | 1 | football
2 | 1 | basketball
(Above is just a simplified example, in real I have a lot many tables to join)
I need to show all these details on a page, so combined all into 1 left join query mainly for performance. Now the result we get should be messy with repeated results for combinations of dresses and interests for a person. To fix this I will need to manually loop through to arrange into an array that I want to consume. My query looks something like this (am I doing it right?):
select p.id, d.name, i.interest
from person as p
left join dress as d on p.id = d.person_id
left join interest as i on p.id = i.person_id
where p.id = 1;
What is a better way to do this? I am aware that I can also use GROUP_CONCAT to avoid repetition.
UPDATED WITH OUTPUT
I want my final result to look like this (I know I need to loop through to get this format), what would be the best way to query my tables to achieve this?
[
[
'id' => 1,
'dresses' => [
[
'id' => 1,
'name' => 'shorts',
...more columns
],
[
'id' => 2,
'name' => 'tshirt',
..more columns
]
],
'interests' => [
'football',
'basketball'
]
]
]

Amount of data vs. flexibility:
Personally, for your task - let's suppose it's a bit more complex than it is presented, ok? - I wouldn't recommend you to use any sql functions (like group_concat, etc) at all. You may, of course, get a smaller quantity of data by using them. But you would certainly loose the flexibility you need to read and process the fetched results.
Think about running a query with (maybe a lot) more columns. Would you still want to "beautify" the query if some of them would suddenly require you to apply other sql functions or conditions - like another simple, but tricky GROUP BY clauses? What would happen then with your results reading algorithm? It would have to be (maybe hard)-rethought again.
Resources eaters:
Also, please keep in mind, that all these group_concat functions/selections are eating MySQL resources too.
Indexes and EXPLAIN for optimizations:
I'm just also thinking about a situation, in which you would want to apply indexes to some fields - for searching purposes, for example. And that you would want to check their validity/rapidity with an EXPLAIN command. I sincerely don't know if having group_concat's would make this an easy and transparent task.
Display purposes vs. post-processing?
In general, functions like group_concat are used for display purposes, for example in data grids/tables. But your task requires post-processing of the fetched data.
Already sorted:
That said, in your original question you already presented an sql solution. IMHO, your version is the proper and the flexible one. And your sql statement is already correct. You can maybe apply some ORDER BY conditions, in order to directly build a sorted array from the fetched data.
Fetch data and/or post-processing... Alternatives?
You are trying to fetch a lot of data at once AND to post-process it too. This is a sign, that both, the database AND the PHP engine have to work a lot. Maybe it would be better to project your task in another way. E.g. fetch a lot of data without post-processing. Or fetch a smaller amount of data and allow PHP to post-process it. Look what I've found today on the PDOStatement::fetchAll webpage
PDOStatement::fetchAll - Return Values:
Using this method to fetch large result sets will result in a heavy
demand on system and possibly network resources. Rather than
retrieving all of the data and manipulating it in PHP, consider using
the database server to manipulate the result sets. For example, use
the WHERE and ORDER BY clauses in SQL to restrict results before
retrieving and processing them with PHP.
Uniform array structure:
Is there a special reason to build your resulting array to have a not uniform structure (regarding interests)? Wouldn't it be better to uniformize the array structure? See my results in PHP after post-processing, to understand what I mean vs. the structure you requested.
Code version:
I've prepared a php version - not OOP for this problem - of the data fetching and array building steps. I've commented it and also displayed the data source on which I was testing. In the end I'll also present the results. The steps of building the final array ($personDetails) are straightforward: loop through the fetched data and transfer it only (!) if not already.
Mandatory aliases for same columns from different tables:
I tried to fetch all dress and interest data at once (using wild cards) like this:
SELECT d.*, i.* FROM ...
I ran some tests in PHP and tried some coding options, but, in the end, I concluded: it's impossible to process the feched data in a way like this:
$fetchedData = $statement->fetchAll(PDO::FETCH_ASSOC);
foreach ($fetchedData as $key => $record) {
$dressId = $record['d.id'];
$interestId = $record['i.id'];
//...
}
PHP have not assigned different items in the $record array for the two id columns, whatever I tried. The only one assigned item always corresponds to the last id column in the columns list. So, for a correct output, it's a mandatory task to skip using the wild-cards and to alias all columns having the same name and residing in different tables. Like this:
SELECT d.id AS dress_id, i.id AS interest_id FROM ...
... and the php code:
$fetchedData = $statement->fetchAll(PDO::FETCH_ASSOC);
foreach ($fetchedData as $key => $record) {
$dressId = $record['dress_id'];
$interestId = $record['interest_id'];
//...
}
I'll be honest: even if this situation is somehow intuitiv, I never tested it. I've always used aliasing for the columns with the same names, but now I have the certitude given by the on-code tests too.
Address array item by key vs. search for array item key:
The resulting array ($personDetails) holds the fetched data as follows: each person's id is the KEY of the corresponding details item. Why I did (and recommend) this? Because you may want to directly read a person from the array by just passing the needed id. It's better to address an array item by its - unique - key than to search for it in the whole array.
Oh, almost forgotten: I ran the example on two persons, with different db entries/record numbers.
Good luck.
The code:
Tested on the following tables:
Results of running the query in db editor:
Fetch and process db data in PHP (read_person_details.php):
<?php
// Db configs.
define('HOST', 'localhost');
define('PORT', 3306);
define('DATABASE', 'db');
define('USERNAME', 'user');
define('PASSWORD', 'pass');
define('CHARSET', 'utf8');
/*
* Error reporting.
* To do: define an error handler, an exception handler and a shutdown
* handler function to handle the raised errors and exceptions.
*
* #link http://php.net/manual/en/function.error-reporting.php
*/
error_reporting(E_ALL);
ini_set('display_errors', 1); // SET IT TO 0 ON A LIVE SERVER!
/*
* Create a PDO instance as db connection to db.
*
* #link http://php.net/manual/en/class.pdo.php
* #link http://php.net/manual/en/pdo.constants.php
* #link http://php.net/manual/en/pdo.error-handling.php
* #link http://php.net/manual/en/pdo.connections.php
*/
$connection = new PDO(
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=%s', HOST, PORT, DATABASE, CHARSET)
, USERNAME
, PASSWORD
, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => FALSE,
PDO::ATTR_PERSISTENT => TRUE,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
// Person ID's to fetch.
$personId1 = 1;
$personId2 = 2;
/*
* The SQL statement to be prepared. Notice the so-called named markers.
* They will be replaced later with the corresponding values from the
* bindings array when using PDOStatement::bindValue.
*
* When using named markers, the bindings array will be an associative
* array, with the key names corresponding to the named markers from
* the sql statement.
*
* You can also use question mark markers. In this case, the bindings
* array will be an indexed array, with keys beginning from 1 (not 0).
* Each array key corresponds to the position of the marker in the sql
* statement.
*
* #link http://php.net/manual/en/mysqli.prepare.php
*/
$sql = 'SELECT
p.id AS person_id,
d.id AS dress_id,
d.name AS dress_name,
d.produced_in AS dress_produced_in,
i.id AS interest_id,
i.interest,
i.priority AS interest_priority
FROM person AS p
LEFT JOIN dress AS d ON d.person_id = p.id
LEFT JOIN interest AS i ON i.person_id = p.id
WHERE
p.id = :personId1 OR
p.id = :personId2
ORDER BY
person_id ASC,
dress_name ASC,
interest ASC';
/*
* The bindings array, mapping the named markers from the sql
* statement to the corresponding values. It will be directly
* passed as argument to the PDOStatement::execute method.
*
* #link http://php.net/manual/en/pdostatement.execute.php
*/
$bindings = [
':personId1' => $personId1,
':personId2' => $personId2,
];
/*
* Prepare the sql statement for execution and return a statement object.
*
* #link http://php.net/manual/en/pdo.prepare.php
*/
$statement = $connection->prepare($sql);
/*
* Execute the prepared statement. Because the bindings array
* is directly passed as argument, there is no need to use any
* binding method for each sql statement's marker (like
* PDOStatement::bindParam or PDOStatement::bindValue).
*
* #link http://php.net/manual/en/pdostatement.execute.php
*/
$executed = $statement->execute($bindings);
/*
* Fetch data (all at once) and save it into $fetchedData array.
*
* #link http://php.net/manual/en/pdostatement.fetchall.php
*/
$fetchedData = $statement->fetchAll(PDO::FETCH_ASSOC);
// Just for testing. Display fetched data.
echo '<pre>' . print_r($fetchedData, TRUE) . '</pre>';
/*
* Close the prepared statement.
*
* #link http://php.net/manual/en/pdo.connections.php Example #3 Closing a connection.
*/
$statement = NULL;
/*
* Close the previously opened database connection.
*
* #link http://php.net/manual/en/pdo.connections.php Example #3 Closing a connection.
*/
$connection = NULL;
// Filter the fetched data.
$personDetails = [];
foreach ($fetchedData as $key => $record) {
$personId = $record['person_id'];
$dressId = $record['dress_id'];
$dressName = $record['dress_name'];
$dressProducedIn = $record['dress_produced_in'];
$interestId = $record['interest_id'];
$interest = $record['interest'];
$interestPriority = $record['interest_priority'];
// Check and add person id as key.
if (!array_key_exists($personId, $personDetails)) {
$personDetails[$personId] = [
'dresses' => [],
'interests' => [],
];
}
// Check and add dress details.
if (!array_key_exists($dressId, $personDetails[$personId]['dresses'])) {
$personDetails[$personId]['dresses'][$dressId] = [
'name' => $dressName,
'producedIn' => $dressProducedIn,
// ... (other fetched dress details)
];
}
// Check and add interest details.
if (!array_key_exists($interestId, $personDetails[$personId]['interests'])) {
$personDetails[$personId]['interests'][$interestId] = [
'interest' => $interest,
'interestPriority' => $interestPriority,
// ... (other fetched interest details)
];
}
}
// Just for testing. Display person details list.
echo '<pre>' . print_r($personDetails, TRUE) . '</pre>';
Fetched results in PHP code:
Fetched data ($fetchedData) of two persons:
Array
(
[0] => Array
(
[person_id] => 1
[dress_id] => 1
[dress_name] => shorts
[dress_produced_in] => Taiwan
[interest_id] => 2
[interest] => basketball
[interest_priority] => 2
)
[1] => Array
(
[person_id] => 1
[dress_id] => 1
[dress_name] => shorts
[dress_produced_in] => Taiwan
[interest_id] => 1
[interest] => football
[interest_priority] => 1
)
[2] => Array
(
[person_id] => 1
[dress_id] => 2
[dress_name] => tshirt
[dress_produced_in] => USA
[interest_id] => 2
[interest] => basketball
[interest_priority] => 2
)
[3] => Array
(
[person_id] => 1
[dress_id] => 2
[dress_name] => tshirt
[dress_produced_in] => USA
[interest_id] => 1
[interest] => football
[interest_priority] => 1
)
[4] => Array
(
[person_id] => 2
[dress_id] => 3
[dress_name] => yellow hat
[dress_produced_in] => England
[interest_id] => 4
[interest] => films
[interest_priority] => 1
)
[5] => Array
(
[person_id] => 2
[dress_id] => 3
[dress_name] => yellow hat
[dress_produced_in] => England
[interest_id] => 5
[interest] => programming
[interest_priority] => 1
)
[6] => Array
(
[person_id] => 2
[dress_id] => 3
[dress_name] => yellow hat
[dress_produced_in] => England
[interest_id] => 3
[interest] => voleyball
[interest_priority] => 3
)
)
Filtered data in PHP, e.g. the final array ($personDetails) holding the infos about two persons:
Array
(
[1] => Array
(
[dresses] => Array
(
[1] => Array
(
[name] => shorts
[producedIn] => Taiwan
)
[2] => Array
(
[name] => tshirt
[producedIn] => USA
)
)
[interests] => Array
(
[2] => Array
(
[interest] => basketball
[interestPriority] => 2
)
[1] => Array
(
[interest] => football
[interestPriority] => 1
)
)
)
[2] => Array
(
[dresses] => Array
(
[3] => Array
(
[name] => yellow hat
[producedIn] => England
)
)
[interests] => Array
(
[4] => Array
(
[interest] => films
[interestPriority] => 1
)
[5] => Array
(
[interest] => programming
[interestPriority] => 1
)
[3] => Array
(
[interest] => voleyball
[interestPriority] => 3
)
)
)
)

MySQL (or any other SQL database) does not return results in the nested array format that you describe. So you're going to have to write application code to process the result of the query one way or another.
Writing multiple joins like you have is bound to create a Cartesian product between the joined tables, and this will multiply the size of the result set, if any of them match multiple rows.
I recommend you run a separate query for each type of dependent information, and combine them in application code. Here's an example:
function get_details($pdo, $person_id) {
$sql = "
select p.id, d.name
from person as p
left join dress as d on p.id = d.person_id
where p.id = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$person_id]);
$rows = $stmt->fetchAll();
foreach ($rows as $row) {
if (!isset($data[$row['id']])) {
$data[$row['id']] = [
'id' => $row['id'],
'dress' => []
];
}
$data[$row['id']]['dress'][] = $row['name'];
}
$sql = "
select p.id, i.interest
from person as p
left join interest as i on p.id = i.person_id
where p.id = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$person_id]);
$rows = $stmt->fetchAll();
foreach ($rows as $row) {
if (!isset($data[$row['id']])) {
$data[$row['id']] = [
'id' => $row['id'],
'interest' => []
];
}
$data[$row['id']]['interest'][] = $row['interest'];
}
return $data;
}
I tested this by calling it in the following way:
$pdo = new PDO("mysql:host=127.0.0.1;dbname=test", "xxxx", "xxxxxxxx");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$result = get_details($pdo, 1);
print_r($result);
Output:
Array
(
[1] => Array
(
[id] => 1
[dress] => Array
(
[0] => shorts
[1] => tshirt
)
[interest] => Array
(
[0] => football
[1] => basketball
)
)
)
Re your comment:
I can't guarantee which method will have better performance. That depends on several other factors, for example the number of rows you need to query, the speed of creating temp tables needed for GROUP_CONCAT() solutions, the network speed of transferring large result sets containing duplicates, and so on.
As with all performance-related questions, the ultimate answer is that you need to test with your data on your server.

What about using a UNION
(
SELECT p.id, d.id AS type_id, d.name, 'dress' AS `type`
FROM person AS p
LEFT JOIN dress AS d ON p.id = person_id
WHERE p.id = 1
)
UNION
(
SELECT p.id, i.id AS type_id , i.interest AS NAME, 'interest' AS `type`
FROM person AS p
LEFT JOIN interest AS i ON p.id = person_id
WHERE p.id = 1
)

You just use group by person id and group_concat and Add distinct on dress and interest otherwise you will get result with duplicate dress and interest.
Query:
select p.id, p.name, group_concat(distinct i.interest) as interests,group_concat(distinct d.name) as dresses
from person as p left
join dress as d on p.id = d.person_id
left join interest as i on p.id = i.person_id
where p.id = 1 group by p.id;
so you will get comma separated interest and dress
Output:
+----+------+---------------------+---------------+
| id | name | interests | dresses |
+----+------+---------------------+---------------+
| 1 | Sam | football,basketball | shorts,tshirt |
+----+------+---------------------+---------------+

Couple of basic ways to do this:
COLLECT ALL INFORMATION AT ONCE
As suggested by #aendeerei, expanding your query:
SELECT p.id AS p_id,
p.name AS p_name,
d.id AS d_id,
d.name AS d_name,
i.id AS i_id,
i.name AS i_name
FROM person as p
LEFT JOIN dress as d on p.id = d.person_id
LEFT JOIN interest as i on p.id = i.person_id
WHERE p.id = 1;
Then in the application code:
$person = [];
foreach ($rows as $row) {
$person['id'] = $row['p_id'];
$person['name'] = $row['p_name'];
if($row['d_id']){
$person['dresses'][$row['d_id']] = [
'id' => $row['d_id'],
'name' => $row['d_name'],
]
}
if($row['i_id']){
$person['interests'][$row['i_id']] = [
'id' => $row['i_id'],
'name' => $row['i_name'],
]
}
}
When you index the dress and interest arrays by their respective IDs, any duplicate data just overwrites the same index. Overwriting could also be avoided with some if(array_key_exists(...)) conditionals.
This idea could be expanded to multiple persons in a $persons array, by indexing each person by their own id.
The downside here is that when people have large numbers of dresses and interests you return a lot of redundant data.. (5 dresses and 5 interests for a person will return their name 25 times).
COLLECT DEPENDENT DATA SEPARATELY
Or as suggested by #BillKarwin, you could run a separate query for each table. I think I'd even be tempted to go one further and separate the person table as well.
SELECT * FROM person WHERE id = 1;
Build person array from single row returned
SELECT * FROM dress WHERE person_id = 1;
Build person's dress array from returned rows if any.
SELECT * FROM interest WHERE person_id = 1;
Build person's interest array from returned rows if any.
This could be expanded to multiple persons by using WHERE person_id IN (...) on the dependent queries using the ids of persons found in the first.
The downside to this is you are running 3 different queries, which could take longer and adds complexity.. and if someone deletes a person in between, you may have some minor concurrency issues to worry about. It could appear that a deleted person still exists, but with no dresses/interests.

Related

How to query employee details and relate their performance metrics?

I am fetching the id, first name, and last name of all employees that are approved and not archived. Then I am looping these results and using the ids to query other tables to collect some count data.
I tried the below code, but I am not getting the expected output.
$queryEmp = "
SELECT id, firstname, lastname
FROM tbl_employee as e
WHERE is_archive=0 and is_approved=1
";
$getQuery= $this->db->query($queryEmp);
$result= $getQuery->result();
foreach ($result as $key=> $value) {
//echo "<pre>";
print_r($value);
$day = "MONTH(date_of_created) = DATE(CURRENT_DATE())";
$group = "f_id IN (SELECT MAX(f_id) FROM tbl_fileStatus GROUP BY f_bankid)";
$condiion = "and ba.createdby='" . $value->id . "' and " . $day ." and " . $group;
$query2 = "
select
(SELECT COUNT(c_id)
FROM tbl_lead
WHERE leadstatus='1' AND ".$day.") as confirmCount,
(SELECT COUNT(f_id)
FROM tbl_fileStatus as fs
join tbl_bankdata as ba on ba.bank_id=fs.f_bankid
WHERE fs.f_filestatus=1 " . $condiion . ") as disbursed,
(SELECT COUNT(f_id)
FROM tbl_fileStatus as fs
join tbl_bankdata as ba on ba.bank_id=fs.f_bankid
WHERE fs.f_filestatus=2 ".$condiion.") as filesubmit
";
# code...
$getQuery2= $this->db->query($query2);
$result2[]=$getQuery2->result();
}
echo "<pre>";
print_r(result2);
$result looks like this:
Array (
[0] => stdClass Object (
[id] => 1
[firstname] => xyz
[lastname] => xyz
)
...
)
Second query output:
Array (
[0] => Array (
[0] => stdClass Object (
[fallowCall] => 0
[confirmCount] => 0
[disbursed] => 0
[filesubmit] => 0
)
)
...
)
How can I produce the correct results which relate respective employees with with their performance metrics? Either this structure:
Array (
[0] => stdClass Object (
[id] => 1
[firstname] => xyz
[lastname] => xyz
[somename] => (
[fallowCall] => 0
[confirmCount] => 0
[disbursed] => 0
[filesubmit] => 0
)
)
...
)
Or this structure:
Array (
[0] => stdClass Object (
[id] => 1
[firstname] => xyz
[lastname] => xyz
[fallowCall] => 0
[confirmCount] => 0
[disbursed] => 0
[filesubmit] => 0
)
...
)
I have added the my table structure and some sample data here: https://www.db-fiddle.com/f/8MoWmKPuzTrrC3DQJsiX35/0
some notes here
1) createdby is the id of table tbl_employee
2) lead_id in the bank table is the c_id of the table tbl_lead
3) f_bankid in the tbl_fileStatus is the bank_id of the table tbl_bankdata
There is actually no need to create the additional depth/complexity just to hold the count data. Furthermore, by using a combination of LEFT JOINs to connect the related tables and apply your required conditional rules, you can achieve your desired result by making just one trip to the database. This will without question provide superior efficiency for your application. LEFT JOINs are important to use so that counts can be zero without excluding employees from the result set.
Also, I should point out that your attempted query was mistakenly comparing a MONTH() value against a DATE() value -- that was never going to end well. :) In fact, to ensure that your sql is accurately isolating the current month from the current year, you need to be also checking the YEAR value.
My recommended sql:
SELECT
employees.id,
employees.firstname,
employees.lastname,
COUNT(DISTINCT leads.c_id) AS leadsThisMonth,
SUM(IF(fileStatus.f_filestatus = 1, 1, 0)) AS disbursedThisMonth,
SUM(IF(fileStatus.f_filestatus = 2, 1, 0)) AS filesubmitThisMonth
FROM tbl_employee AS employees
LEFT JOIN tbl_lead AS leads
ON employees.id = leads.createdby
AND leadstatus = 1
AND MONTH(leads.date_of_created) = MONTH(CURRENT_DATE())
AND YEAR(leads.date_of_created) = YEAR(CURRENT_DATE())
LEFT JOIN tbl_bankdata AS bankData
ON employees.id = bankData.createdby
LEFT JOIN tbl_fileStatus AS fileStatus
ON bankData.bank_id = fileStatus.f_bankid
AND MONTH(fileStatus.date_of_created) = MONTH(CURRENT_DATE())
AND YEAR(fileStatus.date_of_created) = YEAR(CURRENT_DATE())
AND fileStatus.f_id = (
SELECT MAX(subFileStatus.f_id)
FROM tbl_fileStatus AS subFileStatus
WHERE subFileStatus.f_bankid = bankData.bank_id
GROUP BY subFileStatus.f_bankid
)
WHERE employees.is_archive = 0
AND employees.is_approved = 1
GROUP BY employees.id, employees.firstname, employees.lastname
The SUM(IF()) expression is a technique used to execute a "conditional count". "Aggregate data" is formed by using GROUP BY and there are specialized "aggregate functions" which must be used to create linear/flat data from these clusters/non-flat collections of data. fileStatus data is effectively piled up upon itself due to the GROUP BY call. If COUNT(fileStatus.f_filestatus) was called, it would count all of the rows in the cluster. Since you wish to differentiate between f_filestatus = 1 and f_filestatus = 2, an IF() statement is used. This is doing the same thing as COUNT() (adding 1 for every qualifying occurrence), but it is different from COUNT() in that it does not count specific rows (within the scope of the cluster) unless the IF() expression is satisfied. Another example.
Here is a db fiddle demo with some adjustments to your supplied sample data: https://www.db-fiddle.com/f/8MoWmKPuzTrrC3DQJsiX35/4 (The result set will only be "good" while the current is June of this year.)
After saving the above string as $sql, you can simply execute it and loop through the array of objects like this:
foreach ($this->db->query($sql)->result() as $object) {
// these are the properties available in each object
// $object->id
// $object->firstname
// $object->lastname
// $object->leadsThisMonth
// $object->disbursedThisMonth
// $object->filesubmitThisMonth
}

Pushing pointers to followers with the metadata (MySQL Query)

I’ve seen the following question on StackOverflow, Intelligent MySQL GROUP BY for Activity Streams posted by Christian Owens 12/12/12.
So I decided to try out the same approach, make two tables similar to those of his. And then I pretty much copied his query which I do understand.
This is what I get out from my sandbox:
Array
(
[0] => Array
(
[id] => 0
[user_id] => 1
[action] => published_post
[object_id] => 776286559146635
[object_type] => post
[stream_date] => 2015-11-24 12:28:09
[rows_in_group] => 1
[in_collection] => 0
)
)
I am curious, since looking at the results in Owens question, I am not able to fully get something, and does he perform additional queries to grab the actual metadata? And if yes, does this mean that one can do it from that single query or does one need to run different optimized sub-queries and then loop through the arrays of data to render the stream itself.
Thanks a lot in advanced.
Array
(
[0] => Array
(
[id] => 0
[user_id] => 1
[fullname] => David Anderson
[action] => hearted
[object_id] => array (
[id] => 3438983
[title] => Grand Theft Auto
[Category] => Games
)
[object_type] => product
[stream_date] => 2015-11-24 12:28:09
[rows_in_group] => 1
[in_collection] => 1
)
)
In "pseudo" code you need something like this
$result = $pdo->query('
SELECT stream.*,
object.*,
COUNT(stream.id) AS rows_in_group,
GROUP_CONCAT(stream.id) AS in_collection
FROM stream
INNER JOIN follows ON stream.user_id = follows.following_user
LEFT JOIN object ON stream.object_id = object.id
WHERE follows.user_id = '0'
GROUP BY stream.user_id,
stream.verb,
stream.object_id,
stream.type,
date(stream.stream_date)
ORDER BY stream.stream_date DESC
');
then parse the result and convert it in php
$data = array(); // this will store the end result
while($row = $result->fetch(PDO::FETCH_ASSOC)) {
// here for each row you get the keys and put it in a sub-array
// first copy the selected `object` data into a sub array
$row['object_data']['id'] = $row['object.id'];
$row['object_data']['title'] = $row['object.title'];
// remove the flat selected keys
unset($row['object.id']);
unset($row['object.title']);
...
$data[] = $row; // move to the desired array
}
you should get
Array
(
[0] => Array
(
[id] => 0
[user_id] => 1
[fullname] => David Anderson
[verb] => hearted
[object_data] => array (
[id] => 3438983
[title] => Grand Theft Auto
[Category] => Games
)
[type] => product
[stream_date] => 2015-11-24 12:28:09
[rows_in_group] => 1
[in_collection] => 1
)
)
It seems that you want a query where you can return the data you're actually able to get plus the user fullname and the data related to the object_id.
I think that the best effort would be to include some subqueries in your query to extract these data:
Fullname: something like (SELECT fullname FROM users WHERE id = stream.user_id) AS fullname... or some modified version using the stream.user_id, as we can't identify in your schema where this fullname comes from;
Object Data: something like (SELECT CONCAT_WS(';', id, title, category_name) FROM objects WHERE id = stream.object_id) AS object_data. Just as the fullname, we can't identify in your schema where these object data comes from, but I'm assuming it's an objects table.
One object may have just one title and may have just one category. In this case, the Object Data subquery works great. I don't think an object can have more than one title, but it's possible to have more than one category. In this case, you should GROUP_CONCAT the category names and take one of the two paths:
Replace the category_name in the CONCAT_WS for the GROUP_CONCAT of all categories names;
Select a new column categories (just a name suggestion) with the subquery which GROUP_CONCAT all categories names;
If your tables were like te first two points of my answer, a query like this may select the data, just needing a proper parse (split) in PHP:
SELECT
MAX(stream.id) as id,
stream.user_id,
(select fullname from users where id = stream.user_id) as fullname,
stream.verb,
stream.object_id,
(select concat_ws(';', id, title, category_name) from objects where id = stream.object_id) as object_data,
stream.type,
date(stream.stream_date) as stream_date,
COUNT(stream.id) AS rows_in_group,
GROUP_CONCAT(stream.id) AS in_collection
FROM stream
INNER JOIN follows ON 1=1
AND stream.user_id = follows.following_user
WHERE 1=1
AND follows.user_id = '0'
GROUP BY
stream.user_id,
stream.verb,
stream.object_id,
stream.type,
date(stream.stream_date)
ORDER BY stream.stream_date DESC;
In ANSI SQL you can't reference columns not listed in your GROUP BY, unless they're in aggregate functions. So, I included the id as an aggregation.

PHP PDO LEFT JOIN Multidimensional Array

Basically, I have three tables. I have a projects table, a questions table and an answers table. A project can have many questions and a question can have many answers. Using PDO's and LEFT JOIN ON the Question ID, how can I turn comments with answers into a multidimensional array so that the structure would look like this:
[Question] => Array
(
[id] => 1
[question] => 'Random Question'
[askedBy] => 123
[answer] => Array
(
[0] => Array
(
[id] => 1
[answer] => 'An Answer'
[answeredBy] => 123
)
[1] => Array
(
[id] => 1
[answer] => 'Another Answer'
[answeredBy] => 123
)
)
)
Finalized Code (which is returning what I want)
$questions = array();
$questionCounter = 0;
$questionID = NULL;
$STH = $DBH->query("SELECT `project_question`.`id` AS question_id, `project_question`.`question`, `project_question`.`userID` AS askedBy,
`project_question`.`created` AS question_created, `project_answer`.`id` AS answer_id,
`project_answer`.`answer`, `project_answer`.`userID` AS answeredBy,
`project_answer`.`accepted`, `project_answer`.`created` AS answer_created
FROM `project_question`
LEFT JOIN `project_answer`
ON `project_question`.`id` = `project_answer`.`questionID`
WHERE `project_question`.`projectID` = $args[0]
AND `project_question`.`projectPhase` = 2");
while($row = $STH->fetch(PDO::FETCH_ASSOC)){
if($row['question_id'] !== $questionID){
$questions[$questionCounter] = array(
'id' => $row['question_id'],
'question' => $row['question'],
'userID' => $row['askedBy'],
'created' => $row['question_created'],
'answers' => array()
);
array_push($questions[$questionCounter]['answers'],
array(
'id' => $row['answer_id'],
'answer' => $row['answer'],
'userID' => $row['answeredBy'],
'accepted' => $row['accepted'],
'created' => $row['answer_created']
));
$questionCounter++;
$questionID = $row['question_id'];
} else {
array_push($questions[$questionCounter - 1]['answers'],
array(
'id' => $row['answer_id'],
'answer' => $row['answer'],
'userID' => $row['answeredBy'],
'accepted' => $row['accepted'],
'created' => $row['answer_created']
));
}
}
In one query, you can request all answers, join questions to them, and join projects to questions.
Create empty array $projects = array()
Iterate through every resulting row foreach ($rows as $row)
Add project with its data to array, if it does not exist already (use project id as key) if (!isset($projects[$row->project_id])) { $projects[$row->project_id] = array('col1' => $row->col1,'questions' => array()); }
Add question with its data to project questions array, if it does not exist already (use question id as key) if (!isset($projects[$row->project_id]['questions'][$row->question_id])) { $projects[$row->project_id]['questions'][$row->question_id] = array('col2' => $row->col2,'answers' => array()); }
Add answer with its data to project question answers array (use answer id as key) $projects[$row->project_id]['questions'][$row->question_id]['answers'][$row->answer_id] = array('col3' => $row->col3);
But as you can understand, you will get many useless information in that query. You could go with one query, getting projects only, iterate through them, in each cycle add data to array and query again to get questions that has specific project_id, iterate through them, in each cycle add data to array and query again to get answers that has specific question_id, iterate through them, and add data to array.
But if I rethink this, I think MySQL will work faster than PHP, even with returning bunch of useless data (I mean, repeating data about projects and questions) and 1 query vs possible 50 queries from one client is much better, so I suggest better use first method.
Without further information about your database table and columns, you wont get from me anything more than pseudo-code-algorithm.
EDIT: Possible MySQL select query:
SELECT
a.id answer_id, a.answer, a.answeredBy,
q.id question_id, q.question, q.askedBy,
p.id project_id, p.title
FROM answer a
LEFT JOIN question q ON q.id = a.question_id
LEFT JOIN project p ON p.id = q.project_id
For table structure
project -> | id | title |
question -> | id | question | askedBy | project_id |
answer -> | id | answer | answeredBy | question_id |

Sort SQL JOIN results in PHP arrays

I got 2 relational tables:
table "categories"
id int(11)
title varchar(255)
table "posts"
id int(11)
title varhcar(255)
category_id int(11) // foreign key
If I select the "categories" table, I would like to get a PHP array with al the categories (as in "SELECT * categories") but including an inner array with all its posts:
Array (
/* first category */
[0] = Array (
[id] => 1
[title] => "Rock"
/* all its posts */
[posts] => Array (
[0] = Array(
[id] = 100
[title] = "Rock post title"
[category_id] = 1
)
[1] = Array(
[id] = 101
[title] = "Other rock post title"
[category_id] = 1
)
)
/* second category */
[1] = Array (
)
/* ... */
)
If I just made a "join" query I get all the results combined, something like:
id title id title category_id
1 Rock 100 "Rock post title" 1
2 Rock 101 "Other rock post" 1
3 Rock 102 "Final rock post" 1
I don't want to make multiple queries, because I think is inefficient.
Is there anyway to achive the desire result with one query?
I know CakePHP manage to return relational tables results in this format, so I'm looking to achieve the same result.
The join should look something like:
select c.id, c.title, p.id, p.title, p.category_id
from categories c, posts p
where c.id = p.category_id
order by c.id, p.id
First, if you want this functionality consider using an ORM library (such as what CakePHP and other frameworks provide) rather than rolling your own code for a problem that's already been solved.
You cannot do 'inner arrays' in SQL, not without great ugliness (like packing records into a string column then unpacking them in PHP).
But for a quick 'n dirty solution, just use your original join query as long as you rename the post id and title (eg, 'post_id') in the query to avoid confusion with category id. Then loop over the resultset and build your array.
$newArray = array();
foreach($resultset as $row) {
if(!array_key_exists($row['category_id'],$newArray)) {
$newArray[$row['category_id']] = array('id' => $row['category_id'], 'title' => $row['title'], 'posts' => array());
}
$newArray[$row['category_id']]['posts'] = array('id' => $row['post_id'], 'title' => $row['post_title'], 'category_id' => $row['category_id']);
}
I didn't write this code in an editor so I apologize for typos. You get the general idea.

Simple Relational Database - Efficient Querying

This is essentially very simple I've just been rather verbose about it. My data is structured as shown at the bottom of my post. My goal is to organise the data in a simple spreadsheet format, 'flattening' the relational database. Something like:
Product1 URL
Product2 URL URL URL
Product3 URL
My initial approach is as below. The reason I'm looking for an alternative is I have > 10,000 rows in the products table and each product has an average of 5 URLs. This generates an enourmous amount of MySQL queries (in fact exceeding my shared hosting resources), there must be a better way, right? My code has been simplified to distill the essence of my question.
My Approach
//Query to get all products
$products = mysql_query("SELECT * FROM products");
$i = 0;
//Iterate through all products
while($product = mysql_fetch_array($products)){
$prods[$i]['name'] = $product['name'];
$prods[$i]['id'] = $product['id'];
//Query to get all URLs associated with this particular product
$getUrls = mysql_query("SELECT * FROM urls WHERE product_id =".$product['id']);
$n = 0;
while($url = mysql_fetch_array($getUrls)){
$prods[$i]['urls'][$n] = $url['url'];
}
}
The Result
To be clear, this is the intended result, the output was expected. I just need a more efficient way of getting from the raw database tables to this multidimensional array. I suspect there's more efficient ways of querying the database. Also, I understand that this array uses a lot of memory when scaled up to the required size. As my aim is to use this array to export the data to .csv I would consider using XML (or something) as an intermediary if it is more efficient. So feel free to suggest a different approach entirely.
//The result from print_r($prods);
Array
(
[0] => Array
(
[name] => Swarovski Bracelet
[id] => 1
[urls] => Array
(
[0] => http://foo-example-url.com/index.php?id=7327
)
)
[1] => Array
(
[name] => The Stranger - Albert Camus
[id] => 2
[urls] => Array
(
[0] => http://bar-example-url.com/index.php?id=9827
[1] => http://foo-foo-example-url.com/index.php?id=2317
[2] => http://baz-example-url.com/index.php?id=5644
)
)
)
My Database Schema
Products
id product_name
1 Swarovski Bracelet
2 The Stranger - Albert Camus
3 Finnegans Wake - James Joyce
URLs
id product_id url price
1 1 http://foo-example-url.com/index.php?id=7327 £16.95
2 2 http://bar-example-url.com/index.php?id=9827 £7.95
3 2 http://foo-foo-example-url.com/index.php?id=2317 £7.99
4 2 http://baz-example-url.com/index.php?id=5644 £6.00
5 3 http://bar-baz-example-url.com/index.php?id=9534 £11.50
URLs.product_id links to products.id
If you've read this, thank you for your incredible patience! I look forward to reading your response. So how can I do this properly?
The SQL
SELECT p.id, p.product_name, u.url
FROM products p, urls u
WHERE p.id = u.product_id ORDER BY p.id
PHP code to "flatten" the urls, I have just seperated the urls with a space.
$i = 0;
$curr_id;
while($product = mysql_fetch_array($products))
{
$prods[$i]['name'] = $product['product_name'];
$prods[$i]['id'] = $product['id'];
if($product['id'] == $curr_id)
{
$prods[$i]['urls'] .= " ".$url['url'];
}
else
{
$prods[$i]['urls'] = $url['url'];
}
$i++;
$curr_id = $product['id'];
}
The Result
[0] => Array
(
[name] => Swarovski Bracelet
[id] => 1
[urls] => http://foo-example-url.com/index.php?id=7327
)
[1] => Array
(
[name] => The Stranger - Albert Camus
[id] => 2
[urls] => http://bar-example-url.com/index.php?id=9827 http://foo-foo-example-url.com/index.php?id=2317 http://baz-example-url.com/index.php?id=5644
)
You are able to pull back that exact information with one query even if you want to pull back the csv of urls.
SELECT
p.id, p.product_name,
GROUP_CONCAT(u.url) AS URLS
FROM products p LEFT JOIN urls u
ON p.id = u.product_id
GROUP BY p.id, p.product_name
And then you have 1 query to your database that brings back what you need.
Or the cleaner approach is how bruce suggested and then let PHP merge all of the results together.
You can accomplish this in one query:
select * from products join urls on products.id = urls.product_id order by products.id
pseudocode:
while(query_result) {
if (oldpid == pid) { print ", \"url\"" }
else { print "pid, \"url\"" }
oldpid = pid;
}

Categories