sql >> Database >  >> RDS >> Database

Grondbeginselen van tabeluitdrukkingen, deel 6 – Recursieve CTE's

Dit artikel is het zesde deel in een serie over tabeluitdrukkingen. Vorige maand heb ik in deel 5 de logische behandeling van niet-recursieve CTE's behandeld. Deze maand behandel ik de logische behandeling van recursieve CTE's. Ik beschrijf de ondersteuning van T-SQL voor recursieve CTE's, evenals standaardelementen die nog niet door T-SQL worden ondersteund. Ik bied logisch equivalente T-SQL-oplossingen voor de niet-ondersteunde elementen.

Voorbeeldgegevens

Voor voorbeeldgegevens gebruik ik een tabel met de naam Werknemers, die een hiërarchie van werknemers bevat. De empid-kolom vertegenwoordigt het werknemers-ID van de ondergeschikte (het onderliggende knooppunt) en de mgrid-kolom vertegenwoordigt het werknemers-ID van de manager (het bovenliggende knooppunt). Gebruik de volgende code om de tabel Werknemers in de tempdb-database te maken en in te vullen:

SET NOCOUNT ON;
 
USE tempdb;
DROP TABLE IF EXISTS dbo.Employees;
GO
 
CREATE TABLE dbo.Employees
(
  empid   INT         NOT NULL
    CONSTRAINT PK_Employees PRIMARY KEY,
  mgrid   INT         NULL     
    CONSTRAINT FK_Employees_Employees REFERENCES dbo.Employees,
  empname VARCHAR(25) NOT NULL,
  salary  MONEY       NOT NULL,
  CHECK (empid <> mgrid)
);
 
INSERT INTO dbo.Employees(empid, mgrid, empname, salary)
  VALUES(1,  NULL, 'David'  , $10000.00),
        (2,     1, 'Eitan'  ,  $7000.00),
        (3,     1, 'Ina'    ,  $7500.00),
        (4,     2, 'Seraph' ,  $5000.00),
        (5,     2, 'Jiru'   ,  $5500.00),
        (6,     2, 'Steve'  ,  $4500.00),
        (7,     3, 'Aaron'  ,  $5000.00),
        (8,     5, 'Lilach' ,  $3500.00),
        (9,     7, 'Rita'   ,  $3000.00),
        (10,    5, 'Sean'   ,  $3000.00),
        (11,    7, 'Gabriel',  $3000.00),
        (12,    9, 'Emilia' ,  $2000.00),
        (13,    9, 'Michael',  $2000.00),
        (14,    9, 'Didi'   ,  $1500.00);
 
CREATE UNIQUE INDEX idx_unc_mgrid_empid
  ON dbo.Employees(mgrid, empid)
  INCLUDE(empname, salary);

Merk op dat de wortel van de werknemershiërarchie - de CEO - een NULL heeft in de mgrid-kolom. Alle overige werknemers hebben een manager, dus hun mgrid-kolom is ingesteld op de werknemers-ID van hun manager.

Afbeelding 1 toont een grafische weergave van de werknemershiërarchie, met de naam, ID en locatie van de werknemer in de hiërarchie.

Figuur 1:Werknemershiërarchie

Recursieve CTE's

Recursieve CTE's hebben veel gebruiksscenario's. Een klassieke gebruikscategorie heeft te maken met het hanteren van grafiekstructuren, zoals onze werknemershiërarchie. Taken met grafieken vereisen vaak het doorlopen van paden met een willekeurige lengte. Geef bijvoorbeeld, gegeven de werknemers-ID van een manager, de invoermedewerker terug, evenals alle ondergeschikten van de invoermedewerker op alle niveaus; dat wil zeggen, directe en indirecte ondergeschikten. Als u een bekende kleine limiet had voor het aantal niveaus dat u mogelijk moet volgen (de graad van de grafiek), zou u een query kunnen gebruiken met een join per niveau van ondergeschikten. Als de graad van de grafiek echter potentieel hoog is, wordt het op een gegeven moment onpraktisch om een ​​query met veel joins te schrijven. Een andere optie is om imperatieve iteratieve code te gebruiken, maar dergelijke oplossingen zijn vaak lang. Dit is waar recursieve CTE's echt uitblinken:ze stellen u in staat om declaratieve, beknopte en intuïtieve oplossingen te gebruiken.

Ik begin met de basis van recursieve CTE's voordat ik bij de interessantere dingen kom waar ik zoek- en fietsmogelijkheden behandel.

Onthoud dat de syntaxis van een zoekopdracht tegen een CTE als volgt is:

WITH [ ( ) ]
AS
(

)
;

Hier laat ik één CTE zien die is gedefinieerd met behulp van de WITH-component, maar zoals u weet, kunt u meerdere CTE's definiëren, gescheiden door komma's.

In reguliere, niet-recursieve CTE's is de innerlijke tabelexpressie gebaseerd op een query die slechts één keer wordt geëvalueerd. In recursieve CTE's is de expressie van de binnenste tabel gebaseerd op doorgaans twee query's die bekend staan ​​als leden , hoewel u er meer dan twee kunt hebben om aan sommige gespecialiseerde behoeften te voldoen. De leden worden doorgaans gescheiden door een UNION ALL-operator om aan te geven dat hun resultaten uniform zijn.

