Question

MySQL Nested Set / Pre-order tree query

Asked by: killer455

We are using nested sets / pre-order tree to store hierarchical data (categories).

Example:
http://dev.mysql.com/tech-resources/articles/hierarchical-data.html

We need a single query that will select the children of a specific category and also their children.  --BUT-- be able to limit the number of second level children.

Example.  We have a table which has:
id
title
left_
right_
level - stores the deepness level of the category (1, 2, 3, etc)

So say we want to find the children of a node which has ID of 12.

We get the left_ and right_ values where ID = 12.

We then select the children which have left_ and right_ in between the left_ and right_ values of ID 12 and only get categories which have 1 or 2 levels deeper (children and sub children).

I want to limit how many sub children get returned.

So that way we get a list of categories with X amount of sub children for each category.

This Question has been solved and asker verified All Experts Exchange premium technology solutions are available to subscription members.

Subscribe now for full access to Experts Exchange and get

Instant Access to this Solution

  • Plus...
  • 30 Day FREE access, no risk, no obligation
  • Collaborate with the world's top tech experts
  • Unlimited access to our exclusive solution database
  • Never be left without tech help again

Subscribe Now

Asked On
2009-09-10 at 18:18:27ID24723383
Topics

MySQL Server

,

Databases Miscellaneous

Participating Experts
1
Points
300
Comments
22

Trusted by hundreds of thousands everyday for fast, accurate and reliable tech support.

  • "The time we save is the biggest benefit of Experts Exchange to Warner Bros. What could take multiple guys 2 hours or more each to find is accessed in around 15 minutes on Experts Exchange." Mike Kapnisakis, Warner Bros.
  • "Our team likes having a resource that is more secure than just using Google and most experts using this service really know their stuff. It's nice to look here first versus using Google." Dayna Sellner, Lockheed Martin
  • "Anytime that I've been stumped with a problem, 9 out of 10 times Experts Exchange has either the accepted solution or an open discussion of the potential solution to the problem." Kenny Red, eBay Inc.

See what Experts Exchange can do for you.

Got a question?

We've got the answer.

Experts Exchange has been collecting answers to technology questions since 1996…3 million and counting! If you have a question, chances are we already have your answer.

Screenshot of Experts Exchange Knowledgebase

Need individual assistance?

Our experts are ready to help.

If you can't find the exact answer you're looking for, ask our exclusive community of 50,000 experts. You’ll get a personalized answer from a trusted professional.

Screenshot of Experts Exchange Knowledgebase

Want to learn from the best?

Read articles from industry experts.

Thousands of free tech tips, tricks, how-to’s and tutorials are available in our peer reviewed articles section. See for yourself how smart our experts are, no login required.

Screenshot of an Article

Working on a long term project?

Store your work and research.

Save solutions to your questions, answers you’ve discovered through searching plus helpful articles in your personal knowledgebase for easy future access.

Screenshot of Experts Exchange Knowledgebase

Access the answers to your technology questions today.

Subscribe Now

30-day free trial. Register in 60 seconds.

What Makes Experts Exchange Unique?

Members of the expert community talk about why the experience at Experts Exchange is different than what you will find anywhere else.

Trusted by the world's most respected brands.

image of each brand's logo

Faithfully serving IT professionals since 1996.

Experts Exchange Logo

Try it out and discover for yourself.

Subscribe Now

30-day free trial. Register in 60 seconds.

Related Solutions

  1. URGENT: SQL Procedure With Nested Loops For Hierarchi…
    Ok, let me try and explain the situation-- I have a database that stores parts, circuit cards, LRUs, systems, etc... It is stored in a hierarchical format for example: (Note: NHA is the next higher assemble, or what the part belongs to) ID Part NHA ModuleType ...
  2. hierarchical data
    Hello everybody, I have the following table structure (along with an example): ID name parent ============================== 1 MainNode 0 2 SubNode 1 3 MainNode 0 4 SubNode 3 5 SubNode 2 6 SubNode 2 (table name: `category`) The way I thought it is like this: If 'pa...
  3. hierarchical-data in catagory's how to add via php
    http://dev.mysql.com/tech-resources/articles/hierarchical-data.html using the above reference how does one add a child node and a node to the tree via php code if we instead want to add a node as a child of a node that has no existing children SELECT @myLeft := lft FROM nes...

