Trees and Other Hierarchies in MySQL
2011-10-18 14:35
302 查看
TreesandOtherHierarchiesinMySQL
Edgelist
Edge-adjacencylistmodelofatree
Automatetreedrawing
explosions
Mostnon-trivialdataishierarchical.Customershaveorders,whichhavelineitems,whichrefertoproducts,whichhaveprices.Populationsampleshavesubjects,whotaketests,whichgiveresults,whichhavesub-resultsandnorms.Websiteshavepages,
whichhavelinks,whichcollecthits,whichdistributeacrossdatesandtimes.Withsuchdata,weknowthedepthofthehierarchybeforewesitdowntowriteaquery.ThedepthofthehierarchyoftablesfixesthenumberofJOINsweneedtowrite.
Butifourdatadescribesafamilytree,orabrowsinghistory,orabillofmaterials,hierarchicaldepthdependsonthedata.WenolongerknowhowmanyJOINsitwilltaketowalkthetree.Weneedadifferentdatamodel.
Thatmodelisthegraph(Fig1),whichisasetofnodes(vertices)andtheedges(linesorarcs)thatconnectthem.ThischapterisabouthowtomodelandquerygraphsinaMySQLdatabase.
Graphtheoryisabranchoftopology.Itisthestudyofgeometricrelationswhicharen'tchangedbystretchingandcompression—rubbersheetgeometry,somecallit.Graphtheoryisidealformodellinghierarchies—likefamilytrees,browsinghistories,
searchtreesandbillsofmaterials—whoseshapeandsizewecan'tknowinadvance.
LetthesetofnodesinFig1be
N,thesetofedgesbe
L,andthegraphbe
G.Then
Gisthetupleororderedpair
{N,L}:
N={A,B,C,D,E,F}L={AC,CD,CF,BE}G={N,L}
Iftheedgesaredirected,thegraphisadigraphordirectedgraph.A
mixedgraphhasbothdirectedandundirectededges.
Examplesofgraphsareorganisationalcharts;itineraries;routemaps;partsexplosions;massivelymultiplayergames;languagerules;chathistories;networkandlinkanalysisinawidevarietyoffields,forexamplesearchengines,forensics,epidemiology
andtelecommunications;datamining;modelsofchemicalstructurehierarchies;and
Graphcharacteristicsandmodels
Nodesandedges:Twonodesareadjacentifthereisanedgebetweenthem.Twoedgesareadjacentiftheyconnecttoacommonnode.Inacompletegraph,allnodesareadjacenttoallothernodes.
Inadigraph,thenumberofedgesenteringanodeisitsindegree;thenumberleavingisitsoutdegree.Anodeofindegreezeroisarootnode,anodeofoutdegreezeroisaleafnode.
Inaweightedgraph,usedforexampletosolvethetravellingsalesmanproblem,edgeshaveaweightattribute.Adigraphwithweightededgesisa
network.
Pathsandcycles:Aconnectedsequenceofedgesisapath,itslengththenumberofedgestraversed.Twonodesareconnectedifthereisapathbetweenthem.Ifthereisapathconnectingeverypairofnodes,thegraphisaconnectedgraph.
Apathinwhichnonoderepeatsisasimplepath.Apathwhichreturnstoitsownoriginwithoutcrossingitselfisacycleorcircuit.Agraphwithmultiplepathsbetweenatleastonepairofnodesisreconvergent.Areconvergentgraphmaybecyclic
oracyclic.Aunitlengthcycleisaloop.
Ifagraph'sedgesintersectonlyatnodes,itisplanar.Twopathshavingnonodeincommonare
independent.
Traversinggraphs:Therearetwomainapproaches,breadth-firstand
depth-first.Breadth-firsttraversalvisitsallanode'ssiblingsbeforemovingontothenextlevel,andtypicallyusesa
queue.Depth-firsttraversalfollowsedgesdowntoleavesandbackbeforeproceedingtosiblings,andtypicallyusesa
stack.
Sparsity:AgraphwherethesizeofEapproachesthemaximumN2is
dense.WhenthemultipleismuchsmallerthanN,thegraphisconsidered
sparse.
Trees:Atreeisaconnectedgraphwithnocycles.Itisalsoagraphwheretheindegreeoftherootnodeis0,andtheindegreeofeveryothernodeis1.Atreewhereeverynodeisofoutdegree<=2isabinarytree.A
forestisagraphinwhicheveryconnectedcomponentisatree.
Eulerpaths:ApathwhichtraverseseveryedgeinagraphexactlyonceisanEulerpath.AnEulerpathwhichisacircuitisanEulercircuit.
Ifandonlyifeverynodeofaconnectedgraphhasevendegree,ithasanEulercircuit(whichis
whythegoodpeopleofKönigsbergcannotgoforawalkcrossingeachoftheirsevenbridgesexactlyonce).Ifandonlyifaconnectedgraphhasexactly2nodeswithodddegree,ithasanon-circuitEulerpath.Thedegreeofanendpointofanon-cycleEuler
pathis1+twicethenumberoftimesthepathpassesthroughthatnode,soitisalwaysodd.
Modelsforcomputinggraphs
Traditionally,computersciencetextbookshaveofferededgelists,adjacencylistsandadjacencymatricesasdatastructuresforgraphs,withalgorithmsimplementedinlanguageslikeC,C++andJava.Morerecentlyothermodelsandtoolshavebeensuggested,includingquerylanguagescustomisedforgraphs.
Edgelist:Thesimplestwaytorepresentagraphistolistitsedges:forFig1,theedgelistis
{AC,CD,CF,BE}.Itiseasytoaddanedgetothelist;deletionisalittleharder.
Table1 | |
Nodes | Adjacentnodes |
A | C |
B | E |
C | F,D,A |
D | C |
E | B |
F | C |
nnodesasalistofnlistswherelisticontainsnode
jifthegraphhasanedgefromnodeitonodej.
Anundirectedgraphmayberepresentedbyhavingnodejinthelistfornode
i,andnodeiinthelistfornodej.Table1showstheadjacencylistofthegraphinFig1interpretedasundirected.
Adjacencymatrix:Anadjacencymatrixrepresentsagraphwith
nnodesasannxnmatrix,wheretheentryat(i,j)is1ifthereisanedgefromnode
itonodej,orzeroifthereisnot.
Anadjacencymatrixcanrepresentaweightedgraphusingtheweightastheentry,andcanrepresentanundirectedgraphbyusingthesameentryinboth(i,j)and(j,i),orbyusinganuppertriangularmatrix.
Thereareusefulglossaries
hereand
here.
GraphsandSQL
OftenstandardSQLhasbeenthoughtcumbersomeforgraphproblems.CraigMullinsoncewrotethat"theset-basednatureofSQLisnotsimpletomasterandisanathematotheOOtechniquespracticedbyJavadevelopers."
AfewyearsafterMullinswrotethat,SQLiseverywhere,anditisincreasinglyappliedtographproblems.DB2,OracleandSQLServerhaverecursiveoperatorsforprocessingrecursivesets,thoughtheyallworkalittledifferently.MySQLhasnosuchspecial
tools,thoughtheOpenQuerygrouphasa
graphengineunderdevelopment.Meanwhile
JoeCelkoand
ScottStephens,amongothers,havepublishedgeneralSQLgraphproblemsolutionsthataresimplerandsmallerthanequivalentC++,
C#orJavacode.HereweimplementsomeoftheseideasinMySQL.
BewarethatinportsofedgelistandadjacencylistmethodstoSQL,therehasbeennameslippage.
What'softencalledtheadjacencylistmodelintheSQLworldisactuallyanedgelistmodel.Ifyoufollowthenow-commonpracticeintheSQLworldofreferringtoedgelistsasadjacencylists,don'tbesurprisedtofindthatthemodelisn'tquite
liketheadjacencylistinTable1.Herewewaffle.Wecallthemedge-adjacencylists.
Therearealsotwonewerkindsofmodels:whatJoeCelkocalledthenestedsetsmodel—alsoknownasthe
intervalmodel—whichusesgreater-than/less-thanarithmetictoencodetreerelationshipsandmodifiedpreordertreetraversal(MPTT)toquerythem,andTropashko's
materialisedpathmodel,whereeachnodeisstoredwithits(denormalised)pathtotheroot.SowehavefourmainpossibilitiesformodellinggraphsinMySQL:
edge-adjacencylists:basedonanadaptationbyEFCoddofthelogicoflinkedliststotablestructuresandqueries,
adjacencymatrices,
nestedsetsfortreessimplifysomequeries,butinsertionanddeletionarecumbersome,and
materialisedpaths.
Hereweworkouthowtoimplementedge-adjacency,nestedsetsandmaterialisedpathmodels—orpartsofthem—inMySQL5&6.
Theedgelist
Theedgelististhesimplestpossiblerepresentationofagraph:minimally,asingleedgestablewhereeachrowspecifiesonenodeanditsparent(whichisNULLfortherootnode),ormoreelaboratelytwotables,oneforthenodes,theotherabridgingtablefortheir
edges.
Intherealworld,thenodestablemightbeatableofpersonnel,orassemblyparts,orlocationsonamap.Itmighthavemanyothercolummsofdata.Theedgestablemightalsohaveadditionalcolumnsforedgeproperties.Thekeyintegersofbothtablesmight
beBIGINTs.
Tomodel
Fig1,though,wekeepthingsassimpleaspossible:
Listing1CREATETABLEnodes(nodeIDCHAR(1)PRIMARYKEY);CREATETABLEedges(childIDCHAR(1)NOTNULL,parentIDCHAR(1)NOTNULL,PRIMARYKEY(childID,parentID));INSERTINTOnodesVALUES('A'),('B'),('C'),('D'),('E'),('F');INSERTINTOedgesVALUES('A','C'),('C','D'),('C','F'),('B','E');SELECT*FROMedges;+---------+----------+|childID|parentID|+---------+----------+|A|C||B|E||C|D||C|F|+---------+----------+
Now,withoutanyassumptionswhateveraboutwhetherthegraphisconnected,whetheritisdirected,whetheritisatree,orwhatever,howhardisittowritea
reachabilityprocedure,aprocedurewhichtellsuswherewecangettofromhere,wherever'here'is?
Asimpleapproachisa
Seedthelistwiththestartingnode,
Add,butdonotduplicate,nodeswhicharechildrenofnodesinthelist,
Add,butdonotduplicate,nodeswhichareparentsofnodesinthelist,
Repeatsteps2and3untiltherearenomorenodestoadd.
HereitisasaMySQLstoredprocedure.Itavoidsduplicatenodesbydefining
reached.nodeIDasaprimarykeyandaddingreachablenodeswith
INSERTIGNORE:
Listing2DROPPROCEDUREIFEXISTSListReached;DELIMITERgoCREATEPROCEDUREListReached(INrootCHAR(1))BEGINDECLARErowsSMALLINTDEFAULT0;DROPTABLEIFEXISTSreached;CREATETABLEreached(nodeIDCHAR(1)PRIMARYKEY)ENGINE=HEAP;INSERTINTOreachedVALUES(root);SETrows=ROW_COUNT();WHILErows>0DOINSERTIGNOREINTOreachedSELECTDISTINCTchildIDFROMedgesASeINNERJOINreachedASpONe.parentID=p.nodeID;SETrows=ROW_COUNT();INSERTIGNOREINTOreachedSELECTDISTINCTparentIDFROMedgesASeINNERJOINreachedASpONe.childID=p.nodeID;SETrows=rows+ROW_COUNT();ENDWHILE;SELECT*FROMreached;DROPTABLEreached;END;goDELIMITER;CALLListReached('A');+--------+|nodeID|+--------+|A||C||D||F|+--------+
Tomaketheproceduremoreversatile,giveitinputparameterswhichtellitwhethertolistchild,parentorallconnections,andwhethertorecogniseloops(forexampleCtoC).
Togivethemodelreferentialintegrity,useInnoDBandmake
edges.childIDand
edges.parentIDforeignkeys.Toaddordeleteanode,addordeletedesiredsinglerowsin
nodesand
edges.Tochangeanedge,editit.Themodeldoesnotrequirethegraphtobeconnectedortreelike,anddoesnotpresumedirection.
TheedgelistisbasictowhatSQLersoftencalltheadjacencylistmodel.
Edge-adjacencylistmodelofatree
WritersintheSQLgraphliteratureoftengivesolutionsusingsingledenormalisedtables.Denormalisationcancost,bigtime.Thebiggerthetable,thebiggerthecost.Youcannoteditnodesandedgesseparately.Carryingextranodeinformationduringedge
computationslowsperformance.Withnodesandedgesdenormalisedtoonetable,youhavetorepresenttherootnodewitha
NULL.
Toavoidthesedifficulties,normalisetreeslikeWilliamShakespeare'sfamilytree(Fig2)withtwotables,a
nodestable(family)containinginformationaboutindividuals,andan
edgestable(familytree)witharowforeachparent-childlinkoredge.Later,whenweuseadifferenttreemodel,wewon'thavetomesswiththedatabeingmodelled.
Listing3--Basedata:CREATETABLEfamily(IDsmallint(6)PRIMARYKEYAUTO_INCREMENT,namechar(20)default'',sibordertinyint(4)defaultNULL,bornsmallint(4)unsigneddefaultNULL,diedsmallint(4)unsigneddefaultNULL);INSERTINTOfamilyVALUES(1,'RichardShakespeare',NULL,NULL,1561);INSERTINTOfamilyVALUES(2,'HenryShakespeare',1,NULL,1569);INSERTINTOfamilyVALUES(3,'JohnShakespeare',2,1530,1601);INSERTINTOfamilyVALUES(4,'JoanShakespeare',1,1558,NULL);INSERTINTOfamilyVALUES(5,'MargaretShakespeare',2,1562,1563);INSERTINTOfamilyVALUES(6,'WilliamShakespeare',3,1564,1616);INSERTINTOfamilyVALUES(7,'GilbertShakespeare',4,1566,1612);INSERTINTOfamilyVALUES(8,'JoanShakespeare',5,1568,1646);INSERTINTOfamilyVALUES(9,'AnneShakespeare',6,1571,1579);INSERTINTOfamilyVALUES(10,'RichardShakespeare',7,1574,1613);INSERTINTOfamilyVALUES(11,'EdmondShakespeare',8,1580,1607);INSERTINTOfamilyVALUES(12,'SusanaShakespeare',1,1583,1649);INSERTINTOfamilyVALUES(13,'HamnetShakespeare',1,1585,1596);INSERTINTOfamilyVALUES(14,'JudithShakespeare',1,1585,1662);INSERTINTOfamilyVALUES(15,'WilliamHart',1,1600,1639);INSERTINTOfamilyVALUES(16,'MaryHart',2,1603,1607);INSERTINTOfamilyVALUES(17,'ThomasHart',3,1605,1670);INSERTINTOfamilyVALUES(18,'MichaelHart',1,1608,1618);INSERTINTOfamilyVALUES(19,'ElizabethHall',1,1608,1670);INSERTINTOfamilyVALUES(20,'ShakespeareQuiney',1,1616,1617);INSERTINTOfamilyVALUES(21,'RichardQuiney',2,1618,1639);INSERTINTOfamilyVALUES(22,'ThomasQuiney',3,1620,1639);INSERTINTOfamilyVALUES(23,'JohnBernard',1,NULL,1674);--Tablewhichmodelsthetree:CREATETABLEfamilytree(childIDsmallintNOTNULL,parentIDsmallintNOTNULL,PRIMARYKEY(childID,parentID););INSERTINTOfamilytreeVALUES(2,1),(3,1),(4,2),(5,2),(6,2),(7,2),(8,2),(9,2),(10,2),(11,2),(12,6),(13,6),(14,6),(15,8),(16,8),(17,8),(18,8),(19,12),(20,14),(21,14),(22,14),(23,19);
(The
familyPKisauto-increment,butthelistingismorereader-friendlywhenthe
IDvaluesareshown.)
Itwillbeusefultohaveafunctionthatreturns
family.nameforaparentorchildIDin
familytree:
Listing4--5.0.16ORLATER:SETGLOBALlog_bin_trust_function_creators=TRUE;DROPFUNCTIONIFEXISTSPersonName;DELIMITERgoCREATEFUNCTIONPersonName(personIDSMALLINT)RETURNSCHAR(20)BEGINDECLAREpnameCHAR(20)DEFAULT'';SELECTnameINTOpnameFROMfamilyWHEREID=personID;RETURNpname;END;goDELIMITER;SELECTPersonName(parentID)AS'ParentofWilliam'FROMfamilytreeWHEREchildID=6;+-------------------+|ParentofWilliam|+-------------------+|HenryShakespeare|+-------------------+SELECTPersonName(childID)AS'ChildrenofWilliam'FROMfamilytreeWHEREparentID=(SELECTIDFROMfamilyWHEREname='WilliamShakespeare');+---------------------+|ChildrenofWilliam|+---------------------+|SusanaShakespeare||HamnetShakespeare||JudithShakespeare|+---------------------+SELECTPersonName(childID)ASchild,PersonName(parentID)ASparentFROMfamilytree;+----------------------+---------------------+|child|parent|+----------------------+---------------------+|HenryShakespeare|RichardShakespeare||JohnShakespeare|RichardShakespeare||JoanShakespeare|HenryShakespeare||MargaretShakespeare|HenryShakespeare||WilliamShakespeare|HenryShakespeare||GilbertShakespeare|HenryShakespeare||JoanShakespeare|HenryShakespeare||AnneShakespeare|HenryShakespeare||RichardShakespeare|HenryShakespeare||EdmondShakespeare|HenryShakespeare||SusanaShakespeare|WilliamShakespeare||HamnetShakespeare|WilliamShakespeare||JudithShakespeare|WilliamShakespeare||WilliamHart|JoanShakespeare||MaryHart|JoanShakespeare||ThomasHart|JoanShakespeare||MichaelHart|JoanShakespeare||ElizabethHall|SusanaShakespeare||ShakespeareQuiney|JudithShakespeare||RichardQuiney|JudithShakespeare||ThomasQuiney|JudithShakespeare||JohnBernard|ElizabethHall|+----------------------+---------------------+
Asame-tableforeignkeycansimplifytreemaintenance:
Listing4acreatetableedges(IDintPRIMARYKEY, parentidint,foreignkey(parentID)referencesedges(ID)ONDELETECASCADEONUPDATECASCADE)engine=innodb;insertintoedges(ID,parentID)values(1,null),(2,1),(3,1),(4,2);select*fromedges;+----+----------+|ID|parentid|+----+----------+|1|NULL||2|1||3|1||4|2|+----+----------+deletefromedgeswhereid=2;select*fromedges;+----+----------+|ID|parentid|+----+----------+|1|NULL||3|1|+----+----------+
Simplequeriesretrievebasicfactsaboutthetree,forexampleGROUP_CONCAT()collectsparentnodeswiththeirchildrenincorrectorder:
Listing5SELECTparentIDASParent,GROUP_CONCAT(childIDORDERBYsiborder)ASChildrenFROMfamilytreetJOINfamilyfONt.parentID=f.IDGROUPBYparentID;+--------+-------------------+|Parent|Children|+--------+-------------------+|1|3,2||2|4,5,6,7,8,9,10,11||6|12,13,14||8|18,17,16,15||12|19||14|22,21,20||19|23|+--------+-------------------+
Iterateoverthosecomma-separatedlistswithabitofapplicationcodeandyouhaveahybridtreewalk.The
paterfamiliasistherootnode,individualswithnochildrenaretheleafnodes,andqueriestoretrievesubtreestatisticsarestraightforward:
Listing6SELECTPersonName(ID)ASPaterfamilias,IFNULL(born,'?')ASBorn,IFNULL(died,'?')ASDiedFROMfamilyASfLEFTJOINfamilytreeAStONf.ID=t.childIDWHEREt.childIDISNULL;+---------------------+------+------+|Paterfamilias|Born|Died|+---------------------+------+------+|RichardShakespeare|?|1561|+---------------------+------+------+SELECTPersonName(ID)ASChildless,IFNULL(born,'?')ASBorn,IFNULL(died,'?')ASDiedFROMfamilyASfLEFTJOINfamilytreeAStONf.ID=t.parentIDWHEREt.parentIDISNULL;+----------------------+------+------+|Childless|Born|Died|+----------------------+------+------+|JohnShakespeare|1530|1601||JoanShakespeare|1558|?||MargaretShakespeare|1562|1563||GilbertShakespeare|1566|1612||AnneShakespeare|1571|1579||RichardShakespeare|1574|1613||EdmondShakespeare|1580|1607||HamnetShakespeare|1585|1596||WilliamHart|1600|1639||MaryHart|1603|1607||ThomasHart|1605|1670||MichaelHart|1608|1618||ShakespeareQuiney|1616|1617||RichardQuiney|1618|1639||ThomasQuiney|1620|1639||JohnBernard|?|1674|+----------------------+------+------+SELECTROUND(AVG(died-born),2)AS'Longevityofthechildless'FROMfamilyASfLEFTJOINfamilytreeAStONf.ID=t.parentIDWHEREt.parentIDISNULL;+----------------------------+|Longevityofthechildless|+----------------------------+|25.86|+----------------------------+
InstrikingcontrastwithCelko's
nestedsetsmodel,insertinganewiteminthismodelrequiresnorevisionofexistingrows.Wejustaddanew
familyrow,thenanew
familytreerowwithIDsspecifyingwhoisparenttowhom.Deletionisalsoatwo-step:deletethe
familytreerowforthatchild-parentlink,thendeletethe
familyrowforthatchild.
Walkinganedgelisttree
Traversingsubtreesiswhatgivestheedge-adjacencylistmodelitsreputationfordifficulty.Wecan'tknowinadvance,exceptinthesimplestoftrees,howmanylevelsofparentandchildhavetobequeried,soweneedrecursionoralogicallyequivalent
loop.
It'sanaturalproblemforastoredprocedure.Earliereditionsshowedabruteforce
breadth-firstalgorithmthatneededthreeintermediarytables(itcanbefoundin
Listing7DROPPROCEDUREIFEXISTSfamsubtree;DELIMITERgoCREATEPROCEDUREfamsubtree(rootINT)BEGINDROPTABLEIFEXISTSfamsubtree;CREATETABLEfamsubtreeSELECTchildID,parentID,0ASlevelFROMfamilytreeWHEREparentID=root;ALTERTABLEfamsubtreeADDPRIMARYKEY(childID,parentID);REPEATINSERTIGNOREINTOfamsubtreeSELECTf.childID,f.parentID,s.level+1FROMfamilytreeASfJOINfamsubtreeASsONf.parentID=s.childID;UNTILRow_Count()=0ENDREPEAT;END;goDELIMITER;callfamsubtree(1);--fromtherootyoucanseeforeverSELECTConcat(Space(level),parentID)ASParent,Group_Concat(childIDORDERBYchildID)ASChildFROMfamsubtreeGROUPBYparentID;+--------+-------------------+|Parent|Child|+--------+-------------------+|1|2,3||2|4,5,6,7,8,9,10,11||6|12,13,14||8|15,16,17,18||12|19||14|20,21,22||19|23|+--------+-------------------+
Simpleandquick.Thelogicportstoanyedgelist.Wecanprovethatrightnowbywritingagenericversion.GenericTree()justneedsparametersforthenameofthetargettable,thenamesofitschildandparentIDcolumns,andtheparentIDwhose
descendantsaresought:
Listing7a:General-purposeedgelisttreewalkerDROPPROCEDUREIFEXISTSGenericTree;DELIMITERgoCREATEPROCEDUREGenericTree(edgeTableCHAR(64),edgeIDcolCHAR(64),edgeParentIDcolCHAR(64),ancestorIDINT)BEGINDECLARErINTDEFAULT0;DROPTABLEIFEXISTSsubtree;SET@sql=Concat('CREATETABLEsubtreeENGINE=MyISAMSELECT',edgeIDcol,'ASchildID,',edgeParentIDcol,'ASparentID,','0ASlevelFROM',edgeTable,'WHERE',edgeParentIDcol,'=',ancestorID);PREPAREstmtFROM@sql;EXECUTEstmt;DROPPREPAREstmt;ALTERTABLEsubtreeADDPRIMARYKEY(childID,parentID);REPEATSET@sql=Concat('INSERTIGNOREINTOsubtreeSELECTa.',edgeIDcol,',a.',edgeparentIDcol,',b.level+1FROM',edgeTable,'ASaJOINsubtreeASbONa.',edgeParentIDcol,'=b.childID');PREPAREstmtFROM@sql;EXECUTEstmt;SETr=Row_Count();--saverow_count()resultbeforeDROPPREPARElosesthevalueDROPPREPAREstmt;UNTILr<1ENDREPEAT;END;goDELIMITER;
ToretrievedetailslikenamesandotherdataassociatedwithnodeIDs,writeafrontendquerytojointhesubtreeresulttablewiththerequireddetailtable(s),forexample:
CALLGenericTree('familytree','childID','parentID',1);SELECTConcat(Repeat('',s.level),a.name)ASParent,b.nameASChildFROMsubtreesJOINfamilyaONs.parentID=a.IDJOINfamilybONs.childID=b.ID;+-----------------------+----------------------+|Parent|Child|+-----------------------+----------------------+|RichardShakespeare|HenryShakespeare||RichardShakespeare|JohnShakespeare||HenryShakespeare|JoanShakespeare||HenryShakespeare|MargaretShakespeare||HenryShakespeare|WilliamShakespeare||HenryShakespeare|GilbertShakespeare||HenryShakespeare|JoanShakespeare||HenryShakespeare|AnneShakespeare||HenryShakespeare|RichardShakespeare||HenryShakespeare|EdmondShakespeare||WilliamShakespeare|SusanaShakespeare||WilliamShakespeare|HamnetShakespeare||WilliamShakespeare|JudithShakespeare||JoanShakespeare|WilliamHart||JoanShakespeare|MaryHart||JoanShakespeare|ThomasHart||JoanShakespeare|MichaelHart||SusanaShakespeare|ElizabethHall||JudithShakespeare|ShakespeareQuiney||JudithShakespeare|RichardQuiney||JudithShakespeare|ThomasQuiney||ElizabethHall|JohnBernard|+-----------------------+----------------------+
IsGenericTree()fast?Youbet.Onstandardhardwareitprocessesa5,000-nodetreeinlessthan0.5secs—muchfasterthanacomparable
nestedsetsqueryonthesametree!Ithasnoseriousscalingissues.Anditslogiccanbeusedtoprune:callGenericTree()thendeletethelistedrows.Betterstill,writeagenerictreeprunerfromListing7aandaDELETEcommand.Toinsertasubtree,
prepareatableofnewrows,pointitstopedgeatanexistingnodeasparent,andINSERTit.
Theedgelisttreewalkislogicallyrecursive,sohowaboutcodingitrecursively?Hereisarecursive
depth-firstPHPtreewalkforthe
familytreeandfamilytables:
Listing7b:RecursiveedgelistsubtreeinPHP$info=recursivesubtree(1,$a=array(),0);foreach($infoas$row)echostr_repeat(" ",2*$row[4]),($row[3]>0)?"<b>{$row[1]}</b>":$row[1],"<br/>";functionrecursivesubtree($rootID,$a,$level){$childcountqry="(SELECTCOUNT(*)FROMfamilytreeWHEREparentID=t.childID)ASchildcount";$qry="SELECTt.childid,f.name,t.parentid,$childcountqry,$level"."FROMfamilytreetJOINfamilyfONt.childID=f.ID"."WHEREparentid=$rootIDORDERBYchildcount<>0,t.childID";$res=mysql_qry($qry);while($row=mysql_fetch_row($res)){$a[]=$row;if($row[3]>0)$a=recursivesubtree($row[0],$a,$level+1);//downbeforeright}return$a;}
Aquerywithasubquery,afetchloop,andarecursivecall--that'sallthereistoit.Anicefeatureofthisalgorithmisthatitwritesresultrowsindisplay-readyorder.ToportthistoMySQL,youmusthavesetmaximumrecursiondepthin
my.cnf/iniorinyourclient:
Listing7c:RecursiveedgelistsubtreeinMySQLSET@@SESSION.max_sp_recursion_depth=25;DROPPROCEDUREIFEXISTSrecursivesubtree;DELIMITERgoCREATEPROCEDURErecursivesubtree(irootINT,ilevelINT)BEGINDECLAREirows,ichildid,iparentid,ichildcount,doneINTDEFAULT0;DECLAREcnameVARCHAR(64);SETirows=(SELECTCOUNT(*)FROMfamilytreeWHEREparentID=iroot);IFilevel=0THENDROPTEMPORARYTABLEIFEXISTS_descendants;CREATETEMPORARYTABLE_descendants(childIDINT,parentIDINT,nameVARCHAR(64),childcountINT,levelINT);ENDIF;IFirows>0THENBEGINDECLAREcurCURSORFORSELECTchildid,parentid,f.name,(SELECTCOUNT(*)FROMfamilytreeWHEREparentID=t.childID)ASchildcountFROMfamilytreetJOINfamilyfONt.childID=f.IDWHEREparentid=irootORDERBYchildcount<>0,t.childID;DECLARECONTINUEHANDLERFORSQLSTATE'02000'SETdone=1;OPENcur;WHILENOTdoneDOFETCHcurINTOichildid,iparentid,cname,ichildcount;IFNOTdoneTHENINSERTINTO_descendantsVALUES(ichildid,iparentid,cname,ichildcount,ilevel);IFichildcount>0THENCALLrecursivesubtree(ichildid,ilevel+1);ENDIF;ENDIF;ENDWHILE;CLOSEcur;END;ENDIF;IFilevel=0THEN--Showresulttableheadedbynamethatcorrespondstoiroot:SETcname=(SELECTnameFROMfamilyWHEREID=iroot);SET@sql=CONCAT('SELECTCONCAT(REPEAT(CHAR(32),2*level),IF(childcount,UPPER(name),name))','AS',CHAR(39),'Descendantsof',cname,CHAR(39),'FROM_descendants');PREPAREstmtFROM@sql;EXECUTEstmt;DROPPREPAREstmt;ENDIF;END;goDELIMITER;CALLrecursivesubtree(1,0);+------------------------------------+|DescendantsofRichardShakespeare|+------------------------------------+|HENRYSHAKESPEARE||JoanShakespeare||MargaretShakespeare||WILLIAMSHAKESPEARE||SUSANASHAKESPEARE||ELIZABETHHALL||JohnBernard||HamnetShakespeare||JUDITHSHAKESPEARE||ShakespeareQuiney||RichardQuiney||ThomasQuiney||GilbertShakespeare||JOANSHAKESPEARE||WilliamHart||MaryHart||ThomasHart||MichaelHart||AnneShakespeare||RichardShakespeare||EdmondShakespeare||JohnShakespeare|+------------------------------------+
InMySQLthisrecursivetreewalkcanbeupto100timesslowerthanGenericTree().ItsslownessiscomparabletothatofaMySQLversionofKendallWillet'sdepth-firstedgelistsubtreealgorithm:
Listing7d:Depth-firstedgelistsubtreeCREATEPROCEDUREdepthfirstsubtree(irootINT)BEGINDECLAREilastvisited,inxt,ilastordINT;SETilastvisited=iroot;SETilastord=1;DROPTABLEIFEXISTSdescendants;CREATETABLEdescendantsSELECTchildID,parentID,-1ASordFROMfamilytree;UPDATEdescendantsSETord=1WHEREchildID=iroot;this:LOOPSETinxt=NULL;SELECTMIN(childID)INTOinxtFROMdescendants--godownWHEREparentID=ilastvisitedANDord=-1;IFinxtISNULLTHEN--nothingdown,sogorightSELECTMIN(d2.childID)INTOinxtFROMdescendantsd1JOINdescendantsd2ONd1.parentID=d2.parentIDANDd1.childID<d2.childIDWHEREd1.childID=ilastvisited;ENDIF;IFinxtISNULLTHEN--nothingright.sogoupSELECTparentIDINTOinxtFROMdescendantsWHEREchildID=ilastvisitedANDparentIDISNOTNULL;ENDIF;UPDATEdescendantsSETord=ilastord+1WHEREchildID=inxtANDord=-1;IFROW_COUNT()>0THENSETilastord=ilastord+1;ENDIF;IFinxtISNULLTHENLEAVEthis;ENDIF;SETilastvisited=inxt;ENDLOOP;END;
OnereasonWillet'sissloweristhatMySQLdoesnotpermitmultiplereferencestoatemporarytableinaquery.Whenallalgorithmsaredeniedtemptables,though,thisalgorithmisstillslowerthanrecursion,andbotharemuchslowerthan
GenericTree().
Edgelistsubtreequeriesperformfasterandareeasiertowritethantheirreputationsuggests.Andedgetablesareflexible.Foratreedescribingapartsexplosionratherthanafamily,justaddcolumnsforweight,quantity,assemblytime,cost,price
andsoon.Reportsneedonlyaggregatecolumnvaluesandsums.We'll
Enumeratingpathsinanedge-adjacencylist
Pathenumerationinanedgelisttreeisalmostaseasyasdepth-firstsubtreetraversal:
createatableforpaths,
seeditwithpathsofunitlengthfromthetreetable,
iterativelyaddpathstilltherearenomoretoadd.
MySQL'sINSERTIGNOREcommandsimplifiesthecodebyremovingtheneedforaNOTEXISTS(...)clauseintheINSERT...SELECTstatement.Sinceadjacenciesarelogicallysymmetrical,wemakepathdirectionthecaller'schoice,
UPor
DOWN:
Listing8DROPPROCEDUREIFEXISTSListAdjacencyPaths;DELIMITERgoCREATEPROCEDUREListAdjacencyPaths(INdirectionCHAR(5))BEGINDROPTABLEIFEXISTSpaths;CREATETABLEpaths(startSMALLINT,stopSMALLINT,lenSMALLINT,PRIMARYKEY(start,stop))ENGINE=HEAP;IFdirection='UP'THENINSERTINTOpathsSELECTchildID,parentID,1FROMfamilytree;ELSEINSERTINTOpathsSELECTparentID,childID,1FROMfamilytree;ENDIF;WHILEROW_COUNT()>0DOINSERTIGNOREINTOpathsSELECTDISTINCTp1.start,p2.stop,p1.len+p2.lenFROMpathsASp1INNERJOINpathsASp2ONp1.stop=p2.start;ENDWHILE;SELECTstart,stop,lenFROMpathsORDERBYstart,stop;DROPTABLEpaths;END;goDELIMITER;
Tofindthepathsfromjustonenode,seedthe
pathstablewithpathsfromthestartingnode,theniterativelysearchaJOINof
familytreeand
pathsforedgeswhichwillextendexistingpathsintheuser-specifieddirection:
Listing8aDROPPROCEDUREIFEXISTSListAdjacencyPathsOfNode;DELIMITERgoCREATEPROCEDUREListAdjacencyPathsOfNode(INnodeSMALLINT,INdirectionCHAR(5))BEGINTRUNCATEpaths;IFdirection='UP'THENINSERTINTOpathsSELECTchildID,parentID,1FROMfamilytreeWHEREchildID=node;ELSEINSERTINTOpathsSELECTparentID,childID,1FROMfamilytreeWHEREparentID=node;ENDIF;WHILEROW_COUNT()>0DOIFdirection='UP'THENINSERTIGNOREINTOpathsSELECTDISTINCTpaths.start,familytree.parentID,paths.len+1FROMpathsINNERJOINfamilytreeONpaths.stop=familytree.childID;ELSEINSERTIGNOREINTOpathsSELECTDISTINCTpaths.start,familytree.childID,paths.len+1FROMpathsINNERJOINfamilytreeONpaths.stop=familytree.parentID;ENDIF;ENDWHILE;SELECTstart,stop,lenFROMpathsORDERBYstart,stop;END;goDELIMITER;CALLListAdjacencyPathsOfNode(1,'DOWN');+-------+------+------+|start|stop|len|+-------+------+------+|1|2|1||1|3|1||1|4|2||1|5|2||1|6|2||1|7|2||1|8|2||1|9|2||1|10|2||1|11|2||1|12|3||1|13|3||1|14|3||1|15|3||1|16|3||1|17|3||1|18|3||1|19|4||1|20|4||1|21|4||1|22|4||1|23|5|+-------+------+------+
Thesealgorithmsdon'tbendthebrain.Theyperformacceptablywithlargetrees.Queryingedge-adjacencylistsforsubtreesandpathsislessdauntingthantheirreputationsuggests.
Automatetreedrawing!
Tablesofnumbersmaybethemostboringobjectsonearth.Howtobringthemalive?TheGoogleVisualizationAPIlibraryhasan
‘OrgChart’modulethatcanmakeedgelisttreeslooklike
Fig2,buteachinstanceneedsfiftyorsolinesofspecificJavaScriptcode,plusanadditionallineofcodeforeachrowofdatainthetree.Couldweautogeneratethatcode?
Maisoui!Themoduleneedschildnodeandparentnodecolumnsofdata,andacceptsanoptionalthirdcolumnforinfothatpopsupwhenthemousehovers.HereissuchaqueryfortheShakespearefamilytree...
Listing9selectconcat(node.ID,'',node.name)asnode,if(edges.parentIDisnull,'',concat(parent.ID,'',parent.name))asparent,if(node.bornisnull,'Birthdateunknown',concat('Born',node.born))astooltipfromfamilyasnodeleftjoinfamilytreeasedgesonnode.ID=edges.childIDleftjoinfamilyasparentonedges.parentID=parent.ID;
andhereisaPHPfunctionwhichgeneratestheHTMLandJavaScriptneededtopaintanOrgChartfor
anytreequerythatreturnsstringcolumnsfornode,parentandoptionallytooltips:
Listing9afunctionorgchart($qry){$cols=array();$rows=array();$res=mysql_query($qry)orexit(mysql_error());$colcount=mysql_num_fields($res);if($colcount<2)exit("Orgchartneedstwoorthreecolumns");$rowcount=mysql_num_rows($res);for($i=0;$i<$colcount;$i++)$cols[]=mysql_fetch_field($res,$i);while($row=mysql_fetch_row($res))$rows[]=$row;echo"<html>\n<head>\n","<scripttype='text/javascript'src='https://www.google.com/jsapi'></script>\n","<scripttype='text/javascript'>\n","google.load('visualization','1',{'packages':['orgchart']});\n","google.setOnLoadCallback(drawChart);\n","functiondrawChart(){\n","vardata=newgoogle.visualization.DataTable();\n";for($i=0;$i<$colcount;$i++)echo"data.addColumn('string','{$cols[$i]->name}')\n";echo"data.addRows([\n";for($j=0;$j<$rowcount;$j++){$row=$rows[$j];$c=(($j<$rowcount-1)?",":"");echo"['{$row[0]}','{$row[1]}','{$row[2]}']$c\n";}echo"]);\n","varchart=newgoogle.visualization.OrgChart(document.getElementById('chart_div'));\n","varoptions={'size':'small','allowHtml':'true','allowCollapse':'true'};\n","chart.draw(data,options);\n","}\n","</script>\n/head>\n<body>\n","<divid='chart_div'></div>\n","</body>\n</html>";}
Nestedsetsmodelofatree
ImagineanovaldrawnroundeveryleafandeverysubtreeinFig2,andafinalovalroundtheentiretree.Thetreeisaset.Eachsubtreeisasubset.That'sthebasicideaofthenestedsetsmodel.Theadvantageofthenestedsetsmodelisthatroot,leaves,subtrees,levels,treeheight,ancestors,descendantsandpathscanberetrievedwithoutrecursionorapplicationlanguagecode.Thedisadvantagesare:
initialsetupofthetreetablecanbedifficult,
queriesforparents(immediatesuperiors)andchildren(immediatesubordinates)aremorecomplicatedthanwithanedgelistmodel,
insertion,updatesanddeletionareextremelycumbersomesincetheymayrequireupdatestomuchofthetree.
Thenestedsetsmodeldependsonusinga
modifiedpreordertreetraversal(MPTT)
depth-firstalgorithmtoassigneachnodeleftandrightintegerswhichdefinethenode'streeposition.Allnodesofasubtreehave
leftvaluesgreaterthanthesubtreeparent'sleftvalue,and
rightvaluessmallerthanthatofthesubtreeparent'srightvalue.
soqueriesforsubtreesaredeadsimple.Ifthenumberingschemeisinteger-sequentialasinFig3,therootnodereceivesa
leftvalueof1andarightvalueequaltotwicetheitemcount.
ToseehowtocodenestedsetsusingMPTT,tracetheascendingintegersinFig3,startingwith1onthe
leftsideoftherootnode(
RichardShakespeare).Followingedgesdownwardandleftward,the
leftsideofeachboxgetsthenextinteger.Whenyoureachaleaf(
Joan,
left=3),therightsideofthatboxgetsthenextinteger(4).Ifthereisanothernodetotherightonthesamelevel,continueinthatdirection;otherwisecontinueupthe
rightsideofthesubtreeyoujustdescended.Whenyouarrivebackattherootonthe
rightside,you'redone.Down,rightandup.
Aseriousproblemwiththisschemejumpsoutrightaway:afteryou'vewrittentheFig3treetoatable,whatifhistoriansdiscoveranolderbrotherorsisterofHenryandJohn?Everyrowinthetreetablemustbeupdated!
Celkoandothershaveproposedalternativenumberingschemestogetroundthisproblem,butthelogicaldifficultyremains:insertsandupdatescaninvalidatemanyorallrows,andnoSQLCHECKorCONSTRAINTcanpreventit.Thenestedsetsmodelisnotgood
fortreeswhichrequirefrequentupdates,andisprettymuchunsupportableforlargeupdatabletreesthatwillbeaccessedbymanyconcurrentusers.Butaswe'llseeinamoment,itcanbeveryusefulindeedforreportingatree.
Howtobuildanestedsetsrepresentationfromanedgelist
Obviously,numberingatreebyhandwouldbeerror-prone,seriouslyimpracticalforlargetrees,soit'susuallybesttocodethetreeinitiallyasanedgelist,thenuseastoredproceduretotranslatetheedgelistrepresentationtonestedsets.
Celko'sdepth-firstpushdownstackmethodwilltranslateanyedgelisttreeintoanestedsetstree,thoughslowly:
Createatablenestedsettreeforthetree:
node,
leftedge,
rightedge,andastackpointer(
top),
Seedthattablewiththerootnodeoftheedgelisttree,
Setanextedgecounterto1plustheleftvalueoftherootnode,i.e.2,
Whilethatcounterislessthantherightedgevalueoftherootnode...
insertarowforthisparent'ssmallestunwrittenchild,anddropdownalevel,or
ifwe'reoutofchildren,incrementrightedge,writeittothecurrentrow,andbackupalevel.
ThisversionhasbeenimprovedtohandleedgelisttreeswithorwithoutarowcontainingtherootnodeanditsNULLparent:
Listing10DROPPROCEDUREIFEXISTSEdgeListToNestedSet;DELIMITERgoCREATEPROCEDUREEdgeListToNestedSet(edgeTableCHAR(64),idColCHAR(64),parentColCHAR(64))BEGINDECLAREmaxrightedge,rowsSMALLINTDEFAULT0;DECLAREtrees,currentSMALLINTDEFAULT1;DECLAREnextedgeSMALLINTDEFAULT2;DECLAREmsgCHAR(128);--createworkingtreetableasacopyofedgeTableDROPTEMPORARYTABLEIFEXISTStree;CREATETEMPORARYTABLEtree(childIDINT,parentIDINT);SET@sql=CONCAT('INSERTINTOtreeSELECT',idCol,',',parentCol,'FROM',edgeTable);PREPAREstmtFROM@sql;EXECUTEstmt;DROPPREPAREstmt;--initialiseresulttableDROPTABLEIFEXISTSnestedsettree;CREATETABLEnestedsettree(topSMALLINT,nodeIDSMALLINT,leftedgeSMALLINT,rightedgeSMALLINT,KEY(nodeID,leftedge,rightedge))ENGINE=HEAP;--rootischildwithNULLparentorparentwhichisnotachildSET@nulls=(SELECTCount(*)FROMtreeWHEREparentIDISNULL);IF@nulls>1THENSETtrees=2;ELSEIF@nulls=1THENSET@root=(SELECTchildIDFROMtreeWHEREparentIDISNULL);DELETEFROMtreeWHEREchildID=@root;ELSESET@sql=CONCAT('SELECTCount(DISTINCTf.',parentcol,')INTO@rootsFROM',edgeTable,'fLEFTJOIN',edgeTable,'tONf.',parentCol,'=','t.',idCol,'WHEREt.',idCol,'ISNULL');PREPAREstmtFROM@sql;EXECUTEstmt;DROPPREPAREstmt;IF@roots<>1THENSETtrees=@roots;ELSESET@sql=CONCAT('SELECTDISTINCTf.',parentCol,'INTO@rootFROM',edgeTable,'fLEFTJOIN',edgeTable,'tONf.',parentCol,'=','t.',idCol,'WHEREt.',idCol,'ISNULL');PREPAREstmtFROM@sql;EXECUTEstmt;DROPPREPAREstmt;ENDIF;ENDIF;IFtrees<>1THENSETmsg=IF(trees=0,"Notreefound","Tablehasmultipletrees");SELECTmsgAS'Cannotcontinue';ELSE--buildnestedsetstreeSETmaxrightedge=2*(1+(SELECT+COUNT(*)FROMtree));INSERTINTOnestedsettreeVALUES(1,@root,1,maxrightedge);WHILEnextedge<maxrightedgeDOSETrows=(SELECTCount(*)FROMnestedsettreesJOINtreetONs.nodeID=t.parentIDANDs.top=current);IFrows>0THENBEGININSERTINTOnestedsettreeSELECTcurrent+1,MIN(t.childID),nextedge,NULLFROMnestedsettreeASsJOINtreeAStONs.nodeID=t.parentIDANDs.top=current;DELETEFROMtreeWHEREchildID=(SELECTnodeIDFROMnestedsettreeWHEREtop=(current+1));SETnextedge=nextedge+1,current=current+1;END;ELSEUPDATEnestedsettreeSETrightedge=nextedge,top=-topWHEREtop=current;SETnextedge=nextedge+1,current=current-1;ENDIF;ENDWHILE;--showresultIF(SELECTCOUNT(*)FROMtree)>0THENSELECT'Orphanedrowsremain'AS'Error';ENDIF;DROPTEMPORARYTABLEtree;ENDIF;END;goDELIMITER;CALLEdgeListToNestedSet('familytree','childID','parentID');SELECTnodeID,PersonName(nodeID)ASName,ABS(top)AS'TreeLevel',leftedgeAS'Left',rightedgeAS'Right'FROMnestedsettreeORDERBYnodeID;+--------+----------------------+------------+------+-------+|nodeID|Name|TreeLevel|Left|Right|+--------+----------------------+------------+------+-------+|1|RichardShakespeare|1|1|46||2|HenryShakespeare|2|2|43||3|JohnShakespeare|2|44|45||4|JoanShakespeare|3|3|4||5|MargaretShakespeare|3|5|6||6|WilliamShakespeare|3|7|24||7|GilbertShakespeare|3|25|26||8|JoanShakespeare|3|27|36||9|AnneShakespeare|3|37|38||10|RichardShakespeare|3|39|40||11|EdmondShakespeare|3|41|42||12|SusanaShakespeare|4|8|13||13|HamnetShakespeare|4|14|15||14|JudithShakespeare|4|16|23||15|WilliamHart|4|28|29||16|MaryHart|4|30|31||17|ThomasHart|4|32|33||18|MichaelHart|4|34|35||19|ElizabethHall|5|9|12||20|ShakespeareQuiney|5|17|18||21|RichardQuiney|5|19|20||22|ThomasQuiney|5|21|22||23|JohnBernard|6|10|11|+--------+----------------------+------------+------+-------+
Verifythefunctionwithaquerythatgeneratesanedgelisttreefromanestedsetstree:
Listing10a:SELECTa.nodeID,b.nodeIDASparentFROMnestedsettreeASaLEFTJOINnestedsettreeASbONb.leftedge=(SELECTMAX(leftedge)FROMnestedsettreeAStWHEREa.leftedge>t.leftedgeANDa.leftedge<t.rightedge)ORDERBYa.nodeID;
Foramulti-treeversionofListing10,implementingedgelistandnestedsetstreemodelsinasingletable,see“Multiplenestedsetstreesinonetable”onour
Findinganode'sparentandchildren
Inanedgelist,theparentofanodeistherow'sparentID,anditschildrenaretherowswherethatnodeIDisparentID.Whatcouldbesimpler?Incomparison,nestedsetsqueriesforparentsandtheirchildrenaretortuousandslow.Onewaytofetchthe
childnodesofagivennodeistoINNERJOINthenestedsetstreetableASparenttoitselfASchildONchild.leftedgeBETWEENparent.leftedgeANDparent.rightedge,thenscopeonthetargetrow'sleftedgeandrightedgevalues.Intheresultinglist,child.nodeID
valuesoneleveldownoccuronceandarechildren,grandkidsaretwolevelsdownandoccurtwice,andsoon:
Listing11SELECTPersonName(child.nodeID)AS'DescendantsofWilliam',COUNT(*)ASGenerationFROMnestedsettreeASparentJOINnestedsettreeASchildONchild.leftedgeBETWEENparent.leftedgeANDparent.rightedgeWHEREparent.leftedge>7ANDparent.rightedge<24--William’sleftedge,rightedgeGROUPBYchild.nodeID;+------------------------+------------+|DescendantsofWilliam|Generation|+------------------------+------------+|SusanaShakespeare|1||HamnetShakespeare|1||JudithShakespeare|1||ElizabethHall|2||ShakespeareQuiney|2||RichardQuiney|2||ThomasQuiney|2||JohnBernard|3|+------------------------+------------+
Therefore
HAVINGCOUNT(t2.nodeID)=1scopeslisteddescendantstothechildren:
Listing11aSELECTPersonName(child.nodeID)AS'ChildrenofWilliam'FROMnestedsettreeASparentJOINnestedsettreeASchildONchild.leftedgeBETWEENparent.leftedgeANDparent.rightedgeWHEREparent.leftedge>7ANDparent.rightedge<24GROUPBYchild.nodeIDHAVINGCOUNT(child.nodeID)=1+---------------------+|ChildrenofWilliam|+---------------------+|SusanaShakespeare||HamnetShakespeare||JudithShakespeare|+---------------------+
Retrievingasubtreeorasubsetofparentsrequiresyetanotherjoin:
Listing11bSELECTParent,Group_Concat(ChildORDERBYChild)ASChildrenFROM(SELECTmaster.nodeIDASParent,child.nodeIDASChildFROMnestedsettreeASmasterJOINnestedsettreeASparentJOINnestedsettreeASchildONchild.leftedgeBETWEENparent.leftedgeANDparent.rightedgeWHEREparent.leftedge>master.leftedgeANDparent.rightedge<master.rightedgeGROUPBYmaster.nodeID,child.nodeIDHAVINGCOUNT(*)=1)AStmpWHEREparentin(6,8,12,14)GROUPBYParent;+--------+-------------------+|Parent|Children|+--------+-------------------+|6|12,13,14||8|15,16,17,18||12|19||14|20,21,22|+--------+-------------------+
Thistakeshundredsoftimeslongerthanaqueryforthesameinfofromanedgelist!Anaggregatingversionof
Listing19iseasiertowrite,butisanevenworseperformer:
Listing11cSELECTp.nodeIDASParent,Group_Concat(c.nodeID)ASChildrenFROMnestedsettreeASpJOINnestedsettreeAScONp.leftedge=(SELECTMAX(s.leftedge)FROMnestedsettreeASsWHEREc.leftedge>s.leftedgeANDc.leftedge<s.rightedge)GROUPBYParent;+--------+-------------------+|Parent|Children|+--------+-------------------+|1|2,3||2|5,6,7,8,9,10,11,4||6|12,13,14||8|15,16,17,18||12|19||14|20,21,22||19|23|+--------+-------------------+
LogicthatisreciprocaltothatofListing11agetsustheparentofanode:
retrieveits
leftedgeand
rightedgevalues,
computeitslevel,
findthenodewhichisonelevelupandhasedgevaluesoutsidethenode's
leftedgeand
rightedgevalues.
Listing12DROPPROCEDUREIFEXISTSShowNestedSetParent;DELIMITERgoCREATEPROCEDUREShowNestedSetParent(nodeSMALLINT)BEGINDECLARElevel,thisleft,thisrightSMALLINTDEFAULT0;--findnodeedgesSELECTleftedge,rightedgeINTOthisleft,thisrightFROMnestedsettreeWHEREnodeID=node;--findnodelevelSELECTCOUNT(n1.nodeid)INTOlevelFROMnestedsettreeASn1INNERJOINnestedsettreeASn2ONn2.leftedgeBETWEENn1.leftedgeANDn1.rightedgeWHEREn2.nodeid=nodeGROUPBYn2.nodeid;--findparentSELECTPersonName(n2.nodeid)ASParentFROMnestedsettreeASn1INNERJOINnestedsettreeASn2ONn2.leftedgeBETWEENn1.leftedgeANDn1.rightedgeWHEREn2.leftedge<thisleftANDn2.rightedge>thisrightGROUPBYn2.nodeidHAVINGCOUNT(n1.nodeid)=level-1;END;goDELIMITER;CALLShowNestedSetParent(6);+-------------------+|Parent|+-------------------+|HenryShakespeare|+-------------------+
Otherqueries
Forsomequeryproblems,edgelistandnestedsetsqueriesareequivalentlysimple.Forexampletofindthetreerootandleaves,compare
Listing6with:
Listing13SELECTnameASPaterfamilias,IFNULL(born,'?')ASBorn,IFNULL(died,'?')ASDiedFROMnestedsettreeAStINNERJOINfamilyASfONt.nodeID=f.IDWHEREleftedge=1;+---------------------+------+------+|Paterfamilias|Born|Died|+---------------------+------+------+|RichardShakespeare|?|1561|+---------------------+------+------+SELECTnameAS'ChildlessShakespeares',IFNULL(born,'?')ASBorn,IFNULL(died,'?')ASDiedFROMnestedsettreeAStINNERJOINfamilyASfONt.nodeID=f.IDWHERErightedge=leftedge+1;+------------------------+------+------+|ChildlessShakespeares|Born|Died|+------------------------+------+------+|JoanShakespeare|1558|?||MargaretShakespeare|1562|1563||JohnBernard|?|1674||HamnetShakespeare|1585|1596||ShakespeareQuiney|1616|1617||RichardQuiney|1618|1639||ThomasQuiney|1620|1639||GilbertShakespeare|1566|1612||WilliamHart|1600|1639||MaryHart|1603|1607||ThomasHart|1605|1670||MichaelHart|1608|1618||AnneShakespeare|1571|1579||RichardShakespeare|1574|1613||EdmondShakespeare|1580|1607||JohnShakespeare|1530|1601|+------------------------+------+------+
Findingsubtreesinanestedsetsmodelrequiresnotwistedcode,nostoredprocedure.Toretrievethe
nestedsettreenodesinWilliam'ssubtree,justaskfornodeswhose
leftedgevaluesaregreater,andwhose
rightedgevaluesaresmallerthanWilliam's:
Listing14SELECTPersonName(t.nodeID)ASDescendantFROMnestedsettreeASsJOINnestedsettreeAStONs.leftedge<t.leftedgeANDs.rightedge>t.rightedgeJOINfamilyfONs.nodeID=f.IDWHEREf.name='WilliamShakespeare';
Findingasinglepathinthenestedsetsmodelisaboutascomplicatedasedgelistpathenumeration(Listings
8,
9):
Listing15SELECTt2.nodeIDASNode,PersonName(t2.nodeID)ASPerson,(SELECTCOUNT(*)FROMnestedsettreeASt4WHEREt4.leftedgeBETWEENt1.leftedgeANDt1.rightedgeANDt2.leftedgeBETWEENt4.leftedgeANDt4.rightedge)ASPathFROMnestedsettreeASt1INNERJOINnestedsettreeASt2ONt2.leftedgeBETWEENt1.leftedgeANDt1.rightedgeINNERJOINnestedsettreeASt3ONt3.leftedgeBETWEENt2.leftedgeANDt2.rightedgeWHEREt1.nodeID=(SELECTIDFROMfamilyWHEREname='WilliamShakespeare')ANDt3.nodeID=(SELECTIDFROMfamilyWHEREname='JohnBernard');+------+---------------------+------+|Node|Person|Path|+------+---------------------+------+|6|WilliamShakespeare|1||12|SusanaShakespeare|2||19|ElizabethHall|3||23|JohnBernard|4|+------+---------------------+------+
Displayingthetree
Herethenestedsetsmodelshines.Thearithmeticthatwasusedtobuildthetreemakesshortworkofsummaryqueries.Forexampletoretrieveanodelistwhichpreservesallparent-childrelations,weneedjusttwofacts:
listingorderistheordertakeninthenodewalkthatcreatedthetree,i.e.
leftedge,
anode'sindentationdepthissimplytheJOIN(edge)countfromroottonode:
Listing16SELECTCONCAT(SPACE(2*COUNT(parent.nodeid)-2),PersonName(child.nodeid))AS'TheShakespeareFamilyTree'FROMnestedsettreeASparentINNERJOINnestedsettreeASchildONchild.leftedgeBETWEENparent.leftedgeANDparent.rightedgeGROUPBYchild.nodeidORDERBYchild.leftedge;+-----------------------------+|TheShakespeareFamilyTree|+-----------------------------+|RichardShakespeare||HenryShakespeare||JoanShakespeare||MargaretShakespeare||WilliamShakespeare||SusanaShakespeare||ElizabethHall||JohnBernard||HamnetShakespeare||JudithShakespeare||ShakespeareQuiney||RichardQuiney||ThomasQuiney||GilbertShakespeare||JoanShakespeare||WilliamHart||MaryHart||ThomasHart||MichaelHart||AnneShakespeare||RichardShakespeare||EdmondShakespeare||JohnShakespeare|+-----------------------------+
Toretrieveonlyasubtree,addaqueryclausewhichrestrictsnodestothosewhoseedgesarewithintherangeoftheparentnode'sleftandrightedgevalues,forexampleforWilliamandhisdescendants...
WHEREparent.leftedge>=7ANDparent.rightedge<=24
Nodeinsertions,updatesanddeletions
Nestedsetsarithmeticalsohelpswithinsertions,updatesanddeletions,buttheproblemremainsthatchangingjustonenodecanrequirechangingmuchofthetree.
Insertinganodeinthetreerequiresatleasttwopiecesofinformation:the
nodeIDtoinsert,andthe
nodeIDofitsparent.Themodelisnormalisedsothe
nodeIDfirstmusthavebeenaddedtotheparent(
family)table.Thealgorithmforaddingthenodetothetreeis:
increment
leftedgeby2innodeswherethe
rightedgevalueisgreaterthantheparent's
rightedge,
increment
rightedgeby2innodeswherethe
leftedgevalueisgreaterthantheparent's
leftedge,
insertarowwiththegiven
nodeID,
leftedge=1+parent's
leftedge,
rightedge=2+parent's
leftedge.
That'snotdifficult,butallrowswillhavetobeupdated!
Listing17DROPPROCEDUREIFEXISTSInsertNestedSetNode;DELIMITERgoCREATEPROCEDUREInsertNestedSetNode(INnodeSMALLINT,INparentSMALLINT)BEGINDECLAREparentleft,parentrightSMALLINTDEFAULT0;SELECTleftedge,rightedgeINTOparentleft,parentrightFROMnestedsettreeWHEREnodeID=parent;IFFOUND_ROWS()=1THENBEGINUPDATEnestedsettreeSETrightedge=rightedge+2WHERErightedge>parentleft;UPDATEnestedsettreeSETleftedge=leftedge+2WHEREleftedge>parentleft;INSERTINTOnestedsettreeVALUES(0,node,parentleft+1,parentleft+2);END;ENDIF;END;goDELIMITER;
"Sibline"orhorizontalorderisobviouslysignificantinfamilytrees,butmaynotbesignificantinothertrees.Listing17addsthenewnodeattheleftedgeofthesibline.Tospecifyanotherposition,modifytheproceduretoacceptathirdparameter
forthe
nodeIDwhichistobetotheleftorrightoftheinsertionpoint.
Updatinganodeinplacerequiresnothingmorethanediting
nodeIDtopointatadifferentparentrow.
Deletinganoderaisestheproblemofhowtorepairlinksseveredbythedeletion.Intreemodelsofpartsexplosions,theitemtobedeletedisoftenreplacedbyanewitem,soitcanbetreatedlikeasimple
nodeIDupdate.Inorganisationalboss-employeecharts,though,doesacolleaguemoveover,doesasubordinategetpromoted,doeseverybodyinthesubtreemoveupalevel,ordoessomethingelsehappen?Noformulacancatchallthepossibilities.
Listing18illustrateshowtohandletwocommonscenarios,moveeveryoneup,andmovesomeoneover.Allpossibilitiesexceptsimplereplacementofthe
nodeIDinvolvechangeselsewhereinthetree.
Listing18DROPPROCEDUREIFEXISTSDeleteNestedSetNode;DELIMITERgoCREATEPROCEDUREDeleteNestedSetNode(INmodeCHAR(7),INnodeSMALLINT)BEGINDECLAREthisleft,thisrightSMALLINTDEFAULT0;SELECTleftedge,rightedgeINTOthisleft,thisrightFROMnestedsettreeWHEREnodeID=node;IFmode='PROMOTE'THENBEGIN--IanHolsmanfoundthesetwobugsDELETEFROMnestedsettreeWHEREnodeID=node;UPDATEnestedsettreeSETleftedge=leftedge-1,rightedge=rightedge-1--ratherthan=thisleftWHEREleftedgeBETWEENthisleftANDthisright;UPDATEnestedsettreeSETrightedge=rightedge-2WHERErightedge>thisright;UPDATEnestedsettreeSETleftedge=leftedge-2WHEREleftedge>thisright;--ratherthan>thisleftEND;ELSEIFmode='REPLACE'THENBEGINUPDATEnestedsettreeSETleftedge=thisleft-1,rightedge=thisrightWHEREleftedge=thisleft+1;UPDATEnestedsettreeSETrightedge=rightedge-2WHERErightedge>thisleft;UPDATEnestedsettreeSETleftedge=leftedge-2WHEREleftedge>thisleft;DELETEFROMnestedsettreeWHEREnodeID=node;END;ENDIF;END;goDELIMITER;
Nestedssetmodelsummary
Somenestedsetsqueriesarequickerthantheiredgelistcounterparts,somearen't.Giventheconcurrencynightmarewhichnestedsetsimposeforinsertsanddeletions,itisreasonabletoreservethenestedsetsmodelforfairlystatictreeswhoseusers
aremostlyinterestedinqueryingsubtrees.YoucouldthinkofthenestedsetsmodelasaspecialisedOLAPtool:maintainanOLTPtreeinanedgelistrepresentation,andbuildanestedsetsOLAPtablewhencertainreportsareneeded.
Ifyouwillbeusingthenestedsetsmodel,youmaybeconvertingbackandforthwithedgelistmodels,sohereisasimplequerywhichgeneratesanedgelistfromanestedsetstree:
Listing19SELECTp.nodeIDASparentID,c.nodeIDASchildIDFROMnestedsettreeASpINNERJOINnestedsettreeAScONp.leftedge=(SELECTMAX(s.leftedge)FROMnestedsettreeASsWHEREc.leftedge>s.leftedgeANDc.leftedge<s.rightedge)ORDERBYp.nodeID;
Edgelistmodelofanon-treegraph
Manygraphsarenottrees.Imagineasmallairlinewhichhasjustacquiredlicencesforflightsnolongerthan6,000kmbetweenLosAngeles(LAX),NewYork(JFK),HeathrowinLondon,CharlesdeGaulleinParis,Amsterdam-Schiphol,ArlandainSweden,andHelsinki-Vantaa.Youhavebeenaskedtocomputetheshortestpossibleone-wayroutesthatdonotdeviatemorethan90°fromthedirectionofthefirsthop—roughly,one-wayroutesandnocircuits.
Airportsarenodes,flightsareedges,routesarepaths.Wewillneedthreetables.
Airports(nodes)
Toidentifyanairportweneeditscode,locationname,latitudeandlongitude.Latitudeandlongitudeareusuallygivenasdegrees,minutesandseconds,northorsouthoftheequator,eastorwestofGreenwich.Tohidedetailsthataren'tdirectlyrelevant
tonodesandedges,codelatitudeandlongitudeassimplerealswherelongitudeswestofGreenwichandlatitudessouthoftheequatorarenegative,whilstlongitudeseastofGreenwichandlatitudesnorthoftheequatorarepositive:
Listing20CREATETABLEairports(codechar(3)NOTNULL,citychar(100)defaultNULL,latitudefloatNOTNULL,longitudefloatNOTNULL,PRIMARYKEY(code))ENGINE=MyISAM;INSERTINTOairportsVALUES('JFK','NewYork,NY',40.75,-73.97);INSERTINTOairportsVALUES('LAX','LosAngeles,CA',34.05,-118.22);INSERTINTOairportsVALUES('LHR','London,England',51.5,-0.45);INSERTINTOairportsVALUES('HEL','Helsinki,Finland',60.17,24.97);INSERTINTOairportsVALUES('CDG','Paris,France',48.86,2.33);INSERTINTOairportsVALUES('STL','StLouis,MO',38.63,-90.2);INSERTINTOairportsVALUES('ARN','Stockholm,Sweden',59.33,18.05);
Flights(edges)
Themodelattachestwoweightstoflights:distanceanddirection.
WeneedamethodofcalculatingtheGreatCircleDistance—thegeographicaldistancebetweenanytwocities-anothernaturaljobforastoredfunction.Thedistancecalculation
convertstoradiansthedegreecoordinatesofanytwopointsontheearth'ssurface,
calculatestheangleofthearcsubtendedbythetwopoints,and
convertstheresult,alsoinradians,tosurface(circumferential)kilometres(1radian=6,378.388km).
Listing21SETGLOBALlog_bin_trust_function_creators=TRUE;--since5.0.16DROPFUNCTIONIFEXISTSGeoDistKM;DELIMITERgoCREATEFUNCTIONGeoDistKM(lat1FLOAT,lon1FLOAT,lat2FLOAT,lon2FLOAT)RETURNSfloatBEGINDECLAREpi,q1,q2,q3FLOAT;SETpi=PI();SETlat1=lat1*pi/180;SETlon1=lon1*pi/180;SETlat2=lat2*pi/180;SETlon2=lon2*pi/180;SETq1=COS(lon1-lon2);SETq2=COS(lat1-lat2);SETq3=COS(lat1+lat2);SETrads=ACOS(0.5*((1.0+q1)*q2-(1.0-q1)*q3));RETURN6378.388*rads;END;goDELIMITER;
Thattakescareofflightdistances.Flightdirectionis,approximately,thearctangent(ATAN)ofthedifferencebetween
flights.departand
flights.arrivelatitudesandlongitudes.Nowwecanseedtheairline's
flightstablewithone-hopflightsupto6,000kmlong:
Listing22CREATETABLEflights(idINTPRIMARYKEYAUTO_INCREMENT,departCHAR(3),arriveCHAR(3),distanceDECIMAL(10,2),directionDECIMAL(10,2))ENGINE=MYISAM;INSERTINTOflightsSELECTNULL,depart.code,arrive.code,ROUND(GeoDistKM(depart.latitude,depart.longitude,arrive.latitude,arrive.longitude),2),ROUND(DEGREES(ATAN(arrive.latitude-depart.latitude,arrive.longitude-depart.longitude)),2)FROMairportsASdepartINNERJOINairportsASarriveONdepart.code<>arrive.codeHAVINGKm<=6000;SELECT*FROMflights;+----+--------+--------+----------+-----------+|id|depart|arrive|distance|direction|+----+--------+--------+----------+-----------+|1|LAX|JFK|3941.18|8.61||2|LHR|JFK|5550.77|-171.68||3|CDG|JFK|5837.46|-173.93||4|STL|JFK|1408.11|7.44||5|JFK|LAX|3941.18|-171.39||6|STL|LAX|2553.37|-170.72||7|JFK|LHR|5550.77|8.32||8|HEL|LHR|1841.91|-161.17||9|CDG|LHR|354.41|136.48||10|ARN|LHR|1450.12|-157.06||11|LHR|HEL|1841.91|18.83||12|CDG|HEL|1912.96|26.54||13|ARN|HEL|398.99|6.92||14|JFK|CDG|5837.46|6.07||15|LHR|CDG|354.41|-43.52||16|HEL|CDG|1912.96|-153.46||17|ARN|CDG|1545.23|-146.34||18|JFK|STL|1408.11|-172.56||19|LAX|STL|2553.37|9.28||20|LHR|ARN|1450.12|22.94||21|HEL|ARN|398.99|-173.08||22|CDG|ARN|1545.23|33.66|+----+--------+--------+----------+-----------+
Thedistancesagreeapproximatelywithpublicinformationsourcesforflightlengths.ForapairofairportsAandBnotverynearthepoles,theerrorincalculatingdirectionusingATAN(),issmall.Toremovethaterror,insteadofATAN()useaformula
fromsphericaltrigonometry(forexampleoneoftheformulasat
Routes(paths)
Arouteisapathalongoneormoreoftheseedges,so
flights:routesisa1:manyrelationship.Forsimplicity,though,wedenormaliseourrepresentationofrouteswithavariationofthe
materialisedpathmodeltostoreallthehopsofonerouteasalistofflightsinone
routescolumn.Thecolumn
routes.routeisthesequenceofairports,fromfirstdeparturetofinalarrival,thecolumn
routes.hopsisthenumberofhopsinthatroute,andthecolumn
routes.directionisthedirection:
Listing23CREATETABLEroutes(idINTPRIMARYKEYAUTO_INCREMENT,departCHAR(3),arriveCHAR(3),hopsSMALLINT,routeCHAR(50),distanceDECIMAL(10,2),directionDECIMAL(10,2))ENGINE=MYISAM;
Startingwithanempty
routestable,howdowepopulateitwiththeshortest
routesbetweenallorderedpairsof
airports?
Insertall1-hopflightsfromthe
flightstable.
Addinthesetofshortestmulti-hoproutesforallpairsofairportswhichdon'thaverowsintheflightstable.
For1-hopflightswejustwrite
Listing24INSERTINTOroutesSELECTNULL,depart,arrive,1,CONCAT(depart,',',arrive),distance,directionFROMflights;
NULLbeingtheplaceholderfortheauto-incrementing
idcolumn.
Formulti-hoproutes,weiterativelyaddinsetsofallallowed2-hop,3-hop,...n-hoproutes,replacinglongerroutesbyshorterroutesaswefindthem,untilthereisnothingmoretoaddorreplace.Thatalsobreaksdowntotwologicalsteps:addhopsto
buildthesetofnextallowedroutes,andupdatelongerrouteswithshorterones.
Nextallowedroutes
Thesetofnextallowedroutesisthesetofshortestroutesthatcanbebuiltbyadding,toexistingroutes,flightswhichleavefromthelastarrivalairportofanexistingroute,whicharriveatanairportwhichisnotyetinthegivenroute,andwhich
staywithin±90°oftheroute'sinitialcompassdirection.Thatis,everynewrouteisaJOINbetween
routesand
flightsinwhich
depart=routes.depart,
arrive=flights.arrive,
flights.depart=routes.arrive,
distance=
MIN(routes.distance+flights.distance),
LOCATE(
flights.arrive,routes.route)=0,
flights.direction+360>routes.direction+270ANDflights.direction+360<routes.direction+450
ThisisanaturallogicalunitofworkforaVIEW:
Listing25CREATEORREPLACEVIEWnextroutesASSELECTroutes.depart,flights.arrive,routes.hops+1AShops,CONCAT(routes.route,',',flights.arrive)ASroute,MIN(routes.distance+flights.distance)ASdistance,routes.directionFROMroutesINNERJOINflightsONroutes.arrive=flights.departAND[code]LOCATE(
flights.arrive,routes.route)=0WHEREflights.direction+360>routes.direction+270ANDflights.direction+360<routes.direction+450GROUPBYdepart,arrive;[/code]
Howtoaddthesenewhopsto
routes?InstandardSQL,thisvariantonaquerybyScottStephensshoulddoit...
Listing26INSERTINTOroutesSELECTNULL,depart,arrive,hops,route,distance,directionFROMnextroutesWHERE(nextroutes.depart,nextroutes.arrive)NOTIN(SELECTdepart,arriveFROMroutes);
butMySQLdoesnotyetsupportsubqueriesonthetablebeingupdated.Wehavetouseasubquery-less(andfaster)versionofthatlogic:
Listing27INSERTINTOroutesSELECTNULL,nextroutes.depart,nextroutes.arrive,nextroutes.hops,nextroutes.route,nextroutes.distance,nextroutes.directionFROMnextroutesLEFTJOINroutesONnextroutes.depart=routes.departANDnextroutes.arrive=routes.arriveWHEREroutes.departISNULLANDroutes.arriveISNULL;
Runningthatcoderightaftertheinitialseedingfrom
flightsgives...
SELECT*FROMroutes;+----+--------+--------+------+-------------+----------+-----------+|id|depart|arrive|hops|route|distance|direction|+----+--------+--------+------+-------------+----------+-----------+|1|LAX|JFK|1|LAX,JFK|3941.18|8.61||2|LHR|JFK|1|LHR,JFK|5550.77|-171.68||3|CDG|JFK|1|CDG,JFK|5837.46|-173.93||4|STL|JFK|1|STL,JFK|1408.11|7.44||5|JFK|LAX|1|JFK,LAX|3941.18|-171.39||6|STL|LAX|1|STL,LAX|2553.37|-170.72||7|JFK|LHR|1|JFK,LHR|5550.77|8.32||8|HEL|LHR|1|HEL,LHR|1841.91|-161.17||9|CDG|LHR|1|CDG,LHR|354.41|136.48||10|ARN|LHR|1|ARN,LHR|1450.12|-157.06||11|LHR|HEL|1|LHR,HEL|1841.91|18.83||12|CDG|HEL|1|CDG,HEL|1912.96|26.54||13|ARN|HEL|1|ARN,HEL|398.99|6.92||14|JFK|CDG|1|JFK,CDG|5837.46|6.07||15|LHR|CDG|1|LHR,CDG|354.41|-43.52||16|HEL|CDG|1|HEL,CDG|1912.96|-153.46||17|ARN|CDG|1|ARN,CDG|1545.23|-146.34||18|JFK|STL|1|JFK,STL|1408.11|-172.56||19|LAX|STL|1|LAX,STL|2553.37|9.28||20|LHR|ARN|1|LHR,ARN|1450.12|22.94||21|HEL|ARN|1|HEL,ARN|398.99|-173.08||22|CDG|ARN|1|CDG,ARN|1545.23|33.66||23|ARN|JFK|2|ARN,LHR,JFK|7000.89|-157.06||24|CDG|LAX|2|CDG,JFK,LAX|9778.64|-173.93||25|CDG|STL|2|CDG,JFK,STL|7245.57|-173.93||26|HEL|JFK|2|HEL,LHR,JFK|7392.68|-161.17||27|JFK|ARN|2|JFK,LHR,ARN|7000.89|8.32||28|JFK|HEL|2|JFK,LHR,HEL|7392.68|8.32||29|LAX|CDG|2|LAX,JFK,CDG|9778.64|8.61||30|LAX|LHR|2|LAX,JFK,LHR|9491.95|8.61||31|LHR|LAX|2|LHR,JFK,LAX|9491.95|-171.68||32|LHR|STL|2|LHR,JFK,STL|6958.88|-171.68||33|STL|CDG|2|STL,JFK,CDG|7245.57|7.44||34|STL|LHR|2|STL,JFK,LHR|6958.88|7.44|+----+--------+--------+------+-------------+----------+-----------+
...adding12two-hoprows.
Replacelongerrouteswithshorterones
Aswebuildrouteswithmorehops,itislogicallypossiblethatthe
nextroutesviewwillfindshorterroutesforanexisting
routespairof
departand
arrive.StandardSQLforreplacingexisting
routesrowswith
nextroutesrowswhichmatch
(depart,arrive)andhaveshorter
distancevalueswouldbe:
Listing28UPDATEroutesSET(hops,route,distance,direction)=(SELECThops,route,distance,directionFROMnextroutesWHEREnextroutes.depart=routes.departANDnextroutes.arrive=routes.arrive)WHERE(depart,arrive)IN(SELECTdepart,arriveFROMnextroutesWHEREnextroutes.distance<routes.distance);
butMySQLdoesnotsupport
SET(col1,...)syntax,andaswith
Listing7,MySQLdoesnotyetacceptsubqueriesreferencingthetablebeingupdated,sowehavetowritemoreliteralSQL:
Listing29UPDATEroutes,nextroutesSETroutes.hops=nextroutes.hops,routes.route=nextroutes.route,routes.distance=nextroutes.distance,routes.direction=nextroutes.directionWHEREroutes.arrive=nextroutes.arriveANDroutes.depart=nextroutes.departANDnextroutes.distance<routes.distance;
RunningthiscoderightafterthefirstrunofListing27updatesnorows.Totestthelogicofiteration,continuerunningListings27and29untilnorowsarebeingaddedorchanged.Thefinalresultis:
SELECT*FROMROUTES;+----+--------+--------+------+-----------------+----------+-----------+|id|depart|arrive|hops|route|distance|direction|+----+--------+--------+------+-----------------+----------+-----------+|1|LAX|JFK|1|LAX,JFK|3941.18|8.61||2|LHR|JFK|1|LHR,JFK|5550.77|-171.68||3|CDG|JFK|1|CDG,JFK|5837.46|-173.93||4|STL|JFK|1|STL,JFK|1408.11|7.44||5|JFK|LAX|1|JFK,LAX|3941.18|-171.39||6|STL|LAX|1|STL,LAX|2553.37|-170.72||7|JFK|LHR|1|JFK,LHR|5550.77|8.32||8|HEL|LHR|1|HEL,LHR|1841.91|-161.17||9|CDG|LHR|1|CDG,LHR|354.41|136.48||10|ARN|LHR|1|ARN,LHR|1450.12|-157.06||11|LHR|HEL|1|LHR,HEL|1841.91|18.83||12|CDG|HEL|1|CDG,HEL|1912.96|26.54||13|ARN|HEL|1|ARN,HEL|398.99|6.92||14|JFK|CDG|1|JFK,CDG|5837.46|6.07||15|LHR|CDG|1|LHR,CDG|354.41|-43.52||16|HEL|CDG|1|HEL,CDG|1912.96|-153.46||17|ARN|CDG|1|ARN,CDG|1545.23|-146.34||18|JFK|STL|1|JFK,STL|1408.11|-172.56||19|LAX|STL|1|LAX,STL|2553.37|9.28||20|LHR|ARN|1|LHR,ARN|1450.12|22.94||21|HEL|ARN|1|HEL,ARN|398.99|-173.08||22|CDG|ARN|1|CDG,ARN|1545.23|33.66||23|ARN|JFK|2|ARN,LHR,JFK|7000.89|-157.06||24|CDG|LAX|2|CDG,JFK,LAX|9778.64|-173.93||25|CDG|STL|2|CDG,JFK,STL|7245.57|-173.93||26|HEL|JFK|2|HEL,LHR,JFK|7392.68|-161.17||27|JFK|ARN|2|JFK,LHR,ARN|7000.89|8.32||28|JFK|HEL|2|JFK,LHR,HEL|7392.68|8.32||29|LAX|CDG|2|LAX,JFK,CDG|9778.64|8.61||30|LAX|LHR|2|LAX,JFK,LHR|9491.95|8.61||31|LHR|LAX|2|LHR,JFK,LAX|9491.95|-171.68||32|LHR|STL|2|LHR,JFK,STL|6958.88|-171.68||33|STL|CDG|2|STL,JFK,CDG|7245.57|7.44||34|STL|LHR|2|STL,JFK,LHR|6958.88|7.44||35|ARN|LAX|3|ARN,LHR,JFK,LAX|10942.07|-157.06||36|ARN|STL|3|ARN,LHR,JFK,STL|8409.00|-157.06||37|HEL|LAX|3|HEL,LHR,JFK,LAX|11333.86|-161.17||38|HEL|STL|3|HEL,LHR,JFK,STL|8800.79|-161.17||39|LAX|ARN|3|LAX,JFK,CDG,ARN|10942.07|8.61||40|LAX|HEL|3|LAX,JFK,CDG,HEL|11333.86|8.61||41|STL|ARN|3|STL,JFK,CDG,ARN|8409.00|7.44||42|STL|HEL|3|STL,JFK,CDG,HEL|8800.79|7.44|+----+--------+--------+------+-----------------+----------+-----------+
Allthat'slefttodoistoassemblethecodeinastoredprocedure:
Listing30DROPPROCEDUREIFEXISTSBuildRoutes;DELIMITERgoCREATEPROCEDUREBuildRoutes()BEGINDECLARErowsINTDEFAULT0;TRUNCATEroutes;--STEP1,LISTING24:SEEDROUTESWITH1-HOPFLIGHTSINSERTINTOroutesSELECTNULL,depart,arrive,1,CONCAT(depart,',',arrive),distance,directionFROMflights;SETrows=ROW_COUNT();WHILE(rows>0)DO--STEP2,LISTINGS25,27:ADDNEXTSETOFROUTESINSERTINTOroutesSELECTNULL,nextroutes.depart,nextroutes.arrive,nextroutes.hops,nextroutes.route,nextroutes.distance,nextroutes.directionFROMnextroutesLEFTJOINroutesONnextroutes.depart=routes.departANDnextroutes.arrive=routes.arriveWHEREroutes.departISNULLANDroutes.arriveISNULL;SETrows=ROW_COUNT();ENDWHILE;END;goDELIMITER;
InMySQL5.0.6or5.0.7,
BuildRoutes()failstoinsertfourrows:
+--------+--------+-----------------+------+----------+-----------+ |depart|arrive|route|hops|distance|direction| +--------+--------+-----------------+------+----------+-----------+ |ARN|LAX|ARN,LHR,JFK,LAX|3|10942.07|-157.06| |ARN|STL|ARN,LHR,JFK,STL|3|8409.00|-157.06| |HEL|LAX|HEL,LHR,JFK,LAX|3|11333.86|-161.17| |HEL|STL|HEL,LHR,JFK,STL|3|8800.79|-161.17| +--------+--------+-----------------+------+----------+-----------+
ThatMySQLbug(
Routequeries
Routequeriesarestraightforward.Howdowecheckthatthealgorithmproducednoduplicate
depart-arrivepairs?Thefollowingqueryshouldyieldzerorows,
Listing31SELECTdepart,arrive,COUNT(*)FROMroutesGROUPBYdepart,arriveHAVINGCOUNT(*)>1;
anddoes.Reachabilityqueriesarejustassimple,forexamplewherecanweflytofromHelsinki?
Listing32SELECT*FROMroutesWHEREdepart='HEL'ORDERBYdistance;+----+--------+--------+------+-----------------+----------+-----------+|id|depart|arrive|hops|route|distance|direction|+----+--------+--------+------+-----------------+----------+-----------+|21|HEL|ARN|1|HEL,ARN|398.99|-173.08||8|HEL|LHR|1|HEL,LHR|1841.91|-161.17||16|HEL|CDG|1|HEL,CDG|1912.96|-153.46||26|HEL|JFK|2|HEL,LHR,JFK|7392.68|-161.17||38|HEL|STL|3|HEL,LHR,JFK,STL|8800.79|-161.17||37|HEL|LAX|3|HEL,LHR,JFK,LAX|11333.86|-161.17|+----+--------+--------+------+-----------------+----------+-----------+
Anextendededgelistmodelissimpletoimplement,gracefullyacceptsextendedattributesfornodes,edgeandpaths,doesnotundulypenaliseupdates,andrespondstoquerieswithreasonablespeed.
Partsexplosions
Abillofmaterialsforahousewouldincludethecementblock,lumber,shingles,doors,wallboard,windows,plumbing,electricalsystem,heatingsystem,andsoon.Eachsubassemblyalsohasabillofmaterials;theheatingsystemhasafurnace,ducts,andsoon.Abillofmaterialsimplosionlinkscomponentpiecestoamajorassembly.Abillofmaterialsexplosionbreaksapartassembliesandsubassembliesintotheircomponentparts.
Whichgraphmodelbesthandlesapartsexplosion?Combiningedgelistand"nestedsets"algorithmsseemsanaturalsolution.
Imagineanewcompanythatplanstomakevariouslysizedbookcases,eitherpackagedasdo-it-yourselfkitsof,orassembledfromsides,shelves,shelfbrackets,backboards,feetandscrews.Shelvesandsidesarecutfromplanks.Backboardsaretrimmedfrom
laminatedsheeting.Feetaremachine-carvedfromreadycutblocks.Screwsandshelfbracketsarepurchasedinbulk.Herearetheelementsofonebookcase:
1backboard,2x1m1laminate8screws2sides2mx30cm1planklength4m12screws8shelves1mx30cm(incl.topandbottom)2planks24shelfbrackets4feet4cmx4cm4cubes16screws
whichmaybepackagedinaboxforsaleatoneprice,orassembledasafinishedproductatadifferentprice.Atanytimeweneedtobeabletoanswerquestionslike
Dowehaveenoughpartstomakethebookcasesonorder?
Whatassembliesorpackageswouldbemostprofitabletomakegiventhecurrentinventory?
Thereisnoreasontobreakthenormalisingrulethatitemdetailbelongsina
nodestable,andgraphlogicbelongsinanedgestable.Edgesalsorequireaquantityattribute,forexampleashelfincludesfourshelfbrackets.Nodesandedgesmayalsohavecostsandprices:
itempurchasecost,
itemassemblycost,
assemblycost,
assemblysellingprice.
Inmanypartsproblemslikethisone,itemsoccurinmultipleassembliesandsubassemblies.Thegraphisnotatree.Also,itisoftendesirabletomodelmultiplegraphswithoutthetableglutthatwouldarisefromgivingeachgraphitsownedgestable.
Asimplewaytosolvethisproblemistorepresentmultiplegraphs(assemblies)intheedgestablebygivingeveryrownotonly
childIDand
parentIDpointers,butapointerwhichidentifiestheroot
itemIDofthegraphtowhichtherowbelongs.
Sothedatamodelisjusttwotables,foritems(nodes)andforproductgraphsorassemblies(edges).Assumethatthecompanybeginswithaplantosellthe2mx1mbookcaseintwoforms,assembledandkit,andthatthepurchasingdepartment
hasboughtquantitiesofrawmaterials(laminate,planks,shelfsupports,screws,woodcubes,boxes).Herearethenodes(items)andedges(assemblies):
Listing33CREATETABLEitems(itemIDINTPRIMARYKEYAUTO_INCREMENT,nameCHAR(20)NOTNULL,onhandINTNOTNULLDEFAULT0,reservedINTNOTNULLDEFAULT0,purchasecostDECIMAL(10,2)NOTNULLDEFAULT0,assemblycostDECIMAL(10,2)NOTNULLDEFAULT0,priceDECIMAL(10,2)NOTNULLDEFAULT0);
CREATETABLEassemblies(
assemblyIDINTNOTNULL,
assemblyrootINTNOTNULL,
childIDINTNOTNULL,
parentIDINTNOTNULL,
quantityDECIMAL(10,2)NOTNULL,
assemblycostDECIMAL(10,2)NOTNULL,
PRIMARYKEY(assemblyID,childID,parentID)
);
INSERTINTOitemsVALUES--inventory
(1,'laminate',40,0,4,0,8),
(2,'screw',1000,0,0.1,0,.2),
(3,'plank',200,0,10,0,20),
(4,'shelfbracket',400,0,0.20,0,.4),
(5,'woodcube',100,0,0.5,0,1),
(6,'box',40,0,1,0,2),
(7,'backboard',0,0,0,3,0),
(8,'side',0,0,0,8,0),
(9,'shelf',0,0,0,4,0),
(10,'foot',0,0,0,1,0),
(11,'bookcase2x30',0,0,0,10,0),
(12,'bookcase2x30kit',0,0,0,2,0);
INSERTINTOassembliesVALUES
(1,11,1,7,1,0),--laminatetobackboard
(2,11,2,7,8,0),--screwstobackboard
(3,11,3,8,.5,0),--plankstoside
(4,11,2,8,6,0),--screwstoside
(5,11,3,9,0.25,0),--plankstoshelf
(6,11,4,9,4,0),--shelfbracketstoshelf
(7,11,5,10,1,0),--woodcubestofoot
(8,11,2,10,1,0),--screwstofoot
(9,11,7,11,1,0),--backboardtobookcase
(10,11,8,11,2,0),--sidestobookcase
(11,11,9,11,8,0),--shelvestobookcase
(12,11,10,11,4,0),--feettobookcase
(13,12,1,7,1,0),--laminatetobackboard
(14,12,2,7,8,0),--screwstobackboard
(15,12,3,8,0.5,0),--plankstoside
(16,12,2,8,6,0),--screwstosides
(17,12,3,9,0.25,0),--plankstoshelf
(18,12,4,9,4,0),--shelfbracketstoshelves
(19,12,5,10,1,0),--woodcubestofoot
(20,12,2,10,1,0),--screwstofoot
(21,12,7,12,1,0),--backboardtobookcasekit
(22,12,8,12,2,0),--sidestobookcasekit
(23,12,9,12,8,0),--shelvestobookcasekit
(24,12,10,12,4,0),--feettobookcasekit
(25,12,6,12,1,0);--containerboxtobookcasekit
Now,wewantapartslist,abillofmaterials,whichwilllistshowparent-childrelationshipsandquantities,andsumthecosts.Couldweadaptthedepth-first"nestedsets"treewalkalgorithm(Listing
10)tothisproblemeventhoughourgraphisnotatreeandoursetsarenotproperlynested?Yesindeed.Wejusthavetomodifythetreewalktohandlemultipleparentnodesforanychildnode,andaddcodetopercolatecostsandquantitiesupthegraph.
Navigationremainssimpleusing
leftedgeand
rightedgevalues.ThisisjustthesortofproblemtheCelkoalgorithmisgoodfor:reporting!
Listing34DROPPROCEDUREIFEXISTSShowBOM;DELIMITERgoCREATEPROCEDUREShowBOM(INrootINT)BEGINDECLAREthischild,thisparent,rows,maxrightedgeINTDEFAULT0;DECLAREthislevel,nextedgenumINTDEFAULT1;DECLAREthisqty,thiscostDECIMAL(10,2);--Createandseedintermediatetable:DROPTABLEIFEXISTSedges;CREATETABLEedges(childIDsmallintNOTNULL,parentIDsmallintNOTNULL,PRIMARYKEY(childID,parentID))ENGINE=HEAP;INSERTINTOedgesSELECTchildID,parentIDFROMassembliesWHEREassemblyRoot=root;SETmaxrightedge=2*(1+(SELECTCOUNT(*)FROMedges));--Createandseedresulttable:DROPTABLEIFEXISTSbom;CREATETABLEbom(levelSMALLINT,nodeIDSMALLINT,parentIDSMALLINT,qtyDECIMAL(10,2),costDECIMAL(10,2),leftedgeSMALLINT,rightedgeSMALLINT)ENGINE=HEAP;INSERTINTObomVALUES(thislevel,root,0,0,0,nextedgenum,maxrightedge);SETnextedgenum=nextedgenum+1;WHILEnextedgenum<maxrightedgeDO--Howmanychildrenofthisnoderemainintheedgestable?SETrows=(SELECTCOUNT(*)FROMbomASsINNERJOINedgesAStONs.nodeID=t.parentIDANDs.level=thislevel);IFrows>0THEN--Thereisatleastonechildedge.--Computeqtyandcost,insertintobom,deletefromedges.BEGIN--AlasMySQLnullsMIN(t.childid)whenwecombinethenexttwoqueriesSETthischild=(SELECTMIN(t.childID)FROMbomASsINNERJOINedgesAStONs.nodeID=t.parentIDANDs.level=thislevel);SETthisparent=(SELECTDISTINCTt.parentIDFROMbomASsINNERJOINedgesAStONs.nodeID=t.parentIDANDs.level=thislevel);SETthisqty=(SELECTquantityFROMassembliesWHEREassemblyroot=rootANDchildID=thischildANDparentID=thisparent);SETthiscost=(SELECTa.assemblycost+(thisqty*(i.purchasecost+i.assemblycost))FROMassembliesASaINNERJOINitemsASiONa.childID=i.itemIDWHEREassemblyroot=rootANDa.parentID=thisparentANDa.childID=thischild);INSERTINTObomVALUES(thislevel+1,thischild,thisparent,thisqty,thiscost,nextedgenum,NULL);DELETEFROMedgesWHEREchildID=thischildANDparentID=thisparent;SETthislevel=thislevel+1;SETnextedgenum=nextedgenum+1;END;ELSEBEGIN--Setrightedge,removeitemfromedgesUPDATEbomSETrightedge=nextedgenum,level=-levelWHERElevel=thislevel;SETthislevel=thislevel-1;SETnextedgenum=nextedgenum+1;END;ENDIF;ENDWHILE;SETrows:=(SELECTCOUNT(*)FROMedges);IFrows>0THENSELECT'Orphanedrowsremain';ELSE--TotalSETthiscost=(SELECTSUM(qty*cost)FROMbom);UPDATEbomSETqty=1,cost=thiscostWHEREnodeID=root;--ShowtheresultSELECTCONCAT(Space(Abs(level)*2),ItemName(nodeid,root))ASItem,ROUND(qty,2)ASQty,ROUND(cost,2)ASCostFROMbomORDERBYleftedge;ENDIF;END;goDELIMITER;--FunctionusedbyShowBOM()toretrievebomitemnames:DROPFUNCTIONIFEXISTSItemName;SETGLOBALlog_bin_trust_function_creators=TRUE;DELIMITERgoCREATEFUNCTIONItemName(idINT,rootINT)RETURNSCHAR(20)BEGINDECLAREsCHAR(20)DEFAULT'';SELECTnameINTOsFROMitemsWHEREitemid=id;RETURNIF(id=root,UCASE(s),s);END;go
DELIMITER;CALLShowBOM(11);+---------------------+------+--------+
|Item|Qty|Cost|
+---------------------+------+--------+
|BOOKCASE2X30|1.0|327.93|
|backboard|1.0|3.00|
|laminate|1.0|4.00|
|screw|8.0|0.80|
|side|2.0|16.00|
|screw|6.0|0.60|
|plank|0.5|5.00|
|shelf|8.0|32.00|
|plank|0.3|2.50|
|shelfbracket|4.0|0.80|
|foot|4.0|4.00|
|screw|1.0|0.10|
|woodcube|1.0|0.50|
+---------------------+------+--------+
With
ShowBOM()inhand,it'seasytocomparecostsofassembliesandsubassemblies.Byaddingpricecolumns,wecandothesameforpricesandprofitmargins.AndnowthatMySQLhasre-enabledpreparedstatementsinstoredprocedures,itwill
berelativelyeasytowriteamoregeneralversionof
ShowBOM().Weleavethattoyou.
Shorterandsweeter
ButShowBOM()isnotthesmall,efficientbitofnestedsetsreportingcodewewerehopingfor.Thereisasimplersolution:hidegraphcyclesfromtheedges
tablebymakingthemreferencestorowsinanodestable,sowecantreattheedgestablelikeatree;thenapplyabreadth-first
edge-listsubtreealgorithmtogeneratetheBillofMaterials.Againassumeacabinetmakingcompanymakingbookcases(withadifferentcostingmodel).Forclarity,skipinventorytrackingfornow.Anitemstableww_nodestrackspurchasedandassembledbookcase
elementswiththeirindividualcosts,andanassemblies/edgesww_edgestabletrackssetsofedgesthatcombinetomakeproducts.
Listing35:DDLforasimplerpartsexplosionDROPTABLEIFEXISTSww_nodes;
CREATETABLEww_nodes(
nodeIDint,
descriptionCHAR(50),
costdecimal(10,2)
);
INSERTINTOww_nodesVALUES(1,'finishedbookcase',10);
INSERTINTOww_nodesVALUES(2,'backboard2x1',1);
INSERTINTOww_nodesVALUES(3,'laminate2x1',8);
INSERTINTOww_nodesVALUES(4,'screw',.10);
INSERTINTOww_nodesVALUES(5,'side',4);
INSERTINTOww_nodesVALUES(6,'plank',20);
INSERTINTOww_nodesVALUES(7,'shelf',4);
INSERTINTOww_nodesVALUES(8,'shelfbracket',.5);
INSERTINTOww_nodesVALUES(9,'feet',1);
INSERTINTOww_nodesVALUES(10,'cube4cmx4cm',1);
INSERTINTOww_nodesVALUES(11,'bookcasekit',2);
INSERTINTOww_nodesVALUES(12,'carton',1);
DROPTABLEIFEXISTSww_edges;
CREATETABLEww_edges(
rootIDINT,
nodeIDint,
parentnodeIDint,
qtydecimal(10,2)
);
INSERTINTOww_edgesVALUES(1,1,null,1);
INSERTINTOww_edgesVALUES(1,2,1,1);
INSERTINTOww_edgesVALUES(1,3,2,1);
INSERTINTOww_edgesVALUES(1,4,2,8);
INSERTINTOww_edgesVALUES(1,5,1,2);
INSERTINTOww_edgesVALUES(1,6,5,1);
INSERTINTOww_edgesVALUES(1,4,5,12);
INSERTINTOww_edgesVALUES(1,7,1,8);
INSERTINTOww_edgesVALUES(1,6,7,.5);
INSERTINTOww_edgesVALUES(1,8,7,4);
INSERTINTOww_edgesVALUES(1,9,1,4);
INSERTINTOww_edgesVALUES(1,10,9,1);
INSERTINTOww_edgesVALUES(1,4,9,1);
INSERTINTOww_edgesVALUES(11,11,null,1);
INSERTINTOww_edgesVALUES(11,2,11,1);
INSERTINTOww_edgesVALUES(11,3,2,1);
INSERTINTOww_edgesVALUES(11,4,2,8);
INSERTINTOww_edgesVALUES(11,5,11,2);
INSERTINTOww_edgesVALUES(11,6,5,1);
INSERTINTOww_edgesVALUES(11,4,5,12);
INSERTINTOww_edgesVALUES(11,7,11,8);
INSERTINTOww_edgesVALUES(11,6,7,.5);
INSERTINTOww_edgesVALUES(11,8,7,4);
INSERTINTOww_edgesVALUES(11,9,11,4);
INSERTINTOww_edgesVALUES(11,10,9,1);
INSERTINTOww_edgesVALUES(11,4,9,11);
INSERTINTOww_edgesVALUES(11,12,11,1);
Hereisanadaptationofthebreadth-firstedgelistalgorithmtoretrieveaBillofMaterialsforaproductidentifiedbyarootID:
·Initialisealevel-trackingvariabletozero.
·SeedatempreportingtablewiththerootIDofthedesiredproduct.
·Whilerowsarebeingretrieved,incrementtheleveltrackingvariableandaddrowstothetemptablewhoseparentnodeIDsarenodesatthecurrentlevel.
·PrinttheBOMorderedbypathwithindentationproportionaltotreelevel.
Listing36:Asimplerpartsexplosion
DROPPROCEDUREIFEXISTSww_bom;
DELIMITERgo
CREATEPROCEDUREww_bom(rootINT)
BEGIN
DECLARElevINTDEFAULT0;
DECLAREtotalcostDECIMAL(10,2);
DROPTABLEIFEXISTStemp;
CREATETABLEtemp--initialisetemptablewithrootnode
SELECT
e.nodeIDASnodeID,
n.descriptionASItem,
e.parentnodeID,
e.qty,
n.costASnodecost,
e.qty*n.costAScost,
0aslevel,--treelevel
CONCAT(e.nodeID,'')ASpath--pathtothisnodeasastring
FROMww_nodesn
JOINww_edgeseUSING(nodeID)--rootnode
WHEREe.nodeID=rootANDe.parentnodeIDISNULL;
WHILEFOUND_ROWS()>0DO
BEGIN
SETlev=lev+1;--incrementlevel
INSERTINTOtemp--addchildrenofthislevel
SELECT
e.nodeID,
n.descriptionASItem,
e.parentnodeID,
e.qty,
n.costASnodecost,
e.qty*n.costAScost,
lev,
CONCAT(t.path,',',e.nodeID)
FROMww_nodesn
JOINww_edgeseUSING(nodeID)
JOINtemptONe.parentnodeID=t.nodeID
WHEREe.rootID=rootANDt.level=lev-1;
END;
ENDWHILE;
WHILElev>0DO--percolatecostsupthegraph
BEGIN
SETlev=lev-1;
DROPTABLEIFEXISTStempcost;
CREATETABLEtempcost--computechildcost
SELECTp.nodeID,SUM(c.nodecost*c.qty)ASchildcost
FROMtempp
JOINtempcONp.nodeid=c.parentnodeid
WHEREc.level=lev
GROUPbyp.nodeid;
UPDATEtempJOINtempcostUSING(nodeID)--updateparentitemcost
SETnodecost=nodecost+tempcost.childcost;
UPDATEtempSETcost=qty*nodecost--updateparentcost
WHERElevel=lev-1;
END;
ENDWHILE;
SELECT--listBoM
CONCAT(SPACE(level*2),Item)ASItem,
ROUND(nodecost,2)AS'UnitCost',
ROUND(Qty,0)ASQty,ROUND(cost,2)ASCostFROMtemp
ORDERbypath;
ENDgo
DELIMITER;
CALLww_bom(1);
+-------------------+-----------+------+--------+
|Item|UnitCost|Qty|Cost|
+-------------------+-----------+------+--------+
|finishedbookcase|206.60|1.0|206.60|
|backboard2x1|9.80|1.0|9.80|
|laminate2x1|8.00|1.0|8.00|
|screw|0.10|8.0|0.80|
|side|25.20|2.0|50.40|
|screw|0.10|12.0|1.20|
|plank|20.00|1.0|20.00|
|shelf|16.00|8.0|128.00|
|plank|20.00|0.5|10.00|
|shelfbracket|0.50|4.0|2.00|
|foot|2.10|4.0|8.40|
|cube4cmx4cm|1.00|1.0|1.00|
|screw|0.10|1.0|0.10|
+-------------------+-----------+------+--------+
Summary
Storedprocedures,storedfunctionsandViewsmakeitpossibletoimplementedgelistgraphmodels,nestedsetsgraphmodels,andbreadth-firstanddepth-firstgraphsearchalgorithmsinMySQL5&6.FurtherReading
CelkoJ,"TreesandHierarchiesinSQLForSmarties",MorganKaufman,SanFrancisco,2004.Codersource.net,"BranchandBoundAlgorithminC#",
MathForum,"Euler'sSolution:TheDegreeofaVertex",
MuhammadRB,"Trees",
MullinsC,"TheFutureofSQL",
RodrigueJ-P,"GraphTheory:DefinitionandProperties",
SantryP,"RecursiveSQLUserDefinedFunctions",
ShashaD,WangJTL,andGiugnoR,"Algorithmicsandapplicationsoftreeandgraphsearching",InSymposiumonPrinciplesofDatabaseSystems,2002,p39--52.
StephensS,"SolvingdirectedgraphproblemswithSQL,PartI",
Stephens,S,"SolvingdirectedgraphproblemswithSQL,PartII",
SteinbachT,"MigratingRecursiveSQLfromOracletoDB2UDB",
TropashkoV,"NestedIntervalsTreeEncodinginSQL,
VanTulderG,"StoringHierarchicalDatainaDatabase",
VenagallaS,"ExpandingRecursiveOpportunitieswithSQLUDFsinDB2v7.2",
Wikipedia,"GraphTheory",
Wikipedia,“Treetraversal”,
Wikipedia,“Treetraversal”,
WilletsK,"SQLGraphAlgorithms",
相关文章推荐
- Unique Indexes in MySQL and Other Databases
- Joe Celko's Trees and Hierarchies in SQL for Smarties
- Understanding JOINs in MySQL and Other Relational Databases 理解JOIN在MySQL和其他数据库中的运用
- More Trees & Hierarchies in SQL
- 解决Deprecated: mysql_connect(): The mysql extension is deprecated and will be removed in the future:
- Dates in PHP and MySQL
- 解决Deprecated: mysql_connect(): The mysql extension is deprecated and will be removed in the future:
- Docker安装MySQL遇见Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggre的问题
- MySQL backup and point in time recovery with binary logs
- How to Create a Secure Login Script in PHP and mySQL
- More Trees & Hierarchies in SQL
- 解决Deprecated: mysql_connect(): The mysql extension is deprecated and will be removed in the future:
- Grail upload image and storage as BLOB type in mysql
- Benchmarking and Profiling in MySQL
- MySQL报错“Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggre”
- phpmyadmin,解决Deprecated: mysql_connect(): The mysql extension is deprecated and will be removed in t
- mysql遇见Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggre的问题
- More Trees & Hierarchies in SQL
- 解决Deprecated: mysql_connect(): The mysql extension is deprecated and will be removed in the future:
- notes about crosstool,uboot,mysql and some tools in linux(ubuntu)