What is the optimized/best way to retrieve data from two tables? - php

I have two tables:
post table:
|post_id | post_title |
+--------+------------+
| 1 | Post 1 |
| 2 | Post 2 |
| 3 | Post 3 |
post_creator table:
|post_id | creator |
+--------+---------+
| 1 | John |
| 1 | Smith |
| 1 | Mike |
| 2 | Bob |
| 3 | Peter |
| 3 | Brad |
When I join these tables it looks like this.
SELECT *
FROM post p
JOIN post_creator c ON p.post_id = c.post_id
|post_id | post_title | post_id | creator|
+----------------------------------------+
| 1 | Post 1 | 1 | John |
| 1 | Post 1 | 1 | Smith |
| 1 | Post 1 | 1 | Mike |
| 2 | Post 2 | 2 | Bob |
| 3 | Post 3 | 3 | Peter |
| 3 | Post 3 | 3 | Brad |
I want to grab each post with it's creators. But in this case my joined result has same post repeated again and again because of the creator.
What I did was first I fetched all data from post table. Then I looped that result and inside the loop I fetched all creators of each posts. But in this case it query again and again for each content to get the creators.
$sql = "SELECT * FROM post";
$stmt = $conn->prepare($sql);
$stmt->execute();
$res = $stmt->fetchAll(PDO::FETCH_OBJ);
$dataObj = new stdClass;
$dataArr = [];
foreach($res as $post){
$sql = "SELECT creator FROM post_creator WHERE post_id=$post->post_id";
$stmt = $conn->prepare($sql);
$stmt->execute();
$creators = $stmt->fetchAll(PDO::FETCH_OBJ);
$dataObj->post_id = $post->post_id
$dataObj->post_title = $post->title
$dataObj->creators = $creators;
array_push($dataArr, $dataObj);
}
So finally my dataArr has this kind of a structure.
[
{
post_id: 1,
post_title: Post 1,
creators:[John, Smith, Mike]
},
{
post_id: 2,
post_title: Post 2,
creators:[Bob]
},
{
post_id: 2,
post_title: Post 1,
creators:[Peter, Brad]
},
]
This is what I wanted. Now I can loop this and render to a view.
Are there any optimized/better way to get this result without looping and querying again and again?

I think you need to use group_concat to group your creators.
SELECT p.post_id, post_title, group_concat(creator)
FROM post p
JOIN post_creator using(post_id)
group by p.post_id
Additionally, this:
$sql = "SELECT creator FROM post_creator WHERE post_id=$post->post_id";
$stmt = $conn->prepare($sql);
$stmt->execute();
is improper usage of a prepared statement. It should be written as:
$sql = "SELECT creator FROM post_creator WHERE post_id=?";
$stmt = $conn->prepare($sql);
$stmt->execute(array($post->post_id));
if it were needed, but it is not. Always bind values, never put direct to SQL.

I'd say there are 3 different roads you could follow, all of whom have some benefit or another.
Option 1. Simple SELECT query with JOIN (and overlapping rows)
This is more or less what you've already tried, with the first query you listed; which resulted in duplicate rows.
It's fairly trivial to modify your application code to deal with the dupes, and simply fold the creators into the same array/object. The overhead is almost nil as well. From a relational database design point-of-view, this method is still the best practice.
SELECT p.post_id
, p.post_title
, c.creator
FROM post p
LEFT JOIN post_creator c
ON p.post_id = c.post_id
ORDER BY p.post_id ASC
.
/* $rows = ...query...; */
$posts = [];
foreach ($rows as $row) {
if (!isset($posts[( $row['post_id'] )])) {
// this is a new post_id
$post = [];
$post['id'] = $row['post_id'];
$post['creators'] = [];
$post['creators'][] = $row['creator'];
$posts[( $row['post_id'] )] = $post;
} else {
// this is just an additional creator
$posts[( $row['post_id'] )]['creators'][] = $row['creator'];
}
}
Option 2. Multivalue columns (arrays or json)
A slightly more pragmatic solution for non-purists can be to have your query produce output columns which contain more than one value. This generally means either a JSON or an ARRAY column. The exact details depend on your choice of database system.
In either case, you'd combine it with the SQL GROUP BY feature.
Let's assume you use MySQL and prefer the JSON type; you'd then go with a query such as:
SELECT p.post_id
, p.post_title
, JSON_ARRAYAGG(c.creator) AS creators
FROM post p
LEFT JOIN post_creator c
ON p.post_id = c.post_id
GROUP BY p.post_id
ORDER BY p.post_id ASC
This way, you'll only receive one record per post, and you'll get a value such as ['Mike', 'Paul', 'Susan'] which json_decode() can turn into a proper PHP array.
Option 3. Fullblown documents
Another alternative that kind of builds upon option #2 is to go entirely with JSON, and abandon the relational recordset altogether.
Most modern DBMS have plenty of JSON functionality and the format you yourself listed as dataArr, could be fully produced by the database in response to a single SELECT query.
This way, the query would always result in just 1 row with 1 single column, which holds the entire dataArr combining all those posts (which again, can be turned into a native PHP array or object tree with json_decode, just like before).
While the result of this method can be very neat (depending on the way your application is written), some may wonder why you're using an RDBMS and not something like MongoDB.
Overall i'd recommend Option 1.

