Mysql sub-cat and parent cat with posts [Database schema] - php

Okay so I have a simple category table and a separate posts table easy right but when the user posts a post I wast think should I store both the sub and parent cat in the posts table but would that not be a lot of data duplication so I instead just store the sub_cat then I use a few PHP functions to query the database for the primary cat and its name.
categories table
ID | cat_name | main_cat
1 | Dinner | 0
2 | Chicken | 1
posts table
ID | title | sub_cat | fields that are not related to Q
1 | test | 2 |
Get parent(main) category
$sub_cat = is from a selection query that gets posts and their sub_cats
function main_cat($sub_cat){
require("conn_posts.php");
$stmt = $conn_posts->prepare("SELECT `main_cat` FROM `cats` WHERE `ID` = ?");
$stmt->bind_param("s", $sub_cat);
$stmt->execute();
$stmt_results = $stmt->get_result(); // get result
while($row_get = $stmt_results->fetch_assoc()){
if($row_get['main_cat'] == 0){
return $sub_cat;
}elseif($row_get['main_cat'] !== ""){
return $row_get['main_cat'];
}
}
}
This function gets any category name as long as the id is valid
function cat_name($cat_number){
require("conn_posts.php");
$stmt = $conn_posts->prepare("SELECT `cat_name` FROM `cats` WHERE `ID` = ?");
$stmt->bind_param("s", $cat_number);
$stmt->execute();
$stmt_results = $stmt->get_result(); // get result
$row_get = $stmt_results->fetch_assoc();
if($stmt_results->num_rows <= 0){
return 0;
}elseif($stmt_results->num_rows == 1){
return $row_get['cat_name'];
}
}
My question is is this a good way to process my posts sub-category and parent category are there better ways of doing what I am currently doing? eg. is my database schema good(by good I mean is it better to just include the parent cat id in the posts table than to do the PHP server-side processing)?

Your database schema is good: it doesn't include any replication, I wouldn't change it. The way you're handling fetching the categories in PHP isn't really optimal though: you should almost always aim to minimize the number of queries as it (in general) will affect performance more than the complexity of a query.
If you're running MySQL 8+, a great way to do this is with a recursive CTE; it will allow you to fetch all parents with one query:
WITH RECURSIVE cte AS (
SELECT id, cat_name, main_cat, 0 as depth FROM categories WHERE ID=3
UNION ALL
SELECT categories.id, categories.cat_name, categories.main_cat, cte.depth+1 as depth
FROM cte inner join categories
ON cte.main_cat = categories.id
)
SELECT cat_name FROM cte order by depth ASC
The number '3' in that query can be replaced by the category you're trying to retrieve. You can check this DB fiddle for a live example. If I see your code, incorporating it into your PHP should be fairly trivial. If not, leave a comment and I'll try to expand.

Related

MySQL count child elements

I have a categories table:
id | name | parent_id
1 | Camaro | 0
2 | Chevelle | 0
3 | Sale - Camaro Parts | 1
4 | Bestselling Parts | 1
My first request looks like:
'SELECT
*
FROM
`categories`
WHERE
parent_id = :parent_id';
And after I'm fetching result set I make sub query to check if row has child elements:
foreach($result as $r) {
$r->hasChild = count(ORM::forTable('categories')->where('parent_id', $r->id)->findArray());
$data[] = $r;
}
Is any way to avoid multiple connection to DB in foreach loop and get data in first query?
Thanks!
This isn't awful to do, so long as you only want the count of children below the selected rows. If you want the entire hierarchy, you'll need to use a better RDMS.
The main part of the solution here is self joining the same table. Then we can use the count() aggregate function to see how many children are attached to each item.
select
categories.id
, categories.name
, categories.parent_id
, count(chld.id)
from
categories
left join categories chld
on categories.id = chld.parent_id
where
parent_id = :parent_id
group by
categories.id
, categories.name
, categories.parent_id
You can do a self join to the table on parent_id and id. Based on whether you want categories with child or not you can do a left join or inner join. Kind of a similar question is mentioned here -
Mysql Self Join to find a parent child relationship in the same table

PHP & Mysql - nested loop

I have 2 tables - products and productimages.
product has unique id and title.
productimages has productid and imageurl. these are examples of my tables:
products:
|id|title |
_____________
|1 |Laptop |
|2 |Speakers |
productimages:
|productid|imageurl|
___________________
| 1 |lap1.png|
| 1 |lap2.png|
| 1 |lap3.png|
| 2 |spe1.png|
Right now I have a nested loop in PHP.
loop through all rows of -> select * from products
and for every product inside the loop -> select * from productimages where productid = id which is basically another loop inside the first loop.
and then I take all productimages into array and decode to JSON [title,photos].
Now imagine you have 2 million rows in productimages, the query times are too high, is there any way to make it more efficient?
$query = "SELECT * FROM products ORDER BY id LIMIT 10;
$result = mysqli_query($con,$query);
if(mysqli_num_rows($result)>0)
{
$response = array();
while($row = mysqli_fetch_assoc($result)) {
$photos = array();
$id = $row["id"];
$title = $row["title"];
$queryp = "select imageurl from productimages where productid= '".$id."';";
$resultp = mysqli_query($con,$queryp);
if(mysqli_num_rows($resultp)>0)
{
while($row2 = mysqli_fetch_assoc($resultp)) {
$photourl = $row2["imageurl"];
array_push($photos,$photourl);
}
}
}
}
Some betterment for you could be:
1) Don't use select *. Use column names instead. e.g. select products.id, products.title, productimages.imageurl
2) Use JOIN instead of nested loop
So, you can try querying data like:
select products.id, products.title, productimages.imageurl
from products
join productimages on products.id = productimages.productid
ORDER BY products.id LIMIT 10
This case is not uncommon - you have two tables in a one to many relationship.
You should never nest an SQL call in a loop if you can possibly avoid it but there is a decision to be made about one SQL call or two.
A single SQL call could be:
SELECT id, title, imageURL
FROM products LEFT JOIN productImages ON id=productid
The disadvantage of this is that you are extracting the title several times for each product and this is wasteful.
Using two SQL statements you can download the titles once for each product:
SELECT id, title FROM products
The results of this query can be stored in an associative array - so that you can look up the title for each id.
The second query is:
SELECT productid, imageURL FROM productImages ORDER BY productid, imageURL
You can loop through the results of this query, spitting out the title as you go.
To save the images with product you can add a column imageurl in the products table.collect the image names with , and insert that image name string to the products table.
your table looks like below.
+--------------+--------------+---------------------------+
| id | title | imageurl |
+--------------+--------------+---------------------------+
| 1 | Laptop | lap1.png,lap2.png,lap3.png|
+--------------+--------------+---------------------------+
| 2 | Speakers | spe1.png |
Hope you understood what i explain.

Codeigniter - selecting children and parents from db

I want to pull from my database records corresponding to parent_id, like this:
function getChildren($id, $parent_id) {
$q = $this->db->select('id, name, slug, plat');
$q = $this->db->from('games');
$q = $this->db->where('parent_id',$id);
$q = $this->db->or_where('id',$parent_id);
$q = $this->db->get();
return $q->result_array();
}
It - if it's a children game - get parent_id and search for a game with such id and for other games that has parent_id same as this one. If it's the parent game, it only looks for games with parent_id same as it's id.
The problem is... it's not always working. I have four games in db:
id | parent_id | title
15 | 0 | Abe
19 | 15 | Abe
20 | 0 | RE2
21 | 20 | RE2 DS
First two works, last two - only children (id = 21) shows parent.
You likely could not do that with a relation database. RDBMS are not intended to manage any form of trees.
You can in some simple case, like one-level hierarchy, but as soon as it becomes more complex, it's getting messier and messier.
Keeping your structure, you have to make ONE JOIN per LEVEL, and that means knowing the depth in advance.
A solution to store trees in database is called Nested Tree, it basically stores interval for in each rows, but it is a bit complex to implement by yourself.
Take a look at Wikipedia explanation. There are however library which allows you to programmatically abstract such operations.
Use this query
select
*
from games as g
where parent_id = 0
union all
select
l.*
from games r
left join (select * from games) as l on r.id = t.id
where
r.parent_id != 0
Just after posting my problem, I tried another solution. Funny, I sit with this for more than an hour and after wasting your time, I came with the solution ;)
Anyway, here it is:
function getChildren($id, $parent_id) {
$q = $this->db->select('id, name, slug, plat');
$q = $this->db->from('games');
$q = $this->db->where('parent_id',$id);
$q = $this->db->or_where('id',$parent_id);
$q = $this->db->or_where('parent_id',$parent_id);
$q = $this->db->where('id !=', $id);
$q = $this->db->get();
return $q->result_array();
}

Tree Traversal recursive calculation

We have users, questions and unlimited levels of categories. The users can get some points from questions. Questions can have multiple categories.
What I want to do is to calculate the top users per category: It's simply total points taken from the questions under that category AND it's sub-categories too.
So, I have these tables:
questions
--------------
id
title
question
categories
--------------
id
parent_id
category
lft
rgt
question_categories
--------------
question_id
category_id
users
--------------
id
username
user_points
--------------
id
user_id
question_id
point_type
points
user_category
--------------
user_id
category_id
points
What I want to do is to calculate user_category.points value.
Summing up the points for each category is easy but including the sub-categories is getting complicated.
What might be the best way to do this?
Example calculation:
Let's say the categories are:
Programming
PHP
Zend Framework
Symfony
Java
Ruby on Rails
Assume that the user got 3 points from Zend Framework, 2 points from PHP, 5 points from java and 1 point from Rails. The points for this user per categories will be:
Programming 11 (5+5+1)
PHP 5 (2+3)
Zend Framework 3
Symfony
Java 5
Ruby on Rails 1
Perhaps it would be best to use tags instead of a hierarchy. For instance, anything with a "Zend Framework" will also have "PHP" and "Programming" tags. This also helps when some categories can appear in multiple places. For instance, I can use ajax in jQuery and also Javascript. Then, add 1 to each tag listed in the category for the user.
I would create a user_categories table in which I would store 3 values: user_id, category_id and user_score. It's easy to maintain (need only to INSERT or UPDATE) and it's also easy to query for top-users of every category.
If you're only going to sum per top-level category, then you should add a field to your categories table called root_id (holding the id of the transitive parent of the category).
Then your sum would be calculated as:
select up.user_id, ctg.root_id, sum(up.points)
from user_points up
join question_categories qc on up.question_id = qc.question_id
join categories ctg on qc.category_id = ctg.id
group by up.user_id, ctg.root_id
This php and SQL should get you the top 3 users for each category including sub categories:
$query = "SELECT id, parent_id FROM categories";
$parent = array();
...fetch mysql data loop depending on what connection you use, mysqli or pdo...
{
$parent[$result['id']] = $result['parent_id'];
}
$childs = array();
foreach($parent as $id => $parrent_id)
{
$childs[$parrent_id][$id] = $id;
$next_parrent_id = $parrent_id;
while($next_parrent_id = $parent[$next_parrent_id])
{
$childs[$next_parrent_id][$id] = $id;
}
}
foreach($parent as $id => $parrent_id)
{
$current_categories = array($id => $id) + $childs[$id];
$query = "SELECT user_id, username, SUM(points) AS total_points
FROM user_points
LEFT JOIN users ON (user_id = users.id)
LEFT JOIN question_categories USING (question_id)
WHERE category_id IN (" . implode(', ', $current_categories). ")
ORDER BY total_points DESC
LIMIT 3";
...fetch mysql data loop...
}

How to make recordset (categories and subcategories)

I have 2 tables:
categories (id, categoryName),
menu (id, menuname, category_id)
I would like to display all categories, which have one or more records in the menu.
And after every categoryName to show 5 menuname.
Is it possibe, to do this in the one recordset?
Thank you!
These are my 2 recordsets:
$query = "select a.id, a.name from categories as a where a.id in (select count(*) from menu as b on b.category_id = a.id)";
$result = mysql_query($query);
while ($row = mysql_fetch_array($result)) {
echo $row['name'];
$category_id = intval($row['id']);
$query = "select menuname from menu where category_id = $category_id limit 0, 5";
$resultmenu = mysql_query($query);
while ($rowmenu = mysql_fetch_array($resultmenu)) {
echo $rowmenu['menuname'];
}
}
As mentioned above, i'm not sure what is meant by "And after every categoryName to show 5 menuname".
But to show a list of all category/menu names in alphabetical order you could use the following:
SELECT C.categoryName,
M.menuname
FROM categories C
INNER JOIN menu M ON M.category_id = C.id
ORDER BY C.categoryName,
M.menuname
Update:
At the moment your first query will only be returning at best one row. The subquery is currently counting the number of menu rows and then this figure is being used to pull a row from the category table, which isn't what you want.
The following query joins onto the menu table to ensure that at least one item exists, and then groups by the category fields to ensure that each item is only returned once:
$query = "select a.id, a.name from categories as a inner join menu as b on b.category_id = a.id group by a.id, a.name"
Update 2
Ah sorry, I understand now. No I don't think it's possible to achieve what you want in a single query. Even if it were possible I wouldn't recommend it. Looking at your code, you only want to print the categoryName once for each set of menu items. If you were able to pull back the categoryName and menuname items in one result set like so:
| categoryName | menuname |
---------------------------
| category1 | menu1 |
| category1 | menu2 |
| category1 | menu3 |
| category2 | menu4 |
When iterating through the results you would need to manually check when the categoryName had changed in order to print it out once for that set of menuname items.

Categories