Free Tech Articles

  1. WARNING: 5 Reasons why you should NEVER fix a computer for free.
    It is in our nature to love the puzzle. We are obsessed. The lot of us. We love puzzles. We love the challenge. We thrive on finding the answer. We hate disarray. It bothers us deep in our soul. W...
  2. SCCM OSD Basic troubleshooting
    SCCM 2007 OSD is a fantastic way to deploy operating systems, however, like most things SCCM issues can sometimes be difficult to resolve due to the sheer volume of logs to sift through and the dispe...
  3. Migrate Small Business Server 2003 to Exchange 2010 and Windows 2008 R2
    This guide is intended to provide step by step instructions on how to migrate from Small Business Server 2003 to Windows 2008 R2 with Exchange 2010. For this migration to work you will need the fo...
  4. Create a Win7 Gadget
    This article shows you how to create a simple "Gadget" -- a sort of mini-application supported by Windows 7 and Vista. Gadgets can be dropped anywhere on the desktop to provide instant information, ...
  5. Outlook continually prompting for username and password
    There have been a lot of questions recently regarding Outlook prompting for a username and password whilst using Exchange 2007. There are a few reasons why this would happen and I will try to cover t...
  6. Backup Exchange 2010 Information Store using Windows Backup
    There seems to be quite a lot of confusion around the ability to backup Exchange 2010 using the built in Windows Backup feature. This stems from the omission of this feature prior to Exchange 2007 s...

Cloud Class Webinars

  1. Avoiding Bugs in Microsoft Access
    Alison Balter takes and in-depth look at avoiding bugs in Access. In this webinar you will learn about using the immediate window to debug your applications, invoking the debugger, using breakpoints to troubleshoot, stepping through code, setting the next statement to execute, ...
  2. Top 10 Best New Features in Visio 2010
    Scott Helmers gives live demonstrations of the top 10 new features in Visio 2010. This webinar will teach you how to create compelling diagrams by adding shapes to the page with a single click, linking the shapes in a diagram to data in Excel (or SQL Server, or SharePoint), ...
  3. IT Consultant Business Secrets Revealed
    Michael Munger, Experts Exchange tech pro and IT consultant, pulls back the curtain on his very successful businesses and answers question on every IT consultant and business owner should know about. He shares secrets on what he did to solve the 5 most common problems in IT, ...
  4. Disaster Recovery and Business Continuity
    Quest CTO, Mike Billon, gives an overview of the steps involved in building a dunamic disaster recovery plan. Through case studies and an examination of software/hardware tooles for monitoring and testing, you'll gain a better understandin of where you are, where you want ...
  5. Organize Your Visio Diagrams with Containers and Lists
    Scott Helmers uses cross functional flowcharts, wireframe diagrams, data graphic legends and seating charts to teach you: how to ustilize all three new structured diagram components in Visio 2010, the best practices for organizeing shapes in previous version of Visio, how to organize ...
  6. How to Us Objects, Properties, Events and Methods in Microsoft Access
    Alison Dalter gives an in-depbth look at objects, properties, events and methods in Microsoft Access. In this webinar you will learn about using the object browser, referring to objects, working with properties and methods, working with object variables, understanding the ...

Join the Community

Give a Little. Get a Lot.

Join the community of experts here and help other tech pros by answering question in your area of expertise. You can earn FREE access to all Experts Exchange's premium features and resources.

Join the Community

Answers

 

by: mwvisa1Posted on 2009-09-11 at 10:52:59ID: 25311853

killer455,

Although IMHO this is not a 125 point question, I found the challenge quite interesting as my first thought was use UNION to give you the flexibility of getting all children on first level but X number of children on second level with LIMIT to control the number of records; however, LIMIT will not care if you get X number per category, it will just return X number.

With that in mind, I figured you needed a row_number() over() style syntax for MySQL like there is for Oracle and MS SQL and as far as I knew there was not one; hence, I took on the challenge.

My recommendation would be to create a view; however, I will show how to do this without one as well.

===== solution 1 : if can create view(s) ======
1. create view of child categories by parent.

create view vw_childcategories
as
select c.id, c.title, c.left_, c.right_,
c.`level`, p.id as parent
from category c
inner join category p
  on c.left_ between p.left_ and p.right_
  and c.right_ between p.left_ and p.right_
  and c.`level` = coalesce(p.`level`,0)+1
;


2. Query for children of parent = 12 and union that with sub children.

select id, title,
left_, right_
level, parent
from vw_childcategories
where parent = 12

union