Related

SQL need to improve run speed

I am trying to select data from mysql by a date field in the database. (Users can enter start date and end date)
For each selected row between user selected dates, I need to select from the same table to produce a result.
Example:
$query = "SELECT * FROM table WHERE date BETWEEN $begindate AND $enddate"; //Select by date
$result = mysqli_query($dbc,$query);
while($row = mysqli_fetch_array($result)){
vardump($row); //user needs to see all data between date selection
$query = "SELECT * FROM table WHERE field = $row['field']";
// and then do calculations with the data
}
This runs very slowly and I can see why. How can I improve the run speed?
Edit:
The original purpose was to generate a sales report between dates. Now the user wants the report to produce another result. This result could only be produced by searching against the same table, and the rows that I need is not within the date selection.
Edit 2:
I do need to output the entire table between date selection. Each row will need to find ALL other rows where field = field, within or out side of the date selection.
Edit 3: Solved the problem. All the answers are helpful, though I think the chosen answer was most related to my question. However, I believe using join when working with two tables is the right way to go. For my problem, I actually just solved it by duplicating the table and run my search against the duplicated table. The chosen answer did not work for me because the second query selection is not a part of the first query selection. Hope this would help anyone looking at this post. Again, thanks for all the help!
Well, so if you are really looking for such a conditions in same table, I suggest you should use IN selector like following:
$query = "SELECT * FROM table
WHERE field IN
(SELECT DISTINCT field FROM table
WHERE
date BETWEEN $begindate AND $enddate)";
So final code will look some like following:
$query = "SELECT * FROM table
WHERE field IN
(SELECT DISTINCT field FROM table
WHERE
date BETWEEN $begindate AND $enddate)";
$result = mysqli_query($dbc,$query);
while($row = mysqli_fetch_array($result)){
// do calculations with the $row
}
I guess your table names arent TABLE:
just user inner join
$query = "SELECT *
FROM table1
JOIN table2
ON table1.field = table2.field
WHERE date BETWEEN $begindate AND $enddate
ORDER BY table1.field;"
Stop writing pseudo-SQL
SELECT * FROM is technically pseudo-SQL (a sql command which the interpreter has to modify before the command can be executed. It is best to get in a habit of specifying columns in the SELECT statement.
Use SQL joins
Joins are what makes relational databases so useful, and powerful. Learn them. Love them.
Your set of SQL queries, combined into a single query:
SELECT
table1.id as Aid, table1.name as Aname, table1.field as Afield,
table2.id as Bid, table2.name as Bname, table2.field
FROM table table1
LEFT JOIN table table2
ON table1.field = table2.field
WHERE table1.date BETWEEN $begindate AND $enddate
ORDER BY table1.id, table2.id
Your resulting print of the data should result in something which access each set of data akin to:
$previous_table1_id = 0;
while($row = mysqli_fetch_array($result)){
if ($row['Aid'] != $previous_table1_id) {
echo 'Table1: ' . $row['Aid'] . ' - ' . $row['Aname'] . ' - '. $row['Afield'] . "\n";
$previous_table1_id = $row['Aid'];
}
echo 'Table2: ' . $row['Bid'] . ' - ' . $row['Bname'];
}
Dealing with aggregated data
Data-aggregation (multiple matches for table1/table2 on field), is a complex subject, but important to get to know. For now, I'll leave you with this:
What follows is a simplified example of one of what aggregated data is, and one of the myriad approaches to working with it.
Contents of Table
id | name | field
--------------------
1 | foos | whoag
2 | doh | whoag
3 | rah | whoag
4 | fun | wat
5 | ish | wat
Result of query I gave you
Aid | Aname | Afield | Bid | Bname
----------------------------------
1 | foos | whoag | 1 | foos
1 | foos | whoag | 2 | doh
1 | foos | whoag | 3 | rah
2 | doh | whoag | 1 | foos
2 | doh | whoag | 2 | doh
2 | doh | whoag | 3 | rah
3 | rah | whoag | 1 | foos
3 | rah | whoag | 2 | doh
3 | rah | whoag | 3 | rah
4 | fun | wat | 4 | fun
4 | fun | wat | 5 | ish
5 | ish | wat | 4 | fun
5 | ish | wat | 5 | ish
GROUP BY example of shrinking result set
SELECT table1.id as Aid, table1.name as Aname
group_concat(table2.name) as field
FROM table table1
LEFT JOIN table table2
ON table1.field = table2.field
WHERE table1.date BETWEEN $begindate AND $enddate
ORDER BY table1.id, table2.id
GROUP BY Aid
Aid | Aname | field
----------------------------------
1 | foos | foos,doh,rah
2 | doh | foos,doh,rah
3 | rah | foos,doh,rah
4 | fun | fun, ish
5 | ish | fun, ish

MySQL Joins and COUNT() not returning all records

I have two database tables: "blog_posts" and "blog_comments".
blog_posts:
postID | postTitle | postContent | ...
1 | Hello World | This is a blog post | ...
2 | Lorem Ipsum | This is another blog post | ...
3 | Test Post | This is a third post | ...
blog_comments:
commentID | postID | comment | ...
1 | 1 | Very cool | ...
2 | 1 | Nice | ..
My current sql query is:
SELECT
blog_posts.*,
COUNT(blog_comments.commentID) AS commentCount
FROM
blog_posts
LEFT JOIN blog_comments ON blog_posts.postID = blog_comments.post_id;
I want it to return 0 as commentCount if there are no comments to this post, but instead the query returns only the first post which is the only one which has comments at the moment.
How can I fix that?
Thanks.
You're using COUNT without specified grouping column which means all rows are agregated into one and count is evaluated on the whole set.
Specify grouping column explicitely and you should be fine:
SELECT
blog_posts.*,
COUNT(blog_comments.commentID) AS commentCount
FROM
blog_posts
LEFT JOIN blog_comments ON blog_posts.postID = blog_comments.post_id
GROUP BY blog_posts.postID;

How to implement a PHP condition using pure SQL?

I have this tables structure:
// Posts
+----+------------+-----------------------+----------------+-------------+
| id | title | content | money_amount | author_id |
+----+------------+-----------------------+----------------+-------------+
| 1 | title 1 | content 1 | NULL | 12345 |
| 2 | title 2 | content 2 | 25 | 42355 |
| 3 | title 3 | content 3 | 5 | 53462 |
| 4 | title 4 | content 4 | NULL | 36346 |
| 5 | title 5 | content 5 | 15 | 13322 |
+----+------------+-----------------------+----------------+-------------+
// ^^ NULL means this post is free
// Money
+---------+--------------+
| post_id | user_id_paid |
+---------+--------------+
| 2 | 42355 | // He is author of post
| 2 | 34632 | // This row means besides author, this user 34632 can see this post too. Because he paid the money of this post.
| 3 | 53462 | // He is author of post
| 5 | 13322 | // He is author of post
| 3 | 73425 | // This row means besides author, this user 34632 can see this post too. Because he paid the money of this post.
+---------|--------------+
Note1: All post_id(s) in the Money table are belong to those posts which are non-free.
Note2: Always there is a row belong to author of post (which is non-free) in the Money table.
Note3: Money table is just to determines who can see such a post.
Now this user $_SESSION['current_user'] = '23421' wants to see this post id = 2. Here is my code:
$stm = $this->dbh->prepare(SELECT * FROM Posts WHERE id = '2');
$stm->execute();
$result = $stm->fetch(PDO::FETCH_ASSOC);
if ( $result[money] == '') { // money_amount is NULL in the Posts table
this post is free and everybody can see it
} else {
$stm = $this->dbh->prepare(SELECT count(1) FROM Money WHERE post_id = '2' and user_id = $_SESSION['current_user']);
$num_rows = $stm->fetchColumn();
if($num_rows){
$paid = true; // This means current user paid the cost of post and he can see it.
} else {
$paid = false; // this means current user didn't pay the cost of post and he cannot see it.
}
}
I want to know, can I implement those two query in one query and do that condition using MySQL instead of PHP ?
Here is solution using IF and EXISTS functions(MySql):
...
$stmt = $conn->prepare("
SELECT IF(p.money_amount,1,0) as notfree,
EXISTS(SELECT * FROM `Money` WHERE `post_id` = ? AND`user_id_paid` = ?) as paid
FROM `Posts` p WHERE p.id = ? ");
$stmt->execute([2, $_SESSION['current_user'], 2]);
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$result['notfree']) { // post is free
// this post is free and everybody can see it
} else {
$paid = ($result['paid'])? true : false;
}
You can use a join, and the query below uses LEFT JOIN.
SELECT * FROM Money
LEFT JOIN Posts ON Money.post_id = Posts.id
WHERE ((Posts.money_amount IS NOT NULL AND Money.user_id_paid = :userId)
OR Posts.money_amount IS NULL) AND Posts.id = :postId
Note that :userId is a placeholder for PDO parameterized query, where you should bind the parameter to the placeholder before execution. Like:
$postId = 2;
$stmt->bindParam('userId', $_SESSION['current_user']);
$stmt->bindParam('postId', $postId);
Also note that when binding the placeholder name doesn't need the colon. Using a RIGHT JOIN means you SELECT from the Posts table and join the Money table.

Table inside of another

I want to plan my trips publicly so other people can join me. So, I have set-up an PHP site.
I have this tables:
trips:
+----+---------+------------+------------+-------------------------+
| id | title | date_start | date_end | marker_adress |
+----+---------+------------+------------+-------------------------+
| 1 | Berlin | 2015-07-10 | 2015-07-11 | Potsdamer Platz, Berlin |
| 2 | Hamburg | 2015-07-16 | 2015-07-18 | Jungfernstieg, Hamburg |
+----+---------+------------+------------+-------------------------+
fellows:
+----+---------+---------------+
| id | trip_id | twittername |
+----+---------+---------------+
| 1 | 1 | prtyengopls |
| 2 | 1 | itobi_yt |
| 3 | 1 | jessisadancer |
| 4 | 2 | jessisadancer |
| 5 | 2 | woelfch3n |
+----+---------+---------------+
For displaying sake, I want to query them in one query. How can I query the database so I have something like this? (I know, it's JSON but it shows the structure very well.)
{
"id": 1,
"date_start": "2015-07-10",
"date_end": "2015-07-11",
"marker_adress": "Potsdamer Platz, Berlin",
"fellows": [
{
"id": 1,
"twittername": "prtyengopls"
},
{
"id": 2,
"twittername": "itobi_yt"
},
{
"id": 3,
"twittername": "jessisadancer"
}
]
}
First you have to use a LEFT JOIN like this:
SELECT
t.id AS tripID,
t.title AS title,
t.date_start AS dateStart,
t.date_end AS dateEnd,
t.marker_address AS markerAddress,
GROUP_CONCAT(DISTINCT CAST(f.id AS CHAR)) AS fellowID,
GROUP_CONCAT(DISTINCT CAST(f.twittername AS CHAR)) AS twitterName
FROM trips t LEFT JOIN fellows f ON t.id = f.trip_id
GROUP BY t.id
By using this you will get a single row for each trip and you can loop over fellowID and twitterName for each row, as it will be comma delimited list like this:
fellowID: 1,2,3
twitterName: prtyengopls,itobi_yt,jessisadancer
Edit 1: I got a new column to trips called checked which is a boolean.
Could you update your query, so only trips that have this boolean
toggled on are displayed?
SELECT
t.id AS tripID,
t.title AS title,
t.date_start AS dateStart,
t.date_end AS dateEnd,
t.marker_address AS markerAddress,
GROUP_CONCAT(DISTINCT CAST(f.id AS CHAR)) AS fellowID,
GROUP_CONCAT(DISTINCT CAST(f.twittername AS CHAR)) AS twitterName
FROM trips t INNER JOIN fellows f
ON t.id = f.trip_id AND t.checked = 1
GROUP BY t.id
I don't think it's possible like you expected in SQL. Just to answer your question and show you the problem: you have to use a join (left, inner or something ... depends of your database structure; I always prefer left joins if possible) to get all information in just one query:
SELECT * FROM trips t LEFT JOIN fellows f on t.id = f.trip_id WHERE t.id = 1;
But you always will get the trip information with every row, and for that you have to process it afterwards. You will never able to select such a nested structure, you will always get a flat one.
So I would recommend to split it into two queries like this:
SELECT * FROM trips WHERE id = 1;
SELECT * FROM fellows WHERE trip_id = 1;
You will have to process the information afterwards too, but you just select the wanted information from your database.
Hope that helps.

Trouble structuring MySQL query with one-to-many over multiple tables

I have a blog-style app that allows users to tag each post with topics. I keep this data in three separate tables: posts (for the actual blog posts), topics (for the various tags), and posts_topics (a table that stores the relationships between the two).
In order to keep the MVC structure (I'm using Codeigniter) as clean as possible, I'd like to run one MySQL query that grabs all the post data and associated topic data and returns it in one array or object. So far, I'm not having any luck.
The table structure is like this:
Posts
+--------+---------------+------------+-----------+--------------+---------------+
|post_id | post_user_id | post_title | post_body | post_created | post_modified |
+--------+---------------+------------+-----------+--------------+---------------+
| 1 | 1 | Post 1 | Body 1 | 00-00-00 | 00-00-00 |
| 2 | 1 | Post 1 | Body 1 | 00-00-00 | 00-00-00 |
+--------+---------------+------------+-----------+--------------+---------------+
// this table governs relationships between posts and topics
Posts_topics
+--------------+---------------------+-------------------------+-----------------+
|post_topic_id | post_topic_post_id | post_topic_topic_id | post_topic_created |
+--------------+---------------------+-------------------------+-----------------+
| 1 | 1 | 1 | 00-00-00 |
| 2 | 1 | 2 | 00-00-00 |
| 3 | 2 | 2 | 00-00-00 |
| 4 | 2 | 3 | 00-00-00 |
+--------------+---------------------+-------------------------+-----------------+
Topics
+---------+-------------+-----------+----------------+
|topic_id | topic_name | topic_num | topic_modified |
+---------+-------------+-----------+----------------+
| 1 | Politics | 1 | 00-00-00 |
| 2 | Religion | 2 | 00-00-00 |
| 3 | Sports | 1 | 00-00-00 |
+---------+-------------+-----------+----------------+
I have tried this simple query with n success:
select * from posts as p inner join posts_topics as pt on pt.post_topic_post_id = post_id join topics as t on t.topic_id = pt.post_topic_topic id
I've also tried using GROUP_CONCAT, but that gives me two problems: 1) I need all the fields from Topics, not just the names, and 2) I have a glitch in my MySQL so all GROUP_CONCAT data is returned as a BLOB (see here).
I'm also open to hearing any suggestions where I run two queries and try to build out an array for each result; I tried that with the code below but failed (this code also includes joining the user table, which would be great to keep that as well):
$this->db->select('u.username,u.id,s.*');
$this->db->from('posts as p');
$this->db->join('users as u', 'u.id = s.post_user_id');
$this->db->order_by('post_modified', 'desc');
$query = $this->db->get();
if($query->num_rows() > 0)
{
$posts = $query->result_array();
foreach($posts as $p)
{
$this->db->from('posts_topics as p');
$this->db->join('topics as t','t.topic_id = p.post_topic_topic_id');
$this->db->where('p.post_topic_post_id',$p['post_id']);
$this->db->order_by('t.topic_name','asc');
$query = $this->db->get();
if($query->num_rows() > 0)
{
foreach($query->result_array() as $t)
{
$p['topics'][$t['topic_name']] = $t;
}
}
}
return $posts;
}
Any help greatly appreciated.
This query should do the trick. Just change the * to the field list you desire so you are not pulling excess data every time you run the query.
Select
*
FROM
posts,
post_topics,
topics
WHERE
post_topic_topic_id = topic_id AND
post_topic_post_id = post_id
ORDER BY
post_id, topic_id;
Select
*
FROM
posts,
post_topics,
topics,
users
WHERE
post_topic_topic_id = topic_id AND
post_topic_post_id = post_id AND
post_user_id = user_id
ORDER BY
post_id, topic_id;
Holy Cow, You Can do it! See it helps to help. Never knew, try this
Select
post_id,
GROUP_CONCAT(DISTINCT topic_name) as names
FROM
posts,
post_topics,
topics
WHERE
post_topic_topic_id = topic_id AND
post_topic_post_id = post_id
GROUP BY
post_id;
You get
1, 'politics,relligion'
2, 'sports,relligion'

Categories