Recursive function too slow for creating hierarchical select list - php

I have a query that gets me a Geo-Structure from the database.
Categories Table (30.000 rows inside):
id title parent type
-------------------------------
1 germany 0 1
2 bavaria 1 2
3 upper bavaria 2 3
4 munich 3 4
6 italy 0 1
7 toscana 6 2
8 city florence 7 3
9 florence 8 4
Categories Language Table
cid language title
--------------------------
1 en-UK germany
2 de-DE deutschland
Objects table:
id title landid regionid uregionid cityid
--------------------------------------------------
1 o1 1 2 3 4
2 o2 1 2 3 4
3 o3 6 7 8 9
MySQL query:
SELECT c.id, c.title, l.title AS translated, c.type, c.parent, count(c.id) as cnt
FROM category c
LEFT JOIN objects o ON (o.landid = c.id OR o.regionid = c.id OR o.uregionid = c.id OR o.cityid = c.id)
LEFT JOIN category_lang l ON l.cid = c.id AND l.language = "en-UK"
WHERE c.published = 1 AND o.published = 1
GROUP BY c.id
ORDER BY c.parent
I get an associative array ($tree) with values like here:
Array
(
[0] => Array
(
[id] => 1
[title] => Germany
[type] => 1
[parent] => 0
[cnt] => 1
)
[1] => Array
(
[id] => 6
[title] => Italy
[type] => 1
[parent] => 0
[cnt] => 1
)
[2] => Array
(
[id] => 2
[title] => Bavaria
[type] => 2
[parent] => 1
[cnt] => 1
)
[3] => Array
(
[id] => 7
[title] => Toscana
[type] => 2
[parent] => 6
[cnt] => 1
)
[4] => Array
(
[id] => 3
[title] => Upper Bavaria
[type] => 3
[parent] => 2
[cnt] => 1
)
[5] => Array
(
[id] => 8
[title] => City Florence
[type] => 3
[parent] => 7
[cnt] => 1
)
[6] => Array
(
[id] => 4
[title] => Munich
[type] => 4
[parent] => 3
[cnt] => 1
)
[7] => Array
(
[id] => 9
[title] => Florence
[type] => 4
[parent] => 8
[cnt] => 1
)
)
Then i create a structure that will prepare the display of a select list:
public static function buildTree($tree, $root = 0) {
$return = array();
foreach($tree as $child) {
if($child['parent'] == $root) {
$return[] = array(
'name' => $child,
'next' => self::buildTree($tree, $child['id'])
);
}
}
return empty($return) ? null : $return;
}
Then i send the structure inside $return to a function to create the final select list:
public static function buildSelect($tree, $s) {
$option = '';
if(!is_null($tree) && count($tree) > 0) {
foreach($tree as $node) {
$selected = '';
$class_type = $node['name']['type'];
$option .= '<option value="'.$node['name']['id'].'" class="h'.$class_type.'" '.$selected.' data-type="'.$node['name']['type'].'">'
.$node['name']['title']. ' (' . $node['name']['cnt'] . ')</option>'
. self::buildSelect($node['next'], $s);
}
return $option;
}
}
This all works but if the Geo-Structure gets really big the db query gets terrible slow.
Would appreciate any ideas about how to speed this up, thanks!

The basics seem there, and without setting up a lot of test data it is difficult to do any testing.
The following has a couple of minor tweaks (you will need to change to cope with where you want to keep the compare routine). Possibly not enough to fix the issue. However I would be interested to know what effect it has, and also some timings of which method is taking the time, and how it ramps up:-
<?php
function cmp($a, $b)
{
return strcmp($a["parent"], $b["parent"]);
}
usort($tree, "cmp");
$recursive_tree = self::buildTree($tree);
echo self::buildSelect($recursive_tree, '');
public static function buildTree(&$tree, $root = 0)
{
$return = array();
foreach($tree as $child)
{
if($child['parent'] == $root)
{
$return[] = array(
'name' => $child,
'next' => self::buildTree($tree, $child['id'])
);
}
else
{
if ($child['parent'] > $root)
{
break;
}
}
}
return empty($return) ? null : $return;
}
public static function buildSelect(&$tree, $s)
{
$option = '';
if(!is_null($tree) && count($tree) > 0)
{
foreach($tree as $node)
{
$selected = '';
$class_type = $node['name']['type'];
$option .= '<option value="'.$node['name']['id'].'" class="h'.$class_type.'" '.$selected.' data-type="'.$node['name']['type'].'">'
.$node['name']['title']. ' (' . $node['name']['cnt'] . ')</option>'
. self::buildSelect($node['next'], $s);
}
return $option;
}
}
Does the output have to be built up, or can be be directly output (even to a temp file)?
EDIT
If your objects table has indexes on landid, regionid, uregionid and cityid (ie, separate indexes on each one) then try this:-
SELECT c.id, c.title, c.type, c.parent, count(c.id) as cnt
FROM category c
LEFT JOIN objects o1 ON o1.landid = c.id AND o1.published = 1
LEFT JOIN objects o2 ON o2.regionid = c.id AND o2.published = 1
LEFT JOIN objects o3 ON o3.uregionid = c.id AND o3.published = 1
LEFT JOIN objects o4 ON o4.cityid = c.id AND o4.published = 1
WHERE c.published = 1
AND (01.id IS NOT NULL
OR 02.id IS NOT NULL
OR 03.id IS NOT NULL
OR 04.id IS NOT NULL)
GROUP BY c.id,
c.title,
c.type,
c.parent
ORDER BY c.parent
EDIT
Adding in your language table:-
SELECT c.id,
c.title,
l.title AS translated,
c.type,
c.parent,
count(c.id) as cnt
FROM category c
LEFT OUTER JOIN category_lang l ON l.cid = c.id AND l.language = "en-UK"
LEFT OUTER JOIN objects o1 ON o1.landid = c.id AND o1.published = 1
LEFT OUTER JOIN objects o2 ON o2.regionid = c.id AND o2.published = 1
LEFT OUTER JOIN objects o3 ON o3.uregionid = c.id AND o3.published = 1
LEFT OUTER JOIN objects o4 ON o4.cityid = c.id AND o4.published = 1
WHERE c.published = 1
AND (01.id IS NOT NULL
OR 02.id IS NOT NULL
OR 03.id IS NOT NULL
OR 04.id IS NOT NULL)
GROUP BY c.id,
c.title,
translated,
c.type,
c.parent
ORDER BY c.parent
As to whether this or the UNION solution is better, that will come down largely to your data. For example this solution will struggle if the 4 id columns in the objects table can have duplicates (unlikely, as I would doubt a city can also be a region).

Take a look at Nested Sets. It is fairly easy to add calculated tree_left, tree_right, and tree_depth to your current parent-based model — parent stays as primary data, other three will be recalculated on change.
Then, you can use a much simpler select to get items for the whole subtree of categories and thanks to the tree_depth column it is easy to calculate correct indentation in your <select> element or in listings without any recursion.

Related

How to get all categories of a product in PHP/SQL