Een lid kan een ankerlid zijn —wat betekent dat het slechts één keer wordt geëvalueerd; of het kan een recursief lid zijn -wat betekent dat het herhaaldelijk wordt geëvalueerd, totdat het een lege resultatenset retourneert. Dit is de syntaxis van een zoekopdracht tegen een recursieve CTE die is gebaseerd op één ankerlid en één recursief lid:

WITH [ ( ) ]
AS
(

UNION ALL

)
;

Wat een lid recursief maakt, is een verwijzing naar de CTE-naam. Deze referentie vertegenwoordigt de resultatenset van de laatste uitvoering. Bij de eerste uitvoering van het recursieve lid vertegenwoordigt de verwijzing naar de CTE-naam de resultaatset van het ankerlid. In de N-de uitvoering, waarbij N> 1, staat de verwijzing naar de CTE-naam voor de resultaatset van de (N – 1) uitvoering van het recursieve lid. Zoals gezegd, zodra het recursieve lid een lege set retourneert, wordt het niet opnieuw uitgevoerd. Een verwijzing naar de CTE-naam in de buitenste query vertegenwoordigt de uniforme resultaatsets van de enkele uitvoering van het ankerlid en alle uitvoeringen van het recursieve lid.

De volgende code implementeert de bovengenoemde taak van het retourneren van de substructuur van werknemers voor een bepaalde invoermanager, waarbij werknemer-ID 3 wordt doorgegeven als de root-werknemer in dit voorbeeld:

DECLARE @root AS INT = 3;
 
WITH C AS
(
  SELECT empid, mgrid, empname
  FROM dbo.Employees
  WHERE empid = @root
 
  UNION ALL
 
  SELECT S.empid, S.mgrid, S.empname
  FROM C AS M
    INNER JOIN dbo.Employees AS S
      ON S.mgrid = M.empid
)
SELECT empid, mgrid, empname
FROM C;

De ankerquery wordt slechts één keer uitgevoerd, waarbij de rij wordt geretourneerd voor de invoer root-werknemer (werknemer 3 in ons geval).

De recursieve query voegt zich bij de groep werknemers van het vorige niveau (weergegeven door de verwijzing naar de CTE-naam, alias M voor managers) met hun directe ondergeschikten uit de tabel Werknemers, alias S voor ondergeschikten. Het join-predikaat is S.mgrid =M.empid, wat betekent dat de mgrid-waarde van de ondergeschikte gelijk is aan de empid-waarde van de manager. De recursieve query wordt als volgt vier keer uitgevoerd:

  1. Retour ondergeschikten van werknemer 3; output:werknemer 7
  2. Retour ondergeschikten van medewerker 7; output:medewerkers 9 en 11
  3. Retour ondergeschikten van medewerkers 9 en 11; uitvoer:12, 13 en 14
  4. Retour ondergeschikten van werknemers 12, 13 en 14; uitgang:lege set; recursie stopt

De verwijzing naar de CTE-naam in de buitenste query vertegenwoordigt de uniforme resultaten van de enkele uitvoering van de ankerquery (werknemer 3) met de resultaten van alle uitvoeringen van de recursieve query (werknemers 7, 9, 11, 12, 13 en 14). Deze code genereert de volgende uitvoer:

empid  mgrid  empname
------ ------ --------
3      1      Ina
7      3      Aaron
9      7      Rita
11     7      Gabriel
12     9      Emilia
13     9      Michael
14     9      Didi

Een veelvoorkomend probleem bij programmeeroplossingen is op hol geslagen code zoals oneindige lussen die doorgaans worden veroorzaakt door een bug. Met recursieve CTE's kan een bug leiden tot een runway-uitvoering van het recursieve lid. Stel bijvoorbeeld dat u in onze oplossing voor het retourneren van de ondergeschikten van een invoermedewerker een fout had in het lidmaatschapspredikaat van het recursieve lid. In plaats van ON S.mgrid =M.empid te gebruiken, gebruikte je ON S.mgrid =S.mgrid, zoals:

DECLARE @root AS INT = 3;
 
WITH C AS
(
  SELECT empid, mgrid, empname
  FROM dbo.Employees
  WHERE empid = @root
 
  UNION ALL
 
  SELECT S.empid, S.mgrid, S.empname
  FROM C AS M
    INNER JOIN dbo.Employees AS S
      ON S.mgrid = S.mgrid
)
SELECT empid, mgrid, empname
FROM C;

Gegeven ten minste één werknemer met een niet-null manager-ID in de tabel, zal de uitvoering van het recursieve lid een niet-leeg resultaat blijven opleveren. Onthoud dat de impliciete beëindigingsvoorwaarde voor het recursieve lid is wanneer de uitvoering ervan een lege resultaatset retourneert. Als het een niet-lege resultaatset retourneert, voert SQL Server het opnieuw uit. Je realiseert je dat als SQL Server geen mechanisme had om de recursieve uitvoeringen te beperken, het het recursieve lid gewoon voor altijd zou blijven uitvoeren. Als veiligheidsmaatregel ondersteunt T-SQL een MAXRECURSION-queryoptie die het maximaal toegestane aantal uitvoeringen van het recursieve lid beperkt. Deze limiet is standaard ingesteld op 100, maar u kunt deze wijzigen in elke niet-negatieve SMALLINT-waarde, waarbij 0 staat voor geen limiet. Het instellen van de maximale recursielimiet op een positieve waarde N betekent dat de N + 1 poging tot uitvoering van het recursieve lid wordt afgebroken met een fout. Als u bijvoorbeeld de bovenstaande buggy-code ongewijzigd uitvoert, betekent dit dat u vertrouwt op de standaard maximale recursielimiet van 100, dus de 101-uitvoering van het recursieve lid mislukt:

