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.
Related
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.
I am developing a website on wordpress that manages events.
The issue is that I need to get some event title names from their corresponding IDs in a single query. I am trying to get multiple rows out of a single MYSQL query, which works fine using "WHERE IN ({$IDs})":
$events_implode = implode(',',$events);
$events_titles = $wpdb->get_results("SELECT `title` FROM `wp_events` WHERE `id` in ({$events_implode}) ORDER BY FIELD(id,{$events_implode}",ARRAY_A);
However, I need it to return a value/string if one of the $IDs was not found in the query instead of returning nothing.
Example:
If $events_implode is like: 318,185,180,377, and the events with IDs 180 & 185 do not exist (being deleted for instance), I get back the events titles for 318 & 377 only:
Array
(
[0] => Array
(
[title] => Title 1
)
[1] => Array
(
[title] => Title 4
)
)
while I need it to return something like:
Array
(
[0] => Array
(
[title] => Title 1
)
[3] => Array
(
[title] => Title 4
)
)
I tried IFNULL:
$events_titles = $wpdb->get_results("SELECT IFNULL( (SELECT `title` FROM `wp_events` WHERE `id` in ({$events_implode}) ORDER BY FIELD(id,{$events_implode}) ),'not found')",ARRAY_A);
but since the query returns more than one row, I get the error message:
"mysqli_query(): (21000/1242): Subquery returns more than 1 row"
Is there a solution to that?
Many thanks!
You could achieve this by also querying the id and not just the title:
SELECT id, title FROM ... etc
Then your result array will look like this (for the example data)
array(
0 => array("id" => 318, "title" => "title for 318"),
1 => array("id" => 377, "title" => "title for 377")
)
But this can be converted to what you need, with the help of the $events array you already have:
foreach($event_titles as $row) {
$hash[array_search($row['id'], $events)] = $row['title'];
};
$events_titles = $hash;
Now the array looks like this (for the example data):
array(
0 => array("title" => "title for 318"),
3 => array("title" => "title for 377")
)
NB: That conversion can also be done without explicit loop:
$ids = array_intersect($events, array_column($event_titles, 'id'));
$event_titles = array_combine(array_keys($ids), array_column($event_titles, 'title'));
Alternative
The above is probably the best solution for your case, but you could also build your query dynamically to achieve this:
$subsql = implode(
' UNION ALL ',
array_map(function ($event) { return "SELECT $event AS id"; }, $events)
);
$sql = "SELECT wp_events.title
FROM ($subsql) AS eid
LEFT JOIN wp_events ON wp_events.id = eid.id
ORDER BY eid.id";
$events_titles = $wpdb->get_results($sql, ARRAY_A);
The value for $subsql will be like this (for example data):
SELECT 318 AS id UNION ALL
SELECT 185 AS id UNION ALL
SELECT 180 AS id UNION ALL
SELECT 377 AS id
The value of $events_titles will be an array that has one entry for each event, whether or not it matched. So for the example you gave, you would get this result:
array(
0 => array("title" => "title for 318"),
1 => array("title" => null), // because no match with 185
2 => array("title" => null), // because no match with 180
3 => array("title" => "title for 377")
)
So you can now just test for null. If additionally you would like to remove the null entries without changing the indexes (0 ... 3) in the array, then do this:
$event_titles = array_filter($event_titles, function ($title) { return $title; });
This will give you (in the example):
array(
0 => array("title" => "title for 318"),
3 => array("title" => "title for 377")
)
Using in clause seems no possible but you can build a dinamic query using union form dual
"SELECT t.index, a.`title`
FROM `wp_events` as a
INNER JOIN ( select 1 as index , 318 as id
from dual
union
select 2, 185
from dual
union
select 3, 180
from dual
union
select 4, 377
from dual
) t on t.id = b.id
ORDER BY t.index"
No, there is no way. The returning array from the mysql-query has nothing to do with the implode-array you gave to the query. There might be options to achieve that (creating a temp-table, fill it with keys, left join from that table to the events-table), but please don't to that as it is bad practice and nut KISS (keep it simple, stupid).
The simple question is, why do you want to do that?
I have 3 tables: tco_articles, tco_module_eostext and tco_articles_modules. My tco_articles has unique id key. One for each article. My tco_module_eostext has unique instance_id that belongs to each article.
My tco_articles_modules contains all article_ids, but have 9 times as much instance_ids that are used in other tables.
So I can have article_id with instance_id that when you query in the tco_module_eostext will return empty.
I'd like to make a query that will return correct body text for the correct article.
So far I have:
global $wpdb;
$posts = array();
$ids = $wpdb->get_results('SELECT DISTINCT instance_id, article_id FROM tco_articles_modules', ARRAY_A);
This returns array with all the instances and ids like:
Array
(
[0] => Array
(
[instance_id] => 928615
[article_id] => 129396
)
[1] => Array
(
[instance_id] => 928616
[article_id] => 129396
)
[2] => Array
(
[instance_id] => 928617
[article_id] => 129396
)
[3] => Array
(
[instance_id] => 928618
[article_id] => 129396
)
You can see that the article_ids are the same but instance_id. When you put
$wpdb->get_results('SELECT body FROM tco_module_eostext WHERE instance_id=928617 ', ARRAY_A);
You may get empty, but for
$wpdb->get_results('SELECT body FROM tco_module_eostext WHERE instance_id=928618 ', ARRAY_A);
You could have some body text.
This is my problem. I need to go through all of them and filter out the not empty ones and assign them correct article. I managed to output the articles
foreach ($ids as $key => $value) {
$instance_ID = $value['instance_id'];
$article_ID = $value['article_id'];
$article_out = $wpdb->get_results('SELECT * FROM tco_articles WHERE id='.$article_ID.' ', ARRAY_A);
$posts[$article_ID] = $article_out[0];
}
Which returns something like:
Array
(
[129396] => Array
(
[id] => 129396
[headline] => Bla bla bla title
[intro] => This is cool article intro
[needs_review] => 0
[published] => 2014-12-16 09:17:00
[unpublished] =>
[hidden] => 0
[no_single_page] => 0
[permalink] => link-perma-link-here
[internal_slug] =>
[type_id] => 3
[thread_id] => 0
[news_id] => 0
[header_game_id] => 0
[puff_hh_id] => 0
[puff_title] =>
[hdrcol_id] => 900
[review_queued] =>
[lock_timeout] => 0
[created] => 2014-12-16 09:17:00
[updated] => 2015-01-16 13:51:30
[created_by] => 84142
[updated_by] => 84142
)
...
Now I'd like to append the body text from the tco_module_eostext table.
Is there a query I can use to do this automatically or to do this one at the time and then append to the $posts array?
The foreach method of querying is kinda slow when you have 180000+ posts.
Any help is appreciated.
If you are sure that there is always only one row in tco_module_eostext against each article_id, you can use JOIN (inner join), which will only show one row for each article_id.
SELECT a.*, t.body
FROM tco_articles a
JOIN tco_articles_modules m ON m.article_id = a.id
JOIN tco_module_eostext t ON m.instance_id = t.instance_id
//WHERE .....
But, this will not show any row of some articles if there is no entry in other two tables for that article_id. But there is still way to solve this. We can use LEFT OUTER JOIN and then make sure we only make the join if there is any row in tco_module_eostext for any instace_id. This will make sure you get at least the article info from tco_articles table when there is no data in other tables.
SELECT a.*, t.body
FROM tco_articles a
LEFT OUTER JOIN tco_articles_modules m ON m.article_id = a.id AND EXISTS ( SELECT NULL FROM tco_module_eostext WHERE instance_id = m.instance_id )
LEFT OUTER JOIN tco_module_eostext t ON m.instance_id = t.instance_id
//WHERE .....
Why not use a query with join?
Not tested!:
SELECT
article.*,
text.body as bodytext
FROM
tco_articles_modules AS modules LEFT OUTER JOIN
tco_module_eostext AS text ON
modules.instance_id = text.instance_id LEFT OUTER JOIN
tco_articles AS article ON
article.id = modules.article_id
it should get all articles with the assigned article_id from tco_articles_modules
Have a look at OUTER Join - you may want to replace this with an INNER JOIN for faster Queries. Also you ma want an WHERE condition for filtering in the query. Watch also out for the right indexing in mysql table - each joining column should be indexed - this will get much more faster results.
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.
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;
}