简体   繁体   中英

Self-join Query

Is it possible to do parent-child query just using join without looping through temporary table?

Database sample :

menuid  name                parent  url
----------------------------------------------------------
A0000   Master              A0000   #
A0001   Rekening            A0000   /master/rekening.aspx
A0002   Master Nominal      A0001   /master/nominal.aspx
A0003   Master Satuan Other A0001   /master/satuan.aspx
A0004   Master Kondisi      A0000   /master/kondisi.aspx
A0005   Master Tujuan       A0003   /master/tujuan.aspx
A0006   Master Item         A0003   /master/item.aspx
A0007   Master Warehouse    A0000   /master/warehouse.aspx
A0008   Master Kapal        A0006   /master/kapal.aspx

Desired result if choosed uri = '/master/kapal.aspx' :

menuid  name                parent  url
----------------------------------------------------------
A0000   Master              A0000   #
A0001   Rekening            A0000   /master/rekening.aspx
A0003   Master Satuan Other A0001   /master/satuan.aspx
A0006   Master Item         A0003   /master/item.aspx
A0008   Master Kapal        A0006   /master/kapal.aspx

Desired result if choosed uri = /master/tujuan.aspx' :

menuid  name                parent  url
----------------------------------------------------------
A0000   Master              A0000   #
A0001   Rekening            A0000   /master/rekening.aspx
A0005   Master Tujuan       A0003   /master/tujuan.aspx

Sample query :

declare @menuid varchar(255) = 'menuid'
declare @parent varchar(255) = 'parent'
declare @temp_parent varchar(255)
declare @i smallint = 0

delete from temp_menu
while (@menuid <> @parent)
begin
  if(@i = 0) 
  begin
    insert into temp_menu
    select * from menu where uri = '/master/kapal.aspx'
    select @menuid = menuid, @parent = parent from menu where uri = '/master/kapal.aspx'
    set @i = 1;
    end
  else
  begin
    insert into temp_menu
    select * from menu where menuid = @parent
    select @menuid = menuid, @temp_parent = parent from menu where menuid = @parent
    set @parent = @temp_parent;
    end
end
select * from temp_menu

Sample with hieararchy :

A0000
|_______________________
|               |       |
A0001           A0004   A0007
|________
|       |
A0002   A0003
        |_______
        |       |
        A0005   A0006
                |
                A0008

UPDATED : I want to get all rows from the longest branch possible from nodes parent to menuid and stopped if the parent same with menuid or there is no menuid match with parent .

ADDED WITH SCRIPT AND SAMPLES

IF OBJECT_ID('dbo.menu', 'U') IS NOT NULL
  DROP TABLE dbo.menu
GO

IF OBJECT_ID('dbo.temp_menu', 'U') IS NOT NULL
  DROP TABLE dbo.temp_menu
GO

IF OBJECTPROPERTY(object_id('dbo.sp_get_parent'), N'IsProcedure') = 1
  DROP PROCEDURE dbo.sp_get_parent
GO

create table dbo.menu (
menuid varchar(255)
, name varchar(255)
, parent varchar(255)
, uri varchar(255)
);

insert into dbo.menu (menuid, name, parent, uri)
values ('A0000', 'Master', 'A0000', '#')
, ('A0001', 'Rekening', 'A0000', '/master/rekening.aspx')
, ('A0002', 'Master Nominal', 'A0001', '/master/nominal.aspx')
, ('A0003', 'Master Satuan Other', 'A0001', '/master/satuan.aspx')
, ('A0004', 'Master Kondisi', 'A0000', '/master/kondisi.aspx')
, ('A0005', 'Master Tujuan', 'A0003', '/master/tujuan.aspx')
, ('A0006', 'Master Item', 'A0003', '/master/item.aspx')
, ('A0007', 'Master Warehouse', 'A0000', '/master/warehouse.aspx')
, ('A0008', 'Master Kapal', 'A0006', '/master/kapal.aspx');

create table dbo.temp_menu (
menuid varchar(255)
, name varchar(255)
, parent varchar(255)
, uri varchar(255)
);

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