empid  mgrid  empname
------ ------ --------
3      1      Ina
2      1      Eitan
3      1      Ina
...
Msg 530, Level 16, State 1, Line 121
De instructie is beëindigd. De maximale recursie 100 is uitgeput voordat de verklaring is voltooid.

Stel dat het in uw organisatie veilig is om aan te nemen dat de werknemershiërarchie niet hoger zal zijn dan 6 managementniveaus. U kunt de maximale recursielimiet verlagen van 100 naar een veel kleiner aantal, zoals 10 voor de zekerheid, zoals zo:

DECLARE @root AS INT = 3;
 
WITH C AS
(
  SELECT empid, mgrid, empname
  FROM dbo.Employees
  WHERE empid = @root
 
  UNION ALL
 
  SELECT S.empid, S.mgrid, S.empname
  FROM C AS M
    INNER JOIN dbo.Employees AS S
      ON S.mgrid = S.mgrid
)
SELECT empid, mgrid, empname
FROM C
OPTION (MAXRECURSION 10);

Als er nu een bug is die resulteert in een runaway-uitvoering van het recursieve lid, wordt deze ontdekt bij de 11 poging tot uitvoering van het recursieve lid in plaats van bij de 101-uitvoering:

empid  mgrid  empname
------ ------ --------
3      1      Ina
2      1      Eitan
3      1      Ina
...
Msg 530, Level 16, State 1, Line 149
De instructie is beëindigd. De maximale recursie 10 is uitgeput voordat de verklaring is voltooid.

Het is belangrijk op te merken dat de maximale recursielimiet voornamelijk moet worden gebruikt als een veiligheidsmaatregel om de uitvoering van buggy-runaway-code af te breken. Onthoud dat wanneer de limiet wordt overschreden, de uitvoering van de code wordt afgebroken met een fout. Gebruik deze optie niet om het aantal te bezoeken niveaus te beperken voor filterdoeleinden. Je wilt niet dat er een fout wordt gegenereerd als er geen probleem is met de code.

Voor filterdoeleinden kunt u het aantal niveaus beperken dat u programmatisch kunt bezoeken. U definieert een kolom met de naam lvl die het niveaunummer van de huidige werknemer bijhoudt in vergelijking met de ingevoerde rootwerknemer. In de ankerquery stelt u de lvl-kolom in op de constante 0. In de recursieve query stelt u deze in op de lvl-waarde van de manager plus 1. Om zoveel niveaus onder de invoerroot als @maxlevel te filteren, voegt u het predikaat M.lvl <@ toe maxlevel toe aan de ON-clausule van de recursieve query. De volgende code retourneert bijvoorbeeld werknemer 3 en twee niveaus van ondergeschikten van werknemer 3:

DECLARE @root AS INT = 3, @maxlevel AS INT = 2;
 
WITH C AS
(
  SELECT empid, mgrid, empname, 0 AS lvl
  FROM dbo.Employees
  WHERE empid = @root
 
  UNION ALL
 
  SELECT S.empid, S.mgrid, S.empname, M.lvl + 1 AS lvl
  FROM C AS M
    INNER JOIN dbo.Employees AS S
      ON S.mgrid = M.empid
      AND M.lvl < @maxlevel
)
SELECT empid, mgrid, empname, lvl
FROM C;

Deze query genereert de volgende uitvoer:

empid  mgrid  empname  lvl
------ ------ -------- ----
3      1      Ina      0
7      3      Aaron    1
9      7      Rita     2
11     7      Gabriel  2

Standaard SEARCH- en CYCLE-clausules

De ISO/IEC SQL-standaard definieert twee zeer krachtige opties voor recursieve CTE's. Een daarvan is een clausule genaamd SEARCH die de recursieve zoekvolgorde bestuurt, en een andere is een clausule genaamd CYCLE die cycli in de afgelegde paden identificeert. Dit is de standaardsyntaxis voor de twee clausules:

7.18

Functie

Specificeer het genereren van informatie over volgorde en cyclusdetectie in het resultaat van recursieve query-expressies.

Formaat

::=
[ ]
AS

[ ]
::=
| |
::=
ZOEKEN SET
::=
DIEPTE EERST DOOR | BREADTH FIRST BY
::=
::=
CYCLE SET TO
STANDAARD GEBRUIK
::= [ { }… ]
::=
::=
::=
::=
::=

Op het moment van schrijven ondersteunt T-SQL deze opties niet, maar in de volgende links kunt u verzoeken voor functieverbetering vinden waarin wordt gevraagd om hun opname in T-SQL in de toekomst, en hierop stemmen:

  • Ondersteuning voor ISO/IEC SQL SEARCH-clausule toevoegen aan recursieve CTE's in T-SQL
  • Ondersteuning voor ISO/IEC SQL CYCLE-clausule toevoegen aan recursieve CTE's in T-SQL

In de volgende paragrafen beschrijf ik de twee standaardopties en geef ik ook logisch equivalente alternatieve oplossingen die momenteel beschikbaar zijn in T-SQL.

SEARCH-clausule

