Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
456 views
in Technique[技术] by (71.8m points)

php - Hierarchical Query in MySQL II

I'm trying to figure out a way to display the number of grandchildren, great grandchildren, etc. on a website focusing on animals. Someone told me about a really cool query @ Hierarchical queries in MySQL

Below is my adaptation.

$stm = $pdo->prepare("SELECT  COUNT(@id := (
 SELECT `Taxon`
 FROM gz_life_mammals
 WHERE `Parent` = @id
 )) AS numDescendants
FROM (
SELECT  @id := :MyURL
) vars
STRAIGHT_JOIN gz_life_mammals
WHERE @id IS NOT NULL");
$stm->execute(array(
'MyURL'=>$MyURL
));

while ($row = $stm->fetch())
{
 $ChildrenCount = $row['numDescendants'];
}

echo $ChildrenCount;

I think I have it set up to count children, actually, but I'll work on grandchildren next. Anyway, when I navigate to a species page, it correctly displays a count of 0. But when I navigate to a parent page, I get this error message:

Cardinality violation: 1242 Subquery returns more than 1 row

Can anyone tell me what's going on and how I can fix that?

My database table features animal taxa in a parent-child relationship in the field Taxon, like this:

Taxon | Parent

Mammalia | Chordata

Carnivora | Mammalia

Canidae | Carnivora

Canis | Canidae

Canis-lupus | Canis

To see information about the wolf (Canis lupus), I would navigate to MySite/life/canis-lupus

ON EDIT

Here's the table schema. I can't make it work with SQFiddle, though; one error after another.

CREATE TABLE t (
 N INT(6) default None auto_increment,
 Taxon varchar(50) default NULL,
 Parent varchar(25) default NULL,
 NameCommon varchar(50) default NULL,
 Rank smallint(2) default 0
 PRIMARY KEY (N)
) ENGINE=MyISAM
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Hopefully one would agree that this is not an answer-only Answer without explanation, since the code is quite documented throughout.

Basically, it is a self-join table with a row having a reference to who its parent is. The stored proc will use a worktable to find children, children-of-children, etc. And maintain a level.

For instance, level=1 represents children, level=2 represents grandchildren, etc.

At the end, the counts are retrieved. As the id's are in the worktable, expand as you wish with it.

Schema

create schema TaxonSandbox; -- create a separate database so it does not mess up your stuff
use TaxonSandbox; -- use that db just created above (stored proc created in it)

-- drop table t;
CREATE TABLE t (
 N int auto_increment primary key,
 Taxon varchar(50) not null,
 Parent int not null,   -- 0 can mean top-most for that branch, or NULL if made nullable
 NameCommon varchar(50) not null,
 Rank int not null,
 key(parent)
);
-- truncate table t;
insert t(taxon,parent,NameCommon,rank) values ('FrogGrandpa',0,'',0); -- N=1
insert t(taxon,parent,NameCommon,rank) values ('FrogDad',1,'',0); -- N=2  (my parent is N=1)
insert t(taxon,parent,NameCommon,rank) values ('FrogMe',2,'',0); -- N=3 (my parent is N=2)
insert t(taxon,parent,NameCommon,rank) values ('t4',1,'',0); -- N=4 (my parent is N=2)

insert t(taxon,parent,NameCommon,rank) values 
('t5',4,'',0),('t6',4,'',0),('t7',5,'',0),('t8',5,'',0),('t9',7,'',0),('t10',7,'',0),('t11',7,'',0),('t12',11,'',0);

Stored Procedure

use TaxonSandbox;

drop procedure if exists showHierarchyUnder;
DELIMITER $$ -- will be discussed separately at bottom of answer
create procedure showHierarchyUnder
(
theId int -- the id of the Taxon to search for it's decendants (my awkward verbiage)
)
BEGIN
    -- theId parameter means i am anywhere in hierarchy of Taxon
    -- and i want all decendent Taxons
    declare bDoneYet boolean default false;
    declare working_on int;
    declare next_level int; -- parent's level value + 1
    declare theCount int;

    CREATE temporary TABLE xxFindChildenxx
    (   -- A Helper table to mimic a recursive-like fetch
        N int not null, -- from OP's table called 't'
        processed int not null, -- 0 for not processed, 1 for processed
        level int not null, -- 0 is the id passed in, -1=trying to figure out, 1=children, 2=grandchildren, etc
        parent int not null -- helps clue us in to figure out level
        -- NOTE: we don't care about level or parent when N=parameter theId passed into stored proc
        -- in fact we will be deleting that row near the bottom or proc
    );

    set bDoneYet=false;
    insert into xxFindChildenxx (N,processed,level,parent) select theId,0,0,0;  -- prime the pump, get sp parameter in here

    -- stay inside below while til all retrieved children/children of children are retrieved
    while (!bDoneYet) do
        -- see if there are any more to process for children
        -- simply look in worktable for ones where processed=0;
        select count(*) into theCount from xxFindChildenxx where processed=0;

        if (theCount=0) then 
            -- found em all, we are done inside this while loop
            set bDoneYet=true;
        else
            -- one not processed yet, insert its children for processing
            SELECT N,level+1 INTO working_on,next_level FROM xxFindChildenxx where processed=0 limit 1; -- order does not matter, just get one

            -- insert the rows where the parent=the one we are processing (working_on)
            insert into xxFindChildenxx (N,processed,level,parent)
            select N,0,next_level,parent
            from t
            where parent=working_on;

            -- mark the one we "processed for children" as processed
            -- so we processed a row, but its children rows are yet to be processed
            update xxFindChildenxx set processed=1 where N=working_on;
        end if;
    end while;

    delete from xxFindChildenxx where N=theId;  -- don't really need the top level row now (stored proc parameter value)
    select level,count(*) as lvlCount from xxFindChildenxx group by level;
    drop table xxFindChildenxx;
END
$$ -- tell mysql that it has reached the end of my block (this is important)
DELIMTER ; -- sets the default delimiter back to a semi-colon

Test Stored Proc

use TaxonSandbox; -- create a separate database so it does not mess up your stuff
call showHierarchyUnder(1);
+-------+----------+
| level | lvlCount |
+-------+----------+
|     1 |        2 |
|     2 |        3 |
|     3 |        2 |
|     4 |        3 |
|     5 |        1 |
+-------+----------+

So there are 2 children, 3 grandchildren, 2 great-grandchildren, 3 great-great, and 1 great-great-great

Were one to pass an id to the stored proc that does not exist, or one that has no children, no result set rows are returned.

Edit: other comments, due to leaving the OP hanging on understanding his first stored proc creation I believe. Plus other questions that point back here.

Delimiters

Delimiters are important to wrap the block of the stored proc creation. The reason is so that mysql understands that the sequence of statements that follow are still part of the stored proc until it reaches the specified delimiter. In the case above, I made up one called $$ that is different from the default delimiter of a semi-colon that we are all used to. This way, when a semi-colon is encountered inside the stored proc during creation, the db engine will just consider it as one the many statements inside of it instead of terminating the stored proc creation. Without doing this delimiter wrapping, one can waste hours trying to create their first stored proc getting Error 1064 Syntax errors. At the end of the create block I merely have a line

$$

which tell mysql that that is the end of my creation block, and then the default delimiter of a semi-colon is set back with the call to

DELIMITER ;

Mysql manual page Using Delimiters with MySqlScript. Not a great manual page imo, but trust me on this one. Same issue when creating Triggers and Events.

PHP

To call this stored proc from php, it is just a string, "call showHierarchyUnder(1)". It returns a result set as described above, which, as described, can return a result set with no rows.

Remember that the 1 is a parameter to the stored proc. And that this exists in a database created, called TaxonSandbox if you followed the above.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...