Create PROCEDURE [dbo].[sp_get_parent]
@uri VARCHAR (255)
AS

declare @menuid varchar(255) = 'menuid'
declare @parent varchar(255) = 'parent'
declare @temp_parent varchar(255)
declare @i smallint = 0

delete from temp_menu
while (@menuid <> @parent)
begin
  if(@i = 0) 
  begin
    insert into temp_menu
    select * from menu where uri = @uri
    select @menuid = menuid, @parent = parent from menu where uri = @uri
    set @i = 1;
  end
  else
  begin
    insert into temp_menu
    select * from menu where menuid = @parent
    select @menuid = menuid, @temp_parent = parent from menu where menuid = @parent
    set @parent = @temp_parent;
  end
end
select * from temp_menu order by menuid asc
GO

For desired sample above can try this query :

sp_get_parent '/master/kapal.aspx'

AND

sp_get_parent '/master/tujuan.aspx'

In SQL Server, the answer to every question about how to query hierarchical data is using a recursive common table expression.

In your case, since you want to get the longest branch, you should add a counting column:

;WITH CTE AS
(
     SELECT menuid, name, parent, url, 0 as level
     FROM menu WHERE parent = menuid -- Usually, the parent column is simply nullable
     UNION ALL
     SELECT menu.menuid, menu.name, menu.parent, menu.url, level + 1
     FROM menu 
     INNER JOIN CTE ON menu.parent = CTE.menuid 
     AND menu.parent <> CTE.parent -- This is why parent column is nullable :-)
)

SELECT TOP 1 *
FROM CTE
ORDER BY Level DESC 

This query will get you the leaf that's the furthest away from it's top parent.

Update
Based on your comment, I think this is what you are looking for:

;WITH CTERecursion AS
(
     SELECT menuid, 
            name, 
            parent, 
            url, 
            0 as level,
            menuid as TopLevelParent
     FROM menu WHERE parent = menuid -- Usually, the parent column is simply nullable

     UNION ALL
     SELECT menu.menuid, 
            menu.name, 
            menu.parent, 
            menu.url, 
            level + 1,
            TopLevelParent
     FROM menu 
     INNER JOIN CTERecursion CTE ON menu.parent = CTE.menuid 
     AND menu.menuid <> CTE.menuid -- This is why parent column is nullable :-)

), CTELongestPath AS
(
    SELECT TOP 1 TopLevelParent
    FROM CTERecursion
    ORDER BY Level DESC 
)

SELECT menuid, name, parent, url
FROM CTERecursion r
INNER JOIN CTELongestPath l ON r.TopLevelParent = r.TopLevelParent

Update #2
Now that your question is changed, you seem to just want to traverse from leaf to top parent. In that case, your recursive CTE should be something like this:

DECLARE @url varchar(100) = '/master/kapal.aspx';

;WITH CTERecursion AS
(
     SELECT menuid, 
            name, 
            parent, 
            url
     FROM menu 
     WHERE url = @url

     UNION ALL
     SELECT menu.menuid, 
            menu.name, 
            menu.parent, 
            menu.url
     FROM menu 
     INNER JOIN CTERecursion CTE ON menu.menuid = CTE.parent
     AND menu.menuid <> CTE.menuid -- This is why parent column is nullable :-)
)

SELECT menuid, name, parent, url
FROM CTERecursion 
drop table if exists dbo.Menu;

create table dbo.Menu (
menuid varchar(100)
, name varchar(100)
, parent varchar(100)
, url varchar(100)
);

insert into dbo.Menu (menuid, name, parent, url)
values ('A0000', 'Master', 'A0000', '#')
, ('A0001', 'Rekening', 'A0000', '/master/rekening.aspx')
, ('A0002', 'Master Nominal', 'A0001', '/master/nominal.aspx')
, ('A0003', 'Master Satuan Other', 'A0001', '/master/satuan.aspx')
, ('A0004', 'Master Kondisi', 'A0000', '/master/kondisi.aspx')
, ('A0005', 'Master Tujuan', 'A0003', '/master/tujuan.aspx')
, ('A0006', 'Master Item', 'A0003', '/master/item.aspx')
, ('A0007', 'Master Warehouse', 'A0000', '/master/warehouse.aspx')
, ('A0008', 'Master Kapal', 'A0006', '/master/kapal.aspx');