De standaard SEARCH-clausule definieert de recursieve zoekvolgorde als BREADTH FIRST of DEPTH FIRST door enkele gespecificeerde volgordekolom(men), en creëert een nieuwe reekskolom met de ordinale posities van de bezochte knooppunten. U specificeert BREADTH FIRST om eerst over elk niveau (breedte) en vervolgens naar beneden (diepte) te zoeken. U geeft DEPTH FIRST op om eerst naar beneden (diepte) en vervolgens over (breedte) te zoeken.

U specificeert de SEARCH-component vlak voor de buitenste zoekopdracht.

Ik zal beginnen met een voorbeeld voor de eerste recursieve zoekvolgorde in de breedte. Houd er rekening mee dat deze functie niet beschikbaar is in T-SQL, dus u kunt de voorbeelden die de standaardclausules in SQL Server of Azure SQL Database gebruiken niet daadwerkelijk uitvoeren. De volgende code retourneert de substructuur van werknemer 1, die vraagt ​​om een ​​breedte eerste zoekvolgorde op empid, waardoor een reekskolom wordt gemaakt met de naam seqbreadth met de ordinale posities van de bezochte knooppunten:

DECLARE @root AS INT = 1;
 
WITH C AS
(
  SELECT empid, mgrid, empname
  FROM dbo.Employees
  WHERE empid = @root
 
  UNION ALL
 
  SELECT S.empid, S.mgrid, S.empname
  FROM C AS M
    INNER JOIN dbo.Employees AS S
      ON S.mgrid = M.empid
)
 
SEARCH BREADTH FIRST BY empid SET seqbreadth
--------------------------------------------
 
SELECT empid, mgrid, empname, seqbreadth
FROM C
ORDER BY seqbreadth;

Hier is de gewenste uitvoer van deze code:

empid  mgrid  empname  seqbreadth
------ ------ -------- -----------
1      NULL   David    1
2      1      Eitan    2
3      1      Ina      3
4      2      Seraph   4
5      2      Jiru     5
6      2      Steve    6
7      3      Aaron    7
8      5      Lilach   8
9      7      Rita     9
10     5      Sean     10
11     7      Gabriel  11
12     9      Emilia   12
13     9      Michael  13
14     9      Didi     14

Raak niet in de war door het feit dat de seqbreadth-waarden identiek lijken te zijn aan de empid-waarden. Dat is puur toeval als gevolg van de manier waarop ik de empid-waarden handmatig heb toegewezen in de voorbeeldgegevens die ik heb gemaakt.

Afbeelding 2 heeft een grafische weergave van de werknemershiërarchie, met een stippellijn die de breedte van de eerste volgorde aangeeft waarin de knooppunten zijn doorzocht. De empid-waarden verschijnen direct onder de namen van de werknemers en de toegewezen seqbreadth-waarden verschijnen in de linkerbenedenhoek van elk vak.

Figuur 2:Zoek eerst in de breedte

Wat hier interessant is om op te merken, is dat binnen elk niveau de knooppunten worden doorzocht op basis van de opgegeven kolomvolgorde (duidelijk in ons geval), ongeacht de bovenliggende associatie van het knooppunt. Merk bijvoorbeeld op dat in het vierde niveau Lilach als eerste wordt benaderd, Rita als tweede, Sean als derde en Gabriel als laatste. Dat komt omdat van alle medewerkers op het vierde niveau dat hun volgorde is op basis van hun empirische waarden. Het is niet zo dat Lilach en Sean achter elkaar worden gefouilleerd omdat ze directe ondergeschikten zijn van dezelfde manager, Jiru.

Het is vrij eenvoudig om een ​​T-SQL-oplossing te bedenken die het logische equivalent biedt van de standaard SAERCH DEPTH FIRST-optie. Je maakt een kolom met de naam lvl zoals ik eerder heb laten zien om het niveau van het knooppunt te volgen met betrekking tot de wortel (0 voor het eerste niveau, 1 voor het tweede, enzovoort). Vervolgens berekent u de gewenste resultatenreekskolom in de buitenste query met behulp van een ROW_NUMBER-functie. Als venster bestellen geeft u eerst lvl op, en vervolgens de gewenste bestelkolom (duidelijk in ons geval). Hier is de code van de volledige oplossing:

DECLARE @root AS INT = 1;
 
WITH C AS
(
  SELECT empid, mgrid, empname, 0 AS lvl
  FROM dbo.Employees
  WHERE empid = @root
 
  UNION ALL
 
  SELECT S.empid, S.mgrid, S.empname, M.lvl + 1 AS lvl
  FROM C AS M
    INNER JOIN dbo.Employees AS S
      ON S.mgrid = M.empid
)
SELECT empid, mgrid, empname,
  ROW_NUMBER() OVER(ORDER BY lvl, empid) AS seqbreadth
FROM C
ORDER BY seqbreadth;

Laten we vervolgens de DEPTH FIRST-zoekvolgorde bekijken. Volgens de standaard moet de volgende code de substructuur van werknemer 1 retourneren, waarbij wordt gevraagd om een ​​eerste zoekvolgorde voor diepte door empid, waarbij een reekskolom wordt gemaakt met de naam seqdepth met de ordinale posities van de gezochte knooppunten:

DECLARE @root AS INT = 1;
 
WITH C AS
(
  SELECT empid, mgrid, empname
  FROM dbo.Employees
  WHERE empid = @root
 
  UNION ALL
 
  SELECT S.empid, S.mgrid, S.empname
  FROM C AS M
    INNER JOIN dbo.Employees AS S
      ON S.mgrid = M.empid
)
 
SEARCH DEPTH FIRST BY empid SET seqdepth
----------------------------------------
 
SELECT empid, mgrid, empname, seqdepth
FROM C
ORDER BY seqdepth;

Hieronder volgt de gewenste uitvoer van deze code:

empid  mgrid  empname  seqdepth
------ ------ -------- ---------
1      NULL   David    1
2      1      Eitan    2
4      2      Seraph   3
5      2      Jiru     4
8      5      Lilach   5
10     5      Sean     6
6      2      Steve    7
3      1      Ina      8
7      3      Aaron    9
9      7      Rita     10
12     9      Emilia   11
13     9      Michael  12
14     9      Didi     13
11     7      Gabriel  14

Als we naar de gewenste query-uitvoer kijken, is het waarschijnlijk moeilijk om erachter te komen waarom de reekswaarden zijn toegewezen zoals ze waren. Figuur 3 zou moeten helpen. Het heeft een grafische weergave van de werknemershiërarchie, met een stippellijn die de diepte van de eerste zoekvolgorde weergeeft, met de toegewezen reekswaarden in de linkerbenedenhoek van elk vak.

Figuur 3:Zoekdiepte eerst

Het bedenken van een T-SQL-oplossing die het logische equivalent biedt van de standaard SEARCH DEPTH FIRST-optie is een beetje lastig, maar toch uitvoerbaar. Ik zal de oplossing in twee stappen beschrijven.

In stap 1 berekent u voor elk knooppunt een binair sorteerpad dat is gemaakt van een segment per voorouder van het knooppunt, waarbij elk segment de sorteerpositie van de voorouder onder zijn broers en zussen weergeeft. Je bouwt dit pad op dezelfde manier als de manier waarop je de lvl-kolom berekent. Dat wil zeggen, begin met een lege binaire tekenreeks voor het hoofdknooppunt in de ankerquery. Vervolgens voegt u in de recursieve query het pad van de ouder samen met de sorteerpositie van het knooppunt tussen broers en zussen nadat u het naar een binair segment hebt geconverteerd. U gebruikt de ROW_NUMBER-functie om de positie te berekenen, gepartitioneerd door de ouder (mgrid in ons geval) en geordend op de relevante bestelkolom (empid in ons geval). U converteert het rijnummer naar een binaire waarde van voldoende grootte, afhankelijk van het maximale aantal directe kinderen dat een ouder in uw grafiek kan hebben. BINARY(1) ondersteunt tot 255 kinderen per ouder, BINARY(2) ondersteunt 65.535, BINARY(3):16.777.215, BINARY(4):4.294.967.295. In een recursieve CTE moeten de corresponderende kolommen in de anker- en recursieve zoekopdrachten van hetzelfde type en grootte zijn , zorg er dus voor dat u het sorteerpad in beide converteert naar een binaire waarde van dezelfde grootte.

Hier is de code die stap 1 in onze oplossing implementeert (ervan uitgaande dat BINARY(1) voldoende is per segment, wat betekent dat u niet meer dan 255 directe ondergeschikten per manager heeft):

DECLARE @root AS INT = 1;
 
WITH C AS
(
  SELECT empid, mgrid, empname,
    CAST(0x AS VARBINARY(900)) AS sortpath
  FROM dbo.Employees
  WHERE empid = @root
 
  UNION ALL
 
  SELECT S.empid, S.mgrid, S.empname,
     CAST(M.sortpath
            + CAST(ROW_NUMBER() OVER(PARTITION BY S.mgrid ORDER BY S.empid)
                AS BINARY(1))
       AS VARBINARY(900)) AS sortpath
  FROM C AS M
    INNER JOIN dbo.Employees AS S
      ON S.mgrid = M.empid
)
SELECT empid, mgrid, empname, sortpath
FROM C;

Deze code genereert de volgende uitvoer:

empid  mgrid  empname  sortpath
------ ------ -------- -----------
1      NULL   David    0x
2      1      Eitan    0x01
3      1      Ina      0x02
7      3      Aaron    0x0201
9      7      Rita     0x020101
11     7      Gabriel  0x020102
12     9      Emilia   0x02010101
13     9      Michael  0x02010102
14     9      Didi     0x02010103
4      2      Seraph   0x0101
5      2      Jiru     0x0102
6      2      Steve    0x0103
8      5      Lilach   0x010201
10     5      Sean     0x010202

Merk op dat ik een lege binaire tekenreeks heb gebruikt als het sorteerpad voor het hoofdknooppunt van de enkele substructuur die we hier opvragen. Als het ankerlid mogelijk meerdere wortels van de substructuur kan retourneren, begin dan gewoon met een segment dat is gebaseerd op een ROW_NUMBER-berekening in de ankerquery, vergelijkbaar met de manier waarop u deze berekent in de recursieve query. In dat geval is uw sorteerpad één segment langer.

Wat je in stap 2 nog moet doen, is het creëren van de gewenste resultaat seq depth-kolom door rijnummers te berekenen die zijn geordend op sortpath in de buitenste query, zoals zo

DECLARE @root AS INT = 1;
 
WITH C AS
(
  SELECT empid, mgrid, empname,
    CAST(0x AS VARBINARY(900)) AS sortpath
  FROM dbo.Employees
  WHERE empid = @root
 
  UNION ALL
 
  SELECT S.empid, S.mgrid, S.empname,
     CAST(M.sortpath
            + CAST(ROW_NUMBER() OVER(PARTITION BY S.mgrid ORDER BY S.empid)
                AS BINARY(1))
       AS VARBINARY(900)) AS sortpath
  FROM C AS M
    INNER JOIN dbo.Employees AS S
      ON S.mgrid = M.empid
)
SELECT empid, mgrid, empname,
  ROW_NUMBER() OVER(ORDER BY sortpath) AS seqdepth
FROM C
ORDER BY seqdepth;

Deze oplossing is het logische equivalent van het gebruik van de standaard SEARCH DIEPTH FIRST-optie die eerder is weergegeven, samen met de gewenste uitvoer.

Als je eenmaal een sequentiekolom hebt die de eerste zoekvolgorde van diepte weergeeft (seqdepth in ons geval), kun je met een klein beetje meer moeite een heel mooie visuele weergave van de hiërarchie genereren. Je hebt ook de bovengenoemde lvl-kolom nodig. U bestelt de buitenste query op de reekskolom. Je retourneert een attribuut dat je wilt gebruiken om het knooppunt weer te geven (zeg, empname in ons geval), voorafgegaan door een tekenreeks (zeg ' | ') die lvl keer is gerepliceerd. Hier is de volledige code om zo'n visuele afbeelding te bereiken:

DECLARE @root AS INT = 1;
 
WITH C AS
(
  SELECT empid, empname, 0 AS lvl,
    CAST(0x AS VARBINARY(900)) AS sortpath
  FROM dbo.Employees
  WHERE empid = @root
 
  UNION ALL
 
  SELECT S.empid, S.empname, M.lvl + 1 AS lvl,
     CAST(M.sortpath
            + CAST(ROW_NUMBER() OVER(PARTITION BY S.mgrid ORDER BY S.empid)
                AS BINARY(1))
       AS VARBINARY(900)) AS sortpath
  FROM C AS M
    INNER JOIN dbo.Employees AS S
      ON S.mgrid = M.empid
)
SELECT empid,
  ROW_NUMBER() OVER(ORDER BY sortpath) AS seqdepth,
  REPLICATE(' | ', lvl) + empname AS emp
FROM C
ORDER BY seqdepth;

Deze code genereert de volgende uitvoer:

empid  seqdepth  emp
------ --------- --------------------
1      1         David
2      2          | Eitan
4      3          |  | Seraph
5      4          |  | Jiru
8      5          |  |  | Lilach
10     6          |  |  | Sean
6      7          |  | Steve
3      8          | Ina
7      9          |  | Aaron
9      10         |  |  | Rita
12     11         |  |  |  | Emilia
13     12         |  |  |  | Michael
14     13         |  |  |  | Didi
11     14         |  |  | Gabriel

Dat is best gaaf!

CYCLE-clausule

Grafiekstructuren kunnen cycli hebben. Die cycli kunnen geldig of ongeldig zijn. Een voorbeeld van geldige cycli is een grafiek die snelwegen en wegen weergeeft die steden en dorpen met elkaar verbinden. Dat is wat een cyclische grafiek wordt genoemd . Omgekeerd mag een grafiek die een werknemershiërarchie weergeeft, zoals in ons geval, geen cycli hebben. Dat is wat een acyclische wordt genoemd grafiek. Stel echter dat u door een foutieve update onbedoeld een cyclus krijgt. Stel bijvoorbeeld dat u per ongeluk de manager-ID van de CEO (werknemer 1) bijwerkt naar werknemer 14 door de volgende code uit te voeren:

UPDATE Employees
  SET mgrid = 14
WHERE empid = 1;

U heeft nu een ongeldige cyclus in de werknemershiërarchie.

Of cycli nu geldig of ongeldig zijn, wanneer u de grafiekstructuur doorloopt, wilt u zeker niet herhaaldelijk een cyclisch pad volgen. In beide gevallen wil je stoppen met het volgen van een pad zodra een niet-eerste exemplaar van hetzelfde knooppunt is gevonden, en een dergelijk pad markeren als cyclisch.

Dus wat gebeurt er als je een grafiek met cycli opvraagt ​​met een recursieve zoekopdracht, zonder dat er een mechanisme is om die te detecteren? Dit is afhankelijk van de uitvoering. In SQL Server bereikt u op een gegeven moment de maximale recursielimiet en wordt uw zoekopdracht afgebroken. Bijvoorbeeld, aangenomen dat u de bovenstaande update hebt uitgevoerd die de cyclus introduceerde, probeer dan de volgende recursieve query uit te voeren om de substructuur van werknemer 1 te identificeren:

DECLARE @root AS INT = 1;
 
WITH C AS
(
  SELECT empid, mgrid, empname
  FROM dbo.Employees
  WHERE empid = @root
 
  UNION ALL
 
  SELECT S.empid, S.mgrid, S.empname
  FROM C AS M
    INNER JOIN dbo.Employees AS S
      ON S.mgrid = M.empid
)
SELECT empid, mgrid, empname
FROM C;

Aangezien deze code geen expliciete maximale recursielimiet instelt, wordt de standaardlimiet van 100 aangenomen. SQL Server breekt de uitvoering van deze code af voordat deze is voltooid en genereert een fout:

empid  mgrid  empname
------ ------ --------
1      14     David
2      1      Eitan
3      1      Ina
7      3      Aaron
9      7      Rita
11     7      Gabriel
12     9      Emilia
13     9      Michael
14     9      Didi
1      14     David
2      1      Eitan
3      1      Ina
7      3      Aaron
...
Msg 530, Level 16, State 1, Line 432
De instructie is beëindigd. De maximale recursie 100 is uitgeput voordat de verklaring is voltooid.