select id, title,
left_, right_
level, parent
from (

select cc.*,

/* ranking trick to simulate row_number() over() */
find_in_set(cc.id, p.children) as rank

from vw_childcategories as cc
inner join (

   /* get listing of ids by parent */
   select parent as id,
   group_concat(id) as children
   from vw_childcategories
   group by parent

) as p on p.id = cc.parent
where exists ( /* children of parent 12 now become parents */
   select 1
   from vw_childcategories
   where parent = 12
   and id = cc.parent
)

) derived
where rank <= 1 /* change 1 to appropriate X number of sub children */
order by parent, id
;

Here is a good reference on what I am doing with group_concat and find_in_set:
http://onlamp.com/pub/a/mysql/2007/03/29/emulating-analytic-aka-ranking-functions-with-mysql.html?page=1

========================================

Now if you can't create the view, then go with this:

===== solution 2 : if cannot create view(s) ======

/* get children */
select c.id, c.title, c.left_, c.right_,
c.`level`, p.id as parent
from category c
inner join category p
  on c.left_ between p.left_ and p.right_
  and c.right_ between p.left_ and p.right_
  and c.`level` = coalesce(p.`level`,0)+1
where p.id = 12

union

select id, title, left_, right_, `level`, parent
from (

/* get sub children */
select c.id, c.title, c.left_, c.right_, c.`level`, p.id as parent,

/* ranking trick to simulate row_number() over() */
find_in_set(c.id, p_.children) as rank

from category c
inner join category p
  on c.left_ between p.left_ and p.right_
  and c.right_ between p.left_ and p.right_
  and c.`level` = coalesce(p.`level`,0)+1
inner join (

   /* get listing of ids by parent */
   select p.id,
   group_concat(c.id) as children
   from category c
   inner join category p
     on c.left_ between p.left_ and p.right_
     and c.right_ between p.left_ and p.right_
     and c.`level` = coalesce(p.`level`,0)+1
   group by p.id

) as p_ on p_.id = p.id
where exists (
  select 1
  from category c1
  inner join category p1
    on c1.left_ between p1.left_ and p1.right_
    and c1.right_ between p1.left_ and p1.right_
    and c1.`level` = coalesce(p1.`level`,0)+1
  where p1.id = 12 and c1.id = p.id
)

) derivedb
where rank <= 1 /* change 1 to appropriate X number of sub children */
order by parent, id

========================================

Hope that helps.

Regards,

Kevin

 

by: mwvisa1Posted on 2009-09-11 at 10:59:34ID: 25311936

And while I am at it, here was the original suggestion I had before finding that nice approach using find_in_set() and group_concat().  It uses a technique discussed here:

http://jimlife.wordpress.com/2008/09/09/displaying-row-number-rownum-in-mysql/

Showing as it is actually less code if you are stuck with not being able to create a view.  Therefore, if you cannot create a view this may be an alternative.

/* get children */
select c.id, c.title, c.left_, c.right_,
c.`level`, p.id as parent
from category c
inner join category p
  on c.left_ between p.left_ and p.right_
  and c.right_ between p.left_ and p.right_
  and c.`level` = coalesce(p.`level`,0)+1
where p.id = 12
 
union
 
select id, title, left_, right_, `level`, parent
from (
 
/* get sub children */
select c.id, c.title, c.left_, c.right_, c.`level`,
 
/* emulate row_number() over() */
if(p.id = @lastparent, @rownum:=@rownum+1, @rownum:=1) as rank,
 
/* since using @lastparent in @rownum,
  have to set @lastparent 2nd;
  otherwise, p.id will always equal @lastparent in our if().
*/
@lastparent:=p.id as parent
 
from (select @rownum:=0, @lastparent:=null) r, category c
inner join category p
  on c.left_ between p.left_ and p.right_
  and c.right_ between p.left_ and p.right_
  and c.`level` = coalesce(p.`level`,0)+1
where exists (
  select 1
  from category c1
  inner join category p1
    on c1.left_ between p1.left_ and p1.right_
    and c1.right_ between p1.left_ and p1.right_
    and c1.`level` = coalesce(p1.`level`,0)+1
  where p1.id = 12 and c1.id = p.id
)
order by p.id
 
) derivedb
where rank <= 2

                                              
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:

Select allOpen in new window

 

by: killer455Posted on 2009-09-11 at 11:01:27ID: 25311956

Wow thanks.  It may take me a while to review this and test it out, but how do you feel this will perform on large data sets (large number of categories)?

 

by: mwvisa1Posted on 2009-09-11 at 11:43:59ID: 25312315

It is harder to tell with small row set, but here is what I could gather.

All three of these solutions are pretty comparable:

(1) No View - Using @RowNum -- http:#25311936

5 rows fetched in 0.243s (0.0051s)

(2) No View - Using Find_In_Set() / Group_Concat() -- http:#25311853

5 rows fetched in 0.0032s (0.0065s)

(3) View - Using Find_In_Set() / Group_Concat() -- http:#25311853

5 rows fetched in 0.0033s (0.1200s)


#1 seems to be the faster of the three.  EXPLAIN plan for each is also pretty similar, but shows better for the #1 solution due to not requiring the extra inner query to get the group_concat() on parent category.  

With a little work, can probably modify the other no view solution to combine the join on category to determine parent/child with join to get children list of that parent.  For that matter, may be able to do ranking before storing in the view.

Can try for yourself using this as a helpful guide to tweaking / troubleshooting for performance:
3 Ways to Speed Up MySQL - http:viewArticle.jsp?aid=1250

The biggest key I found was to have an index on level column.


M-1

 

by: mwvisa1Posted on 2009-09-11 at 11:45:37ID: 25312328

>>The biggest key I found was to have an index on level column.

That is with assumption that id is already indexed as primary key.

 

by: mwvisa1Posted on 2009-09-12 at 07:53:45ID: 25316695

More efficient:
(no UNION, using view shown above)

select c.id, title, 
left_, right_,
level, parent
from vw_childcategories c
where parent = 12 or
(
 
/* handles sub children */
exists (select 1
   from vw_childcategories
   where parent = 12
   and id = c.parent)
 
and
 
/* handles X # of sub children */
find_in_set(c.id,
   (select group_concat(id) as children
    from vw_childcategories
    where parent = c.parent
    group by parent
  )) <= 2
 
)
order by parent, id
;

                                              
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:

Select allOpen in new window

 

by: killer455Posted on 2009-09-12 at 08:01:21ID: 25316724

Unfortunately I won't be able to use views.  I will review the non-review method unless you have a more optimized version of it?

 

by: mwvisa1Posted on 2009-09-12 at 08:14:36ID: 25316770

Give me a second.  I updated data to have a larger number of records, so can better tell efficiency.  Will try with no view and post back.

 

by: mwvisa1Posted on 2009-09-12 at 08:23:42ID: 25316793

Here you go:

select c.id, c.title, c.left_, c.right_,
c.`level`, p.id as parent
from category c
inner join category p
  on c.left_ between p.left_ and p.right_
  and c.right_ between p.left_ and p.right_
  and c.`level` = coalesce(p.`level`,0)+1
where p.id = 12 /* get children */
or (
 
/* get sub children */
exists (
  select 1
  from category c1
  inner join category p1
    on c1.left_ between p1.left_ and p1.right_
    and c1.right_ between p1.left_ and p1.right_
    and c1.`level` = coalesce(p1.`level`,0)+1
  where p1.id = 12 and c1.id = p.id
)
 
and
 
/* filter X # of sub children */
find_in_set(c.id,
   (select group_concat(c1.id) as children
    from category c1
    inner join category p1
      on c1.left_ between p1.left_ and p1.right_
      and c1.right_ between p1.left_ and p1.right_
      and c1.`level` = coalesce(p1.`level`,0)+1
    where p1.id = p.id
    group by p1.id
  )) <= 2
 
)
order by parent, id
;

                                              
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:

Select allOpen in new window

 

by: killer455Posted on 2009-09-12 at 09:20:47ID: 25316962

You mentioned you had a larger data set to test on, what kind of numbers were we talking about?

 

by: mwvisa1Posted on 2009-09-12 at 10:32:45ID: 25317199

Not huge, just more than 5 records so could see actual impact on EXPLAIN plan.  Think about 60 records.  This is more efficient based on how the indexes get used.  The indexes I have are on left_ and right_ together and both are integers.  Another index on primary key id.  And another on level which is an integer also.

 

by: killer455Posted on 2009-09-12 at 10:51:21ID: 25317259

I tested it out on a set of 2000 categories.

I have the same indexes mentioned.

But it is slow.  Showing rows 0 - 29 (103 total, Query took 3.1897 sec)

EXPLAIN says:
1        PRIMARY        p        ALL        PRIMARY        NULL        NULL        NULL        2023        Using where
1       PRIMARY       c       ALL       left_,level       NULL       NULL       NULL       1518       Range checked for each record (index map: 0x6)

The results look ok but is there anyway to order these so we get the category, then the subcategories, then the next main category, then the subcategories? and so on?

 

by: mwvisa1Posted on 2009-09-12 at 11:13:35ID: 25317339

You can probably try some of the other approaches mentioned without views and see if any perform better for you.

For the sort, you will need something like:

order by if(p.id=12,c.id,p.id), c.level, c.id

 