with cteMenu as (
select
    m.menuid, m.name, m.parent, m.url
    , convert(varchar(max), '.' + m.menuid + '.') as Hierarchy
    , 0 as Lvl
from dbo.Menu m
where m.menuid = m.parent

union all

select
    m.menuid, m.name, m.parent, m.url
    , cm.Hierarchy + m.menuid + '.' as Hierarchy
    , cm.Lvl + 1 as Lvl
from dbo.Menu m
    inner join cteMenu cm on m.parent = cm.menuid
where m.menuid <> m.parent
)
select
cm.menuid, cm.name, cm.parent, cm.url
from (
select
    top(1)
    cm.*
from cteMenu cm
order by cm.Lvl desc
) t
    inner join cteMenu cm on t.Hierarchy like cm.Hierarchy + '%'

Use a common table expression (CTE) like this:

WITH cte_name AS
(
    SELECT <base_elements> FROM <table_name> WHERE <root_condition>
    UNION ALL
    SELECT <child_elements> 
    FROM cte_name 
    JOIN <table_name> ON cte_name.id = <table_name>.parentid
)
SELECT DISTINCT * FROM cte_name

What this does is it will select all elements that are root elements. In your case, this would be something like menuid = 'A0000' .

It then unions all of these back onto itself and joins the original table again as long as it finds matches for parentId to (child)-Id in your case the on condition would be menu.parent = cte.menuid .

And then it selects everything distinct from whatever that recursive query returns netting you all paths. If you want to get the maximum depth, you will have to add a constant value on the root select like (0) and then subsequently increase it with every union all. Then you can select a max(nesting_level) from the final distinct query.

In your case something like this could work

WITH cteMenu AS
(
    SELECT menuid, name, parent, url, 0 as nesting_level FROM menu WHERE menuid = 'A0000'
    UNION ALL
    SELECT menu.menuid, menu.name, menu.parent, menu.url, nesting_level + 1 
        FROM menu 
        JOIN cteMenu ON menu.parent = cteMenu.menuid
        WHERE menu.id <> 'A0000'
)
SELECT DISTINCT * FROM cteMenu WHERE nesting_level = (SELECT MAX(nesting_level) FROM cteMenu)

/update: added the WHERE-clause WHERE menu.id <> 'A0000' to the inner select, this removes the root element from the selection and stops the infinite recursion.

declare @uri nvarchar(256) = '/master/kapal.aspx'

; with cte as (
    select MenuId, Name, Parent, Url
    , 1 reverseOrder
    from MyTable
    where url = @uri

    union all

    select b.MenuId, b.Name, b.Parent, b.Url
    , a.reverseOrder + 1
    from cte a
    inner join MyTable b
    where b.MenuId = a.Parent
    and b.MenuId = a.MenuId --don't repeat ourselves after reaching the root
)
select MenuId, Name, Parent, Url
from cte
ordre by reverseOrder desc

Update

For the second part of your question (ie longest branch), try:

; with cte as (
    select MenuId, Name, Parent, Url
    , 1 branchLength
    , cast(MenuId as nvarchar(max)) branchPath
    from MyTable
    where Parent = menuid --i.e. top level / root elements

    union all

    select b.MenuId, b.Name, b.Parent, b.Url
    , a.branchLength + 1
    , a.branchPath + '\' + cast(b.MenuId as nvarchar(max)) branchPath
    from cte a
    inner join MyTable b
    where b.Parent = a.MenuId --i.e. branch of previous result 
    and b.MenuId != a.MenuId --ensure we don't repeat ourselves
)
select a.MenuId, a.Name, a.Parent, a.Url
from cte a
inner join (
    select top 1 branchPath 
    from cte 
    order by branchLength Desc
) b
on b.branchPath like a.branchPath + '%' --gives us an easy way to traverse back up the tree, without recording every possible paths' inheritance / by using a directory structure
ordre by a.branchLength desc

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM