Count descendants in hierarchical query - php

I was told that PostgreSQL is a better choice than MySQL for displaying hierarchical data, so I installed PostgreSQL and I'm ready to go.
This is the schema from my title (copied from pgAdmin):
CREATE TABLE public.gz_life_mammals (
id integer NOT NULL,
taxon text NOT NULL,
parent text NOT NULL,
parent_id smallint NOT NULL,
slug text,
name_common text,
plural text,
extinct smallint NOT NULL,
rank smallint NOT NULL,
key smallint NOT NULL,
CONSTRAINT "Primary Key" PRIMARY KEY (id)
);
This is my database connection and first query:
$dbh = pg_connect("host=localhost dbname=geozoo user=postgres");
if (!$dbh) {
die("Error in connection: " . pg_last_error());
}
$sql = "SELECT * FROM gz_life_mammals";
$result = pg_query($dbh, $sql);
while ($row = pg_fetch_array($result)) {
echo "ID: " . $row[0] . " | ";
echo "Taxon: " . $row[1] . " | ";
echo "ParentID: " . $row[3] . "<br>";
}
// free memory
pg_free_result($result);
// close connection
pg_close($dbh);
The most important table fields for this exercise are the first four (id, taxon, parent and parent_id. The data looks like this:
ID | TAXON | PARENT | PARENT_ID
1 | Mammalia | Chordata | 1
2 | Carnivora | Mammalia | 2
3 | Canidae | Carnivora | 3
4 | Canis | Canidae | 4
5 | Canis-lupus | Canis | 5
6 | Canis-latrans | Canis | 5
Where the last two rows represent the wolf (Canis lupus) and coyote (Canis latrans). Eventually, I'd like to be able to display the names of children, grandchildren, parents, great grandparents, etc. But right now I'm just trying to display the number of descendants. For example, if I navigated to MySite/life/mammalia, I might see the following display:
Orders: 19
Families: 58
Genera: 688
Species: 8,034
If I navigated to MySite/life/canidae, it might display something like this:
Genera: 6
Species: 37
Can anyone show me the best way to write that kind of query and display the results (with PHP)?

Given the table:
select * from gz_life_mammals;
id | taxon | parent | parent_id
----+---------------+-----------+-----------
1 | Mammalia | Chordata | 1
2 | Carnivora | Mammalia | 2
3 | Canidae | Carnivora | 3
4 | Canis | Canidae | 4
5 | Canis-lupus | Canis | 5
6 | Canis-latrans | Canis | 5
(6 rows)
and the function to translate parent_id into taxonomic rank name:
create function tax_rank(id integer) returns text as $$
select case id
when 1 then 'Classes'
when 2 then 'Orders'
when 3 then 'Families'
when 4 then 'Genera'
when 5 then 'Species'
end;
$$ language sql;
you can query number of descendants with the following recursive query:
with recursive hier(taxon,parent_id) as (
select m.taxon, null::integer
from gz_life_mammals m
where taxon='Mammalia' --<< substitute me
union all
select m.taxon, m.parent_id
from hier, gz_life_mammals m
where m.parent=hier.taxon
)
select tax_rank(parent_id),
count(*) num_of_desc
from hier
where parent_id is not null
group by parent_id
order by parent_id;
tax_rank | num_of_desc
----------+-------------
Orders | 1
Families | 1
Genera | 1
Species | 2
(4 rows)
The interesting part is inside with recursive. The first part of the query selects the root row(s) of hierarchy. The second part (after union all) is called recursively and each time adds direct descendants to the previous result set. Read this to understand how it works in details.
After hierarchy is constructed, it can be represented as you like. In the above example only number of descendants are shown. You can get names as well:
with recursive hier(taxon,parent_id) as (
...
)
select tax_rank(parent_id),
taxon as name
from hier
where parent_id is not null
order by parent_id;
tax_rank | name
----------+---------------
Orders | Carnivora
Families | Canidae
Genera | Canis
Species | Canis-lupus
Species | Canis-latrans
(5 rows)
The same on one line:
with recursive hier(taxon,parent_id) as (
...
)
select tax_rank(parent_id),
string_agg(taxon,', ') as names
from hier
where parent_id is not null
group by parent_id
order by parent_id;
tax_rank | names
----------+----------------------------
Orders | Carnivora
Families | Canidae
Genera | Canis
Species | Canis-lupus, Canis-latrans
(4 rows)
And so on...

Related

select all products from child categories in parent category

I have the following 'categories' table:
+--------+---------------+----------------------------------------+
| ID | Parent ID | Name |
+--------+---------------+----------------------------------------+
| 1 | 0 | Computers |
| 2 | 1 | Apple |
| 3 | 1 | HP |
| 4 | 2 | Macbook Air |
| 5 | 2 | Macbook Pro |
| 6 | 1 | Dell |
| 7 | 6 | Inspiron |
| 8 | 6 | Alienware |
| 9 | 8 | Alienware 13 |
| 10 | 8 | Alienware 15 |
| 11 | 8 | Alienware 17 |
| 12 | 0 | Smartphones |
| 13 | 12 | Apple |
| 14 | 12 | Samsung |
| 15 | 12 | LG |
+--------+---------------+----------------------------------------+
Let's say I have the following 'products' table:
+--------+---------------+----------------------------------------+
| ID | Category ID | Name |
+--------+---------------+----------------------------------------+
| 1 | 13 | Apple iPhone 8 |
| 2 | 13 | Apple iPhone 8 Plus |
| 3 | 14 | Samsung Galaxy S8 |
+--------+---------------+----------------------------------------+
With the following query, I select all the products in a category:
SELECT
id,
name
FROM
products
WHERE
category_id = ?
Ok, my question:
The product 'Apple iPhone 8' is in the category Apple, this is a subcategory of the category Smartphones. If I replace the '?' in my query with 13 (the category ID of Apple), I get the product. When I replace the '?' in my query with 12 (the category ID of Smartphones), I don't get the product. I want to select all products that are in the category or in one of the child/grandchild/... categories. How can I do this with a single query (if possible)?
you can use join .
your query should be like this
SELECT
id,
name
FROM
products
JOIN
categories
ON
products.category_id = categories.id;
It can be achieved using join
SELECT
id,
name
FROM
products
JOIN
categories
ON
products.category_id = categories.id
WHERE products.category_id = 13 OR categories.parent_id = 12
SELECT id, name FROM products LEFT JOIN categories ON products.category_id = categories.id
1) A QUERY. I'm taking a query from this answer.
How to create a MySQL hierarchical recursive query Please read it for a full explanation of the query. The query assumes that the parent ID will be less than the child IDs (like 19 is less than 20,21,22).
select * from products where `Category ID` in
(select ID from
(select * from categories order by `Parent ID`, ID) categories_sorted,
(select #pv := '12') initialisation
where (find_in_set(`Parent ID`, #pv) > 0
and #pv := concat(#pv, ',', ID)) or ID = #pv)
You have to set the "12" to be whatever the parent category is.
2) Via two sections in PHP, one that loops until you have all category IDs. Then a second section that gets all products in those categories. This is far more verbose but I like how clear you can see what is happening.
$db = new mysqli(host, user, password, database);
$all_ids = []; // total ids found, starts empty
$new_ids = [12]; // put parent ID here
do {
// master list of IDs
$all_ids = array_merge($new_ids, $all_ids);
// set up query
$set = "(".implode($new_ids, ',').")";
$sql = "select ID from categories where `Parent ID` in $set";
// find any more parent IDs?
$new_ids = []; // reset to nothing
$rows = $db->query($sql) or die("DB error: (" . $db->errno . ") " . $db->error);
while ($row = mysqli_fetch_assoc($rows)) {
$new_ids[] = $row['ID'];
}
} while (count($new_ids) > 0);
// get products
$set = "(".implode($all_ids, ',').")";
$sql = "select * from products where `Category ID` in $set";
$rows = $db->query($sql) or die("DB error: (" . $db->errno . ") " . $db->error);
while ($row = mysqli_fetch_assoc($rows)) {
echo "{$row['Name']}<br>\n";
}

MySQL get Parent as Heading and children as items

Hello I have a database structure like this and some data in it
id | parent_id | name
1 | 0 | Nissan
2 | 1 | 240SX
3 | 1 | 350z
4 | 0 | Toyota
5 | 4 | Camry
6 | 4 | Prado
7 | 1 | Skyline
8 | 4 | Hilux
I want to take Nissan as heading and after show all the models. As well Toyota as heading and it's models below it. How do I achieve this using one query? Is it even possible?
You should normalize your database or at least separate your columns so there is a manufacturer column and a model column. This will then help you do a query such as
SELECT * FROM CARS WHERE manufacturer = 'Toyota';
or
SELECT * FROM CARS GROUP BY manufacturer;
But the best practise would be to have a table for storing manufacturers and a table for storing all models of the cars, then a table for storing your cars itself which will have 2 columns that are referenced from the other 2 tables.
So this will become:
id | parent_id | manufacturer_id | model_id
1 | 0 | 1 | 2 |
2 | 1 | 4 | 1 |
3 | 1 | 3 | 6 |
4 | 0 | 3 | 9 |
you will then do a query to join the 3 tables
SELECT cars.id, cars.parent_id, cars.manufacturer_id, cars.model_id, t1.manufacturer_name, t2.model_name from CARS
JOIN manufacturer_table t1
ON t1.manufacturer_id = cars.manufacturer_id
JOIN model_table t2
ON t2.model_id = cars.model_id
hope that helps
Try this, it should solve your problem:
SELECT
child.*
FROM
{your_table} parent
LEFT JOIN
{your_table} child ON child.parent_id = parent.id
OR (child.parent_id = 0 AND child.id = parent.id)
WHERE
parent.parent_id = 0
ORDER BY
parent.id, child.parent_id, child.name
{your_table} should be replaced in two places with your table's name (which you presented in the question).

for each value of one table get and display count number of coresponding values from another table

unfortunately i have to do this in mysql / php . I looked for three days, and there is like 10.000 explantions of this but NONE (and I repeat NONE) works for me. I tried it all. I have to ask, sorry.
I have two tables - articles and control.
table "articles"
------------------
art_id | name |
------------------
1 | aaa |
2 | bbb |
3 | ccc |
4 | ddd |
table "control"
--------------------------------------------
con_id | art_id | data |
--------------------------------------------
1 | 1 | something-a |
2 | 2 | something-b |
3 | 1 | something-a |
4 | 2 | something-c |
5 | 3 | something-f |
art_id exists in both tables. Now what i wanted - for query:
"select * from articles order by art_id ASC" displayed in a table
to have also one cell displaying the count for each of art_id's from table CONTROL...
and so i tried join, left join, inner join - i get errors ... I also tried for each get only one result (for example 2 for everything)... this is semi-right but it displays the array of correct results and it's not even with join!!! :
$query = "SELECT art_id, count(*) as counting
FROM control GROUP BY art_id ORDER BY con_id ASC";
$result = mysql_query($query);
while($row=mysql_fetch_array($result)) {
echo $row['counting'];
}
this displays 221 -
-------------------------------------------------
art_id | name | count (this one from control) |
-------------------------------------------------
1 | aaa | 221 |
2 | bbb | 221 |
3 | ccc | 221 |
and it should be:
for art_id(value1)=2,
for art_id(2)=2,
for art_id(3)=1
it should be simple - like a count of values from CONTROL table displayed in query regarding the "articles" table...
The result query on page for table articles should be:
"select * from articles order by art_id ASC"
-------------------------------------------------
art_id | name | count (this one from control) |
-------------------------------------------------
1 | aaa | 2 |
2 | bbb | 2 |
3 | ccc | 1 |
So maybe i should go with JOIN or with join plus for each... Tried tha too, but then i'm not sure what is the proper thing to echo... all-in-all i'm completely lost here. Please help. Thank you.
So imagine this in two steps:
Get the counts per art_id from the control table
Using your articles table, pick up the counts from step 1
That will give you a query that looks like this:
SELECT a.art_id, a.name, b.control_count
FROM articles a
INNER JOIN
(
SELECT art_id, COUNT(*) AS control_count
FROM control
GROUP BY art_id
) b
ON a.art_id = b.art_id;
Which will give you the results you're looking for.
However, instead of using a subquery, you can do it all in one shot:
SELECT a.art_id, a.name, COUNT(b.art_id) AS control_count
FROM articles a
INNER JOIN control b
ON a.art_id = b.art_id
GROUP BY a.art_id, a.name;
SQL Fiddle demo
SELECT *, (SELECT COUNT(control.con_id) FROM control WHERE control.art_id = articles.art_id) AS count_from_con FROM articles ORDER BY art_id DESC;
If I understood your question right, this query should do the trick.
Edit: Created the tables you have described, and it works.
SELECT * FROM articles;
+--------+------+
| art_id | name |
+--------+------+
| 1 | aaa |
| 2 | bbb |
| 3 | ccc |
| 4 | ddd |
+--------+------+
4 rows in set (0.00 sec)
SELECT * FROM control;
+--------+--------+------+
| con_id | art_id | data |
+--------+--------+------+
| 1 | 1 | NULL |
| 2 | 2 | NULL |
| 3 | 1 | NULL |
| 4 | 2 | NULL |
| 5 | 3 | NULL |
+--------+--------+------+
5 rows in set (0.00 sec)
SELECT *, (SELECT COUNT(control.con_id) FROM control WHERE control.art_id = articles.art_id) AS count_from_con FROM articles ORDER BY art_id ASC;
+--------+------+----------------+
| art_id | name | count_from_con |
+--------+------+----------------+
| 1 | aaa | 2 |
| 2 | bbb | 2 |
| 3 | ccc | 1 |
| 4 | ddd | 0 |
+--------+------+----------------+
You haven't quite explained what you want to accomplish with the print out but here is an example in PHP: (Use PDO instead of mysql_)
$pdo = new PDO(); // Make your connection here
$stm = $pdo->query('SELECT *, (SELECT COUNT(control.con_id) FROM control WHERE control.art_id = articles.art_id) AS count_from_con FROM articles ORDER BY art_id ASC');
while( $row = $stm->fetch(PDO::FETCH_ASSOC) )
{
echo "Article with id: ".$row['art_id']. " has " .$row['count_from_con'].' connected rows in control.';
}
Alternatively with the mysql_ extension:
$result = mysql_query('SELECT *, (SELECT COUNT(control.con_id) FROM control WHERE control.art_id = articles.art_id) AS count_from_con FROM articles ORDER BY art_id ASC');
while( $row = mysql_fetch_assoc($result) )
{
echo "Article with id: ".$row['art_id']. " has " .$row['count_from_con'].' connected rows in control.';
}
This should be enough examples to help you accomplish what you need.

Duplicate entries in dropdown from mySQL

Okay so I'm new to mySQL. I'm sorry this is a very novice question. Essentially I have two tables, Associates, and keys.
The content is as follows:
associates:
id,
department,
associate,
date_added
keys:
id,
key_name,
date_added,
my code to make my dropdown is as follows:
<?php
mysql_connect('hostname', 'user', 'Password');
mysql_select_db('log');
$key_fetch = "SELECT `associates`.`department`,`associates`.`associate`,`keys`.`key_name` FROM associates , `keys` ORDER BY `key_name` DESC";
$results = mysql_query($key_fetch);
echo "<select name='key_name' size='5'>";
while ($row = mysql_fetch_array($results)) {
echo "<option value='" . $row['key_name'] . "'>" . $row['key_name'] . "</option>";
}
echo "</select>";
?>
The problem is I only have 5 keys and I have ten associates, and this creates duplicates in my dropdown and I can't fix it with SELECT DISTINCT, and I'm not too sure what else to try.
To visualize cartesian product based on above Q comments.
create table t1
( id int auto_increment primary key,
stuff1 varchar(50) not null
);
insert t1 (stuff1) values ('111.1'),('111.2'),('111.3');
create table t2
( id int auto_increment primary key,
stuff2 varchar(50) not null
);
insert t2 (stuff2) values ('222.1'),('222.2'),('222.3');
A: an explicit Join
select t1.id,t1.stuff1,t2.stuff2
from t1
join t2
on t2.id=t1.id;
+----+--------+--------+
| id | stuff1 | stuff2 |
+----+--------+--------+
| 1 | 111.1 | 222.1 |
| 2 | 111.2 | 222.2 |
| 3 | 111.3 | 222.3 |
+----+--------+--------+
B: An old-style cartesian product
select t1.id,t1.stuff1,t2.stuff2
from t1,t2;
+----+--------+--------+
| id | stuff1 | stuff2 |
+----+--------+--------+
| 1 | 111.1 | 222.1 |
| 2 | 111.2 | 222.1 |
| 3 | 111.3 | 222.1 |
| 1 | 111.1 | 222.2 |
| 2 | 111.2 | 222.2 |
| 3 | 111.3 | 222.2 |
| 1 | 111.1 | 222.3 |
| 2 | 111.2 | 222.3 |
| 3 | 111.3 | 222.3 |
+----+--------+--------+
9 rows in set (0.00 sec)
C: Cross join, same output as B:
select t1.id,t1.stuff1,t2.stuff2
from t1 cross join t2
So, your output, as I see it, is like B, 50 rows.

MySQL recursive tree search

I have a database with a tree of names that can go down a total of 9 levels deep and I need to be able to search down a signal branch of the tree from any point on the branch.
Database:
+----------------------+
| id | name | parent |
+----------------------+
| 1 | tom | 0 |
| 2 | bob | 0 |
| 3 | fred | 1 |
| 4 | tim | 2 |
| 5 | leo | 4 |
| 6 | sam | 4 |
| 7 | joe | 6 |
| 8 | jay | 3 |
| 9 | jim | 5 |
+----------------------+
Tree:
tom
fred
jay
bob
tim
sam
joe
leo
jim
For example:
If I search "j" from the user "bob" I should get only "joe" and "jim". If I search "j" form "leo" I should only get "jim".
I can't think of any easy way do to this so any help is appreciated.
You should really consider using the Modified Preorder Tree Traversal which makes such queries much easier. Here's your table expressed with MPTT. I have left the parent field, as it makes some queries easier.
+----------------------+-----+------+
| id | name | parent | lft | rght |
+----------------------+-----+------+
| 1 | tom | 0 | 1 | 6 |
| 2 | bob | 0 | 7 | 18 |
| 3 | fred | 1 | 2 | 5 |
| 4 | tim | 2 | 8 | 17 |
| 5 | leo | 4 | 12 | 15 |
| 6 | sam | 4 | 9 | 16 |
| 7 | joe | 6 | 10 | 11 |
| 8 | jay | 3 | 3 | 4 |
| 9 | jim | 5 | 13 | 14 |
+----------------------+-----+------+
To search j from user bob you'd use the lft and rght values for bob:
SELECT * FROM table WHERE name LIKE 'j%' AND lft > 7 AND rght < 18
Implementing the logic to update lft and rght for adding, removing and reordering nodes can be a challenge (hint: use an existing library if you can) but querying will be a breeze.
There isn't a nice/easy way of doing this; databases don't support tree-style data structures well.
You will need to work on a level-by-level basis to prune results from child-to-parent, or create a view that gives all 9 generations from a given node, and match using an OR on the descendants.
Have you thought about using a recursive loop? i use a loop for a cms i built on top of codeigniter that allows me to start anywhere in the site tree and will then subsequently filter trhough all the children> grand children > great grand children etc. Plus it keeps the sql down to short rapid queries opposed to lots of complicated joins. It may need some modifying in your case but i think it could work.
/**
* build_site_tree
*
* #return void
* #author Mike Waites
**/
public function build_site_tree($parent_id)
{
return $this->find_children($parent_id);
}
/** end build_site_tree **/
// -----------------------------------------------------------------------
/**
* find_children
* Recursive loop to find parent=>child relationships
*
* #return array $children
* #author Mike Waites
**/
public function find_children($parent_id)
{
$this->benchmark->mark('find_children_start');
if(!class_exists('Account_model'))
$this->load->model('Account_model');
$children = $this->Account_model->get_children($parent_id);
/** Recursively Loop over the results to build the site tree **/
foreach($children as $key => $child)
{
$childs = $this->find_children($child['id']);
if (count($childs) > 0)
$children[$key]['children'] = $childs;
}
return $children;
$this->benchmark->mark('find_children_end');
}
/** end find_children **/
As you can see this is a pretty simplfied version and bear in mind this has been built into codeigniter so you will need to modyfy it to suite but basically we have a loop that calls itself adding to an array each time as it goes. This will allow you to get the whole tree, or even start from a point in the tree as long as you have the parent_id avaialble first!
Hope this helps
The new "recursive with" construct will do the job, but I don't know id MySQL supports it (yet).
with recursive bobs(id) as (
select id from t where name = 'bob'
union all
select t.id from t, bobs where t.parent_id = bobs.id
)
select t.name from t, bobs where t.id = bobs.id
and name like 'j%'
There is no single SQL query that will return the data in tree format - you need processing to traverse it in the right order.
One way is to query MySQL to return MPTT:
SELECT * FROM table ORDER BY parent asc;
root of the tree will be the first item of the table, its children will be next, etc., the tree being listed "breadth first" (in layers of increasing depth)
Then use PHP to process the data, turning it into an object that holds the data structure.
Alternatively, you could implement MySQL search functions that given a node, recursively search and return a table of all its descendants, or a table of all its ancestors. As these procedures tend to be slow (being recursive, returning too much data that is then filtered by other criteria), you want to only do this if you know you're not querying for that kind of data again and again, or if you know that the data set remains small (9 levels deep and how wide?)
You can do this with a stored procedure as follows:
Example calls
mysql> call names_hier(1, 'a');
+----+----------+--------+-------------+-------+
| id | emp_name | parent | parent_name | depth |
+----+----------+--------+-------------+-------+
| 2 | ali | 1 | f00 | 1 |
| 8 | anna | 6 | keira | 4 |
+----+----------+--------+-------------+-------+
2 rows in set (0.00 sec)
mysql> call names_hier(3, 'k');
+----+----------+--------+-------------+-------+
| id | emp_name | parent | parent_name | depth |
+----+----------+--------+-------------+-------+
| 6 | keira | 5 | eva | 2 |
+----+----------+--------+-------------+-------+
1 row in set (0.00 sec)
$sqlCmd = sprintf("call names_hier(%d,'%s')", $id, $name); // dont forget to escape $name
$result = $db->query($sqlCmd);
Full script
drop table if exists names;
create table names
(
id smallint unsigned not null auto_increment primary key,
name varchar(255) not null,
parent smallint unsigned null,
key (parent)
)
engine = innodb;
insert into names (name, parent) values
('f00',null),
('ali',1),
('megan',1),
('jessica',3),
('eva',3),
('keira',5),
('mandy',6),
('anna',6);
drop procedure if exists names_hier;
delimiter #
create procedure names_hier
(
in p_id smallint unsigned,
in p_name varchar(255)
)
begin
declare v_done tinyint unsigned default(0);
declare v_dpth smallint unsigned default(0);
set p_name = trim(replace(p_name,'%',''));
create temporary table hier(
parent smallint unsigned,
id smallint unsigned,
depth smallint unsigned
)engine = memory;
insert into hier select parent, id, v_dpth from names where id = p_id;
/* http://dev.mysql.com/doc/refman/5.0/en/temporary-table-problems.html */
create temporary table tmp engine=memory select * from hier;
while not v_done do
if exists( select 1 from names n inner join tmp on n.parent = tmp.id and tmp.depth = v_dpth) then
insert into hier select n.parent, n.id, v_dpth + 1
from names n inner join tmp on n.parent = tmp.id and tmp.depth = v_dpth;
set v_dpth = v_dpth + 1;
truncate table tmp;
insert into tmp select * from hier where depth = v_dpth;
else
set v_done = 1;
end if;
end while;
select
n.id,
n.name as emp_name,
p.id as parent,
p.name as parent_name,
hier.depth
from
hier
inner join names n on hier.id = n.id
left outer join names p on hier.parent = p.id
where
n.name like concat(p_name, '%');
drop temporary table if exists hier;
drop temporary table if exists tmp;
end #
delimiter ;
-- call this sproc from your php
call names_hier(1, 'a');
call names_hier(3, 'k');

Categories