De SQL-standaard definieert een clausule met de naam CYCLE voor recursieve CTE's, waardoor u cycli in uw grafiek kunt verwerken. U geeft deze clausule op vlak voor de buitenste query. Als er ook een SEARCH-component aanwezig is, geeft u deze eerst op en vervolgens de CYCLE-component. Dit is de syntaxis van de CYCLE-clausule:

CYCLE
SET TO STANDAARD
GEBRUIK

De detectie van een cyclus is gebaseerd op de gespecificeerde cycluskolomlijst . In onze werknemershiërarchie zou u bijvoorbeeld waarschijnlijk de empid-kolom voor dit doel willen gebruiken. Wanneer dezelfde empid-waarde voor de tweede keer verschijnt, wordt een cyclus gedetecteerd en vervolgt de query een dergelijk pad niet verder. U specificeert een nieuwe cyclusmarkeringskolom die aangeeft of een cyclus is gevonden of niet, en uw eigen waarden als de cyclusmarkering en niet-cyclusteken waarden. Dit kunnen bijvoorbeeld 1 en 0 of 'Ja' en 'Nee' zijn, of andere waarden naar keuze. In de clausule USING specificeert u een nieuwe matrixkolomnaam die het pad van de tot nu toe gevonden knooppunten zal bevatten.

Hier is hoe je een verzoek om ondergeschikten van een of andere input root-medewerker zou behandelen, met de mogelijkheid om cycli te detecteren op basis van de empid-kolom, waarbij 1 in de cyclusmarkeringskolom wordt aangegeven wanneer een cyclus wordt gedetecteerd, en anders 0:

DECLARE @root AS INT = 1;
 
WITH C AS
(
  SELECT empid, mgrid, empname
  FROM dbo.Employees
  WHERE empid = @root
 
  UNION ALL
 
  SELECT S.empid, S.mgrid, S.empname
  FROM C AS M
    INNER JOIN dbo.Employees AS S
      ON S.mgrid = M.empid
)
 
CYCLE empid SET cycle TO 1 DEFAULT 0
------------------------------------
 
SELECT empid, mgrid, empname
FROM C;

Hier is de gewenste uitvoer van deze code:

empid  mgrid  empname  cycle
------ ------ -------- ------
1      14     David    0
2      1      Eitan    0
3      1      Ina      0
7      3      Aaron    0
9      7      Rita     0
11     7      Gabriel  0
12     9      Emilia   0
13     9      Michael  0
14     9      Didi     0
1      14     David    1
4      2      Seraph   0
5      2      Jiru     0
6      2      Steve    0
8      5      Lilach   0
10     5      Sean     0

T-SQL ondersteunt momenteel de CYCLE-clausule niet, maar er is een tijdelijke oplossing die een logisch equivalent biedt. Het omvat drie stappen. Bedenk dat u eerder een binair sorteerpad hebt gebouwd om de recursieve zoekvolgorde af te handelen. Op een vergelijkbare manier bouwt u als eerste stap in de oplossing een op tekenreeksen gebaseerd voorouderpad gemaakt van de knooppunt-ID's (werknemer-ID's in ons geval) van de voorouders die naar het huidige knooppunt leiden, inclusief het huidige knooppunt. U wilt scheidingstekens tussen de knooppunt-ID's, inclusief één aan het begin en één aan het einde.

Hier is de code om zo'n voorouderpad te bouwen:

DECLARE @root AS INT = 1;
 
WITH C AS
(
  SELECT empid, mgrid, empname,
    CAST('.' + CAST(empid AS VARCHAR(4)) + '.' AS VARCHAR(900)) AS ancpath
  FROM dbo.Employees
  WHERE empid = @root
 
  UNION ALL
 
  SELECT S.empid, S.mgrid, S.empname,
    CAST(M.ancpath + CAST(S.empid AS VARCHAR(4)) + '.' AS VARCHAR(900)) AS ancpath
  FROM C AS M
    INNER JOIN dbo.Employees AS S
      ON S.mgrid = M.empid
)
SELECT empid, mgrid, empname, ancpath
FROM C;

Deze code genereert de volgende uitvoer, waarbij nog steeds cyclische paden worden gevolgd en daarom wordt afgebroken voordat deze is voltooid zodra de maximale recursielimiet is overschreden:

empid  mgrid  empname  ancpath
------ ------ -------- -------------------
1      14     David    .1.
2      1      Eitan    .1.2.
3      1      Ina      .1.3.
7      3      Aaron    .1.3.7.
9      7      Rita     .1.3.7.9.
11     7      Gabriel  .1.3.7.11.
12     9      Emilia   .1.3.7.9.12.
13     9      Michael  .1.3.7.9.13.
14     9      Didi     .1.3.7.9.14.
1      14     David    .1.3.7.9.14.1.
2      1      Eitan    .1.3.7.9.14.1.2.
3      1      Ina      .1.3.7.9.14.1.3.
7      3      Aaron    .1.3.7.9.14.1.3.7.
...
Msg 530, Level 16, State 1, Line 492
De instructie is beëindigd. De maximale recursie 100 is uitgeput voordat de verklaring is voltooid.

Eyeballing the output, you can identify a cyclic path when the current node appears in the path for the nonfirst time, such as in the path '.1.3.7.9.14.1.' .