I have an sql request that retrieves all the products in the database with it's categories using mysqli_fetch_all, problem arises when a product has two categories I have twice the product coming back in the result or n times as many categories the product has and I cain't get it to output properly in the html template if someone explained to me in php or sql how to do what i wish thanks.
products
--------
id
name
etc ...
categories
----------
id
name
category_product
----------------
category_id
product_id
$sql = "SELECT
p.id,
p.name,
p.price,
categories.name as category_name
FROM products as p
JOIN category_product
ON p.id = category_product.product_id
JOIN categories
ON category_product.category_id = categories.id";
output => [0] => Array (
[id] => 3
[name] => product 3
[price] => 1.00
[category_name] => cat 4 )
[1] => Array (
[id] => 3
[name] => product 3
[price] => 1.00
[category_name] => cat 3 )
expected => [0] => Array (
[id] => 3
[name] => product 3
[price] => 1.00
[category_name] => Array(
[0] => cat 3
[1] => cat 4 ]))
Change your query to generate a GROUP_CONCAT of category names, then after fetching you can split that value using explode in PHP. For example (assuming you are using MySQLi with a connection $conn):
$sql = "SELECT
p.id,
p.name,
p.price,
GROUP_CONCAT(categories.name SEPARATOR '|') as category_name
FROM products as p
JOIN category_product ON p.id = category_product.product_id
JOIN categories ON category_product.category_id = categories.id
GROUP BY p.id, p.name, p.price";
$result = $conn->query($sql) or die($conn->error);
while ($row = $result->fetch_assoc()) {
// split the category names into an array
$row['category_name'] = explode('|', $row['category_name']);
// do something with the row value
}
Note that you need to specify a separator to GROUP_CONCAT (in this case I've used |) which will not occur in a category name.

PHP SQL left join fetch all

I have 3 mysql tables from where I am trying to fetch data
Table: list
list_id | name | description
-------------------------------------
1234 | name1 | sample description1
1235 | name2 | sample description2
Table: list_to_category
id | list_id | category_id
--------------------------------
1 | 1234 | 1
2 | 1234 | 2
3 | 1234 | 3
4 | 1235 | 2
5 | 1235 | 3
And table: category
id | title | parent_id
--------------------------------
1 | Category 1 | 0
2 | Category 2 | 0
3 | Category 3 | 0
And from PHP SQL query I want to fetch data like below
1. name1 - category 1, category 2, category 3
2. name2 - category 2, category 3
I tried below query
SELECT list.name, category.title FROM list
LEFT JOIN list_to_category
ON list.id = list_to_category.list_id
LEFT JOIN category
ON list_to_category.id = category.id
This gives me only single category name assigned to a list like this
1. name1 - category 1
2. name2 - category 2
Is it possible in single query?
You can use GROUP_CONCAT for this:
select
l.list_id,
l.name,
group_concat(distinct c.title) categories
from list l
left join list_to_category lc
on l.list_id = lc.list_id
left join category c
on lc.category_id = c.id
group by l.list_id
You can try this solution.
select l.list_id, l.name, (select group_concat(c.title) from list_to_category ltc JOIN category c ON c.id=ltc.category_id where ltc.list_id=l.id) from list l
Hope this will help you!!!
Use GROUP_CONCAT for group by "name" to fetch result :
SELECT L.name, GROUP_CONCAT(C.title) as title FROM list L
LEFT outer JOIN list_to_category LC ON L.list_id = LC.list_id
LEFT outer JOIN category C ON LC.category_id = C.id
group by L.name
Use GROUP_CONCAT for group by "list_id" for same name of list to fetch result :
SELECT L.name, GROUP_CONCAT(C.title) as title FROM list L
LEFT outer JOIN list_to_category LC ON L.list_id = LC.list_id
LEFT outer JOIN category C ON LC.category_id = C.id
group by L.list_id
It should be apparent from the code below that I'm no PHP coder. However, this should get the idea across. You can also use javascript/css to handle the transformation, which means things can be even more dynamic...
Oh, and I changed some table/column names - because I like it better that way...
<?php
require('path/to/connection/statements'); // $con
$query = "
SELECT l.list_id
, l.name
, l.description
, c.category_id
, c.title
, c.parent_id
FROM list l
JOIN list_category lc
ON lc.list_id = l.list_id
JOIN category c
ON c.category_id = lc.category_id
ORDER
BY l.list_id
, c.category_id;
";
$result = mysqli_query($con,$query);
$my_array = array();
while($row = mysqli_fetch_assoc($result)){
$my_array[] = $row;
}
$new_array = array();
foreach ($my_array as $row)
{
$new_array[$row['list_id']][$row['name']][$row['description']][] = $row['title'];
}
print_r($new_array);
?>
This will turn an array like this:
Array
(
[0] => Array
(
[list_id] => 1234
[name] => name1
[description] => sample description1
[category_id] => 1
[title] => Category 1
[parent_id] => 0
)
[1] => Array
(
[list_id] => 1234
[name] => name1
[description] => sample description1
[category_id] => 2
[title] => Category 2
[parent_id] => 0
)
[2] => Array
(
[list_id] => 1234
[name] => name1
[description] => sample description1
[category_id] => 3
[title] => Category 3
[parent_id] => 0
)
[3] => Array
(
[list_id] => 1235
[name] => name2
[description] => sample description2
[category_id] => 2
[title] => Category 2
[parent_id] => 0
)
[4] => Array
(
[list_id] => 1235
[name] => name2
[description] => sample description2
[category_id] => 3
[title] => Category 3
[parent_id] => 0
)
)
...into an array like this...
Array
(
[1234] => Array
(
[name1] => Array
(
[sample description1] => Array
(
[0] => Category 1
[1] => Category 2
[2] => Category 3
)
)
)
[1235] => Array
(
[name2] => Array
(
[sample description2] => Array
(
[0] => Category 2
[1] => Category 3
)
)
)
)
try this code
select l.name,c.title
from list_to_category lc join list l on lc.list_id=l.id
join category c on lc.catg_id=c.id

Php, MySql join tables, limit records per category

I have two tables:
Items
ID Name Model_ID
---------------------------------
1 2010 Audi L1 1
2 2014 BMW X2 2
3 2015 Acura L3 3
4 2016 BMW X5 2
5 2012 BMW X3 2
6 2013 BMW X4 2
7 2015 Acura L1 3
8 2011 Acura L2 3
9 2011 Audi L5 1
10 2012 Audi L6 1
Brands
Model_ID Title
---------------------
1 Audi
2 BMW
3 Acura
And following query:
SELECT
b.name,
i.title,
FROM
items AS i
INNER JOIN brands AS b
ON b.Model_ID = i.Model_ID
WHERE i.status = 1
ORDER BY i.created DESC;
The above produces working array:
Array
(
[0] => stdClass Object
(
[name] => 2010 Audi L1
[title] => Audi
)
[1] => stdClass Object
(
[name] => 2014 MBW X5
[title] => BMW
)
...
)
Than I use custom function to loop through array and end up with
Array
(
[Acura] => Array
(
[0] => stdClass Object
(
[name] => 2015 Acura L1
[title] => Acura
)
...
)
[BWM] => Array
(
[0] => stdClass Object
(
[name] => 2016 BMW X5
[title] => BWM
)
...
)
[Audi] => Array
(
[0] => stdClass Object
(
[name] => 2010 Audi L1
[title] => Audi
)
...
)
)
Now I can use foreach loop and limit each Brand to show x number of items, but the idea is to do it within database, so instead of pulling all records, I would like to be able to limit to 5 items per each brand.
Note: I did not list the rest of the table fields, such as created, which is used to sort records.
get brands:
$q = 'SELECT Model_ID AS id FROM brands ORDER BY title ASC';
$r = mysql_query($q);
$model_ids = array();
while($brand = mysql_fetch_assoc($r)) { $model_ids[] = $brand['id']; }
then make generate select query like this using Model_ID-s collected from 1st step or somewhere else (ex: from search form):
$q = array();
foreach($mode_ids AS $model_id) {
$q[] = '(SELECT b.name, i.title FROM items AS i INNER JOIN brands AS b ON b.Model_ID = i.Model_ID WHERE b.Model_ID = '.(int)$model_id.' AND i.status = 1 ORDER BY i.created DESC LIMIT 5)';
}
if(!empty($q)) {
$q = implode(' UNION ALL ', $q);
$r = mysql_query($q);
while($record = mysql_fetch_object($r)) {
var_dump($record);
}
}
as result we'll get records of resulting query:
(SELECT
b.name,
i.title FROM
items AS i
INNER JOIN brands AS b
ON b.Model_ID = i.Model_ID WHERE b.Model_ID=1 AND i.status = 1 ORDER BY i.created DESC LIMIT 5)
UNION ALL
(SELECT
b.name,
i.title FROM
items AS i
INNER JOIN brands AS b
ON b.Model_ID = i.Model_ID WHERE b.Model_ID=2 AND i.status = 1 ORDER BY i.created DESC LIMIT 5)
UNION ALL
(SELECT
b.name,
i.title FROM
items AS i
INNER JOIN brands AS b
ON b.Model_ID = i.Model_ID WHERE b.Model_ID=3 AND i.status = 1 ORDER BY i.created DESC LIMIT 5)

Output concated value from mysql query

I am trying to output a concated value from my query but am having some issues.
Here is my query:
$result = mysql_query(
"SELECT c.company, n.nid, n.createdOn, CONCAT_WS(' ',c2.fname,c2.lname), CONCAT_WS(' ',c3.fname,c3.lname), n.urgent, n.description
FROM notes n
INNER JOIN Positions p ON FIND_IN_SET(p.id, n.forDepts) > 0
LEFT JOIN companies c ON c.userid = n.clientId
LEFT JOIN companies c2 ON c2.userid = n.createdBy
LEFT JOIN companies c3 ON c3.userid = n.claimedBy
GROUP BY n.nid
LIMIT 0,100"
);
My array is printing like so:
Array ( [0] => Honda of Kirkland [company] => Honda of Kirkland [1] => 1 [nid] => 1 [2] => 2009-09-28 21:33:15 [createdOn] => 2009-09-28 21:33:15 [3] => [CONCAT_WS(' ',c2.fname,c2.lname)] => [4] => [CONCAT_WS(' ',c3.fname,c3.lname)] => [5] => 0 [urgent] => 0 [6] => Milestones [description] => Milestones )
I am trying it like this, but it does not work:
while ($row = mysql_fetch_array($result)) {
$created_by = $row[3];
}

How do i get this data in the proper format?

ok so i have this query
select ss.system_step_id, ss.step_number, cd.name, ssp.system_step_product_id, p.cost, ssp.class_id from system_step ss
join system as s on s.system_id=ss.system_id
join category_description as cd on cd.category_id=ss.sub_category_id
join system_step_product as ssp on ss.system_step_id=ssp.system_step_id
join product as p on p.product_id=ssp.product_id where s.system_id = 41
order by ss.step_number, ssp.class_id;
which yields this result
7 1 Screens 808 115.0000 1
7 1 Screens 809 381.9000 2
7 1 Screens 810 441.9000 3
8 2 Printers 811 112.3200 1
8 2 Printers 812 201.0400 2
8 2 Printers 813 202.8700 3
9 3 Cash Drawers 814 135.7000 1
9 3 Cash Drawers 815 86.5400 2
9 3 Cash Drawers 816 135.7000 3
Is there a way to turn this into a php array of three elements like this
Array
(
[0] => Array
(
[name] => "Screens"
[standard_product] => Array ([id] => 808, [price] => '115.0000')
[business_product] => Array ([id] => 809, [price] => '381.9000')
[premium_product] => Array ([id] => 810, [price] => '441.9000')
)
[1] => Array
(
[name] => "Printers"
[standard_product] => Array ([id] => 811, [price] => '112.3200')
[business_product] => Array ([id] => 812, [price] => '201.0400')
[premium_product] => Array ([id] => 813, [price] => '202.8700')
)
[2] => Array
(
[name] => "Cash Drawers"
[standard_product] => Array ([id] => 814, [price] => '135.7000')
[business_product] => Array ([id] => 815, [price] => '86.5400')
[premium_product] => Array ([id] => 816, [price] => '135.7000')
)
)
$sql = "select ss.system_step_id, ss.step_number, cd.name, ssp.system_step_product_id, p.cost, ssp.class_id, pd.name as product_name, pd.description from system_step ss join system as s on s.system_id=ss.system_id join category_description as cd on cd.category_id=ss.sub_category_id join system_step_product as ssp on ss.system_step_id=ssp.system_step_id join product as p on p.product_id=ssp.product_id join product_description as pd on pd.product_id=p.product_id where s.system_id = {$system['system_id']} order by ss.step_number, ssp.class_id;";
$result = mysql_query($sql);
$steps = array();
while($row_r = mysql_fetch_assoc($result)){
$steps[] = $row_r;
}
so steps is the the full array with 9 elements
As you can see the only thing of note is the class_id 1 is standard_product class_id 2 is business_product and class_id 3 is premium_product
You can fetch fill a multi-dimensional array while fetching the SQL result or split into several SQL commands and therefore build up your array (1st fetching the names, than the products with values of every name).
I do not know which you prefer but as you have a large join splitting the SQL might not be the worst in terms of readability of your code. Performance impact may vary.
Methinks this will do it
<?php
$finalArray = array();
$nameArray = array('Screens', 'Printers', 'Cash Drawers');
foreach ($nameArray as $name) {
while ($row = mysql_fetch_assoc($result)) {
if ($row['name'] == $name) {
if ($row['class_id'] == 1) {
$standard = array('id' => $row['system_step_product_id'], 'price' => $row['cost']);
}
else if ($row['class_id'] == 2) {
$business = array('id' => $row['system_step_product_id'], 'price' => $row['cost']);
}
else if ($row['class_id'] == 3) {
$premium = array('id' => $row['system_step_product_id'], 'price' => $row['cost']);
}
}
}
array_push($finalArray, array('name' => $name, 'standard_product' => $standard, 'business_product' => $business, 'premium_product' => $premium,));
}
?>
Hopefully I got all the column names right.
This is what I tested it off of and I get the correct output http://codepad.org/yjU3gxbB
The only thing you may what to change is creating the name array dynamically
while ($row = mysql_fetch_assoc($result)) {
array_push($nameArray, $row['name']);
}
$nameArray = array_unique($nameArray);
However, PHP doesn't like it when you do a mysql_fetch_assoc twice on the same query. You might want to do another query where you only select the cd.name

Categories