by: killer455Posted on 2009-09-12 at 11:14:45ID: 25317347

The code below was the one I tried with the slow results.

select c.id, c.title, c.left_, c.right_,
c.`level`, p.id as parent
from category c
inner join category p
  on c.left_ between p.left_ and p.right_
  and c.right_ between p.left_ and p.right_
  and c.`level` = coalesce(p.`level`,0)+1
where p.id = 12 /* get children */
or (
 
/* get sub children */
exists (
  select 1
  from category c1
  inner join category p1
    on c1.left_ between p1.left_ and p1.right_
    and c1.right_ between p1.left_ and p1.right_
    and c1.`level` = coalesce(p1.`level`,0)+1
  where p1.id = 12 and c1.id = p.id
)
 
and
 
/* filter X # of sub children */
find_in_set(c.id,
   (select group_concat(c1.id) as children
    from category c1
    inner join category p1
      on c1.left_ between p1.left_ and p1.right_
      and c1.right_ between p1.left_ and p1.right_
      and c1.`level` = coalesce(p1.`level`,0)+1
    where p1.id = p.id
    group by p1.id
  )) <= 2
 
)
order by parent, id
;

                                              
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:

Select allOpen in new window

 

by: mwvisa1Posted on 2009-09-12 at 11:16:14ID: 25317356

I understood that, so I was saying you could try some of the UNION variations to see if works better with your actual data.

 

by: killer455Posted on 2009-09-15 at 17:13:55ID: 25340788

Unfortunately I could not get any of these to return decent speeds.

 

by: mwvisa1Posted on 2009-09-15 at 17:37:23ID: 25340870

3 seconds is not decent speed?  And the question was how to do this?  My providing extra work towards efficiency was bonus with all due respect.  You asked how this was possible.  The fastest method as I told you involved views.  I cannot control your not being able to create a view in your database.  Anyway, good luck -- optimizing is very difficult.  Doing some now myself, it is very tedious so all the best to you.

Regards,

Kevin

 

by: killer455Posted on 2009-09-15 at 17:42:30ID: 25340892

3 seconds is not a decent speed, no.

Especially when the query is used on a high traffic website on the home page.

Yes, the question was how to do this.  We simply assumed that the answer given would take into account speed.  I didn't think we had to ask for it to be a optimized query, I simply assumed this was obvious as it should be for -any- SQL query.

As far as not being able to use views, we simply said a single SQL query.  I figured it was obvious there as well, as we did not mention a "single SQL query with a view".

Thanks for your detailed answer and hard work so no disrespect, but it is still not what we need as its slow therefore making it applicable for what we need.

 

by: mwvisa1Posted on 2009-09-15 at 17:52:12ID: 25340929

:) As I said good luck!

 

by: mwvisa1Posted on 2009-10-09 at 15:20:00ID: 25539343

All of my answers above starting with http:#a25311853 were tested and *work* to create the query as requested; however, asker was never able to get this to function at a good performance level in their environment.

My take was this was a task that could not  be done by them previously and so, although not optimal, was better than nothing; however, we never came to agreement on that point.  IMHO, there are some tasks that unfortunately are resource intensive because of what is being asked and how the data was designed originally, but can also respect point of view that my response didn't meet their needs.

So in short, "PAQ Refund" maybe?  This would allow future readers who may be able to benefit find this information while giving points back to this particular asker since I have not fulfilled their needs.

Regards,

Kevin

20120131-EE-VQP-002

3 Ways to Join

30-Day Free Trial

The Experts

98% positive feedback on 31,087 answers since March 2000. angeliii is a Microsoft Most Valuable Professional for his work with MS SQL Server & Develoment.

He has also proven his knowledge of Visual Basic Programming, PHP Scripting and Oracle Databases.

The Experts

97% positive feedback on 10,752 answers since July 2000. lrmoore has more than 18 years experience in the networking industry.

The six-time Mircosoft MVPs specialties include firewalls, virtual private networking, and network management.

Testimonials

"...and excellent source for support... Kind of like having your very own IT dept." Electriciansnet

Testimonials

"I was apprehensive at signing up at first. However... it has already made my life as an IT administrator much easier." JaCrews

Testimonials

"WOW! You guys have great, active, and knowledgeable people on here." moore50

Business Clients

Business Clients

In the Press

"If you’ve got a question... Experts Exchange can supply an answer.”

In the Press

"...an invaluable aid for both IT professionals and those who require tech support."

In the Press

"where IT professionals provide quick answers on just about any topic"

Business Account Plans

Loading Advertisement...