In the second step of the solution you produce the cycle mark column. In the anchor query you simply set it to 0 since clearly a cycle cannot be found in the first node. In the recursive member you use a CASE expression that checks with a LIKE-based predicate whether the current node ID already appears in the parent’s path, in which case it returns the value 1, or not, in which case it returns the value 0.

Here’s the code implementing Step 2:

DECLARE @root AS INT = 1;
 
WITH C AS
(
  SELECT empid, mgrid, empname,
    CAST('.' + CAST(empid AS VARCHAR(4)) + '.' AS VARCHAR(900)) AS ancpath,
    0 AS cycle
  FROM dbo.Employees
  WHERE empid = @root
 
  UNION ALL
 
  SELECT S.empid, S.mgrid, S.empname,
    CAST(M.ancpath + CAST(S.empid AS VARCHAR(4)) + '.' AS VARCHAR(900)) AS ancpath,
    CASE WHEN M.ancpath LIKE '%.' + CAST(S.empid AS VARCHAR(4)) + '.%' THEN 1
      ELSE 0 END AS cycle
  FROM C AS M
    INNER JOIN dbo.Employees AS S
      ON S.mgrid = M.empid
)
SELECT empid, mgrid, empname, ancpath, cycle
FROM C;

This code generates the following output, again, still pursuing cyclic paths and failing once the max recursion limit is reached:

empid  mgrid  empname  ancpath             cycle
------ ------ -------- ------------------- ------
1      14     David    .1.                 0
2      1      Eitan    .1.2.               0
3      1      Ina      .1.3.               0
7      3      Aaron    .1.3.7.             0
9      7      Rita     .1.3.7.9.           0
11     7      Gabriel  .1.3.7.11.          0
12     9      Emilia   .1.3.7.9.12.        0
13     9      Michael  .1.3.7.9.13.        0
14     9      Didi     .1.3.7.9.14.        0
1      14     David    .1.3.7.9.14.1.      1
2      1      Eitan    .1.3.7.9.14.1.2.    0
3      1      Ina      .1.3.7.9.14.1.3.    1
7      3      Aaron    .1.3.7.9.14.1.3.7.  1
...
Msg 530, Level 16, State 1, Line 532
The statement terminated. The maximum recursion 100 has been exhausted before statement completion.

The third and last step is to add a predicate to the ON clause of the recursive member that pursues a path only if a cycle was not detected in the parent’s path.

Here’s the code implementing Step 3, which is also the complete solution:

DECLARE @root AS INT = 1;
 
WITH C AS
(
  SELECT empid, mgrid, empname,
    CAST('.' + CAST(empid AS VARCHAR(4)) + '.' AS VARCHAR(900)) AS ancpath,
    0 AS cycle
  FROM dbo.Employees
  WHERE empid = @root
 
  UNION ALL
 
  SELECT S.empid, S.mgrid, S.empname,
    CAST(M.ancpath + CAST(S.empid AS VARCHAR(4)) + '.' AS VARCHAR(900)) AS ancpath,
    CASE WHEN M.ancpath LIKE '%.' + CAST(S.empid AS VARCHAR(4)) + '.%' THEN 1
      ELSE 0 END AS cycle
  FROM C AS M
    INNER JOIN dbo.Employees AS S
      ON S.mgrid = M.empid
      AND M.cycle = 0
)
SELECT empid, mgrid, empname, cycle
FROM C;

This code runs to completion, and generates the following output:

empid  mgrid  empname  cycle
------ ------ -------- -----------
1      14     David    0
2      1      Eitan    0
3      1      Ina      0
7      3      Aaron    0
9      7      Rita     0
11     7      Gabriel  0
12     9      Emilia   0
13     9      Michael  0
14     9      Didi     0
1      14     David    1
4      2      Seraph   0
5      2      Jiru     0
6      2      Steve    0
8      5      Lilach   0
10     5      Sean     0

You identify the erroneous data easily here, and fix the problem by setting the manager ID of the CEO to NULL, like so:

UPDATE Employees
  SET mgrid = NULL
WHERE empid = 1;

Run the recursive query and you will find that there are no cycles present.

Conclusie

Recursive CTEs enable you to write concise declarative solutions for purposes such as traversing graph structures. A recursive CTE has at least one anchor member, which gets executed once, and at least one recursive member, which gets executed repeatedly until it returns an empty result set. Using a query option called MAXRECURSION you can control the maximum number of allowed executions of the recursive member as a safety measure. To limit the executions of the recursive member programmatically for filtering purposes you can maintain a column that tracks the level of the current node with respect to the root node.

The SQL standard supports two very powerful options for recursive CTEs that are currently not available in T-SQL. One is a clause called SEARCH that controls the recursive search order, and another is a clause called CYCLE that detects cycles in the traversed paths. I provided logically equivalent solutions that are currently available in T-SQL. If you find that the unavailable standard features could be important future additions to T-SQL, make sure to vote for them here:

  • Add support for ISO/IEC SQL SEARCH clause to recursive CTEs in T-SQL
  • Add support for ISO/IEC SQL CYCLE clause to recursive CTEs in T-SQL

This article concludes the coverage of the logical aspects of CTEs. Next month I’ll turn my attention to physical/performance considerations of CTEs.


  1. Postgres-datumnotaties en hun verschillende functies verkennen

  2. Postgresql-kolom niet gevonden, maar wordt weergegeven in Beschrijven

  3. Hoe parallelle plannen opstarten - deel 1

  4. Netherlands Access Developer Day 2019 – 14 september