Leonel Morales Díaz Ingeniería Simple leonel@ingenieriasimple.com Tablas y Funciones de SQL Server para Implementar una Jerarquía sin Límite de Niveles Leonel Morales Díaz Ingeniería Simple leonel@ingenieriasimple.com Copyright 2008 by Leonel Morales Díaz – Ingeniería Simple. Derechos reservados Disponible en: http://www.ingenieriasimple.com/TSQL
Jerarquías usuales Estructura tabla padre – tabla hijo La llave primaria de la padre es llave foránea en la hija Puede haber una tabla nieto Hasta una biznieto Padres Código Descripción Hijos Código CódigoPadre Descripción Nietos Código CódigoHijoPadre Descripción
Problemas en jerarquías Limitada a tres niveles O a la cantidad de niveles establecida Inflexibilidad: Nuevos niveles reales deben ser “adaptados” La estructura es permanente y coercitiva Aunque un nivel ya no sea necesario Dificultad de consultas Se trata de relacionar tres o más tablas
Jerarquías de una sola tabla Estructura registro hijo – registro padre La llave primaria es llave foránea de la misma tabla Si no hay padre la llave foránea es nula Tabla con relación a sí misma Datos Código CódigoPadre Descripción
Ventajas jerarquía unitabla Ilimitados niveles Se ajusta a las necesidades reales Consultas más sencillas Estructura más simple Llega a conocerse muy bien Bastante flexible
Desventajas El nivel del registro no se conoce inmediatamente En la jerarquía tradicional se conoce el nivel con solo saber a qué tabla pertenece Se necesita agregar campos para esto
Jerarquía contable Usualmente por posiciones en una cadena #.##.###.#### Cuenta, subcuenta, sub-subcuenta, cuenta de detalle, etc. Puede ser una sola tabla Con referencia a sí misma La cantidad de niveles está pre-establecida No hay necesidad de campo con cuenta padre Usualmente se llama Nomenclatura Contable
Ejemplo jerarquía contable En el código está implícito el código del padre Puede ser necesario poner validaciones para evitar que se inserte un código sin padre Los códigos de longitud 1 no tienen padre Una sola tabla Código Cuenta 1 Activo 2 Pasivo 3 Capital 4 Gastos 5 Ingresos 1.1 Circulante 1.2 Fijo 1.3 Diferido 2.1 Circulante 2.2 Fijo 2.3 Diferido 3.1 Acciones al portador 3.2 Acciones preferentes 4.1 Fijos 4.2 Variables 5.1 Fijos 5.2 Variables 1.1.1 Caja 1.1.2 Bancos
Nomenclatura Implementación Puede hacerse mediante “constraints” de tipo “Check” Y una función para encontrar el código padre Si la función devuelve “Null” no se acepta CREATE TABLE Nomenclatura( Código nVarChar(13) NOT NULL CONSTRAINT Código_Nomenclatura Check ( (Código Like '[1-9]' Or Código Like '[1-9].[0-9][0-9]' Or Código Like '[1-9].[0-9][0-9].[0-9][0-9][0-9]' Or Código Like '[1-9].[0-9][0-9].[0-9][0-9][0-9].[0-9][0-9][0-9][0-9]') And (len(Código)=1 Or Not dbo.CuentaPadre(Código) Is Null)), Cuenta nVarChar(30) NULL, Constraint PK_Nomenclatura Primary Key Clustered ( Código ASC ) ) Nomenclatura Código Cuenta
Función para chequeo CuentaPadre(@Código) Encuentra la cuenta padre de @Código Si no hay devuelve “Null” CREATE FUNCTION CuentaPadre ( @Código nVarChar(13) ) RETURNS nVarChar(13) AS BEGIN DECLARE @Resu nVarChar(13) Set @Resu = Null If CharIndex('.',@Código) > 0 Begin Declare @PosiblePadre nVarChar(13) Set @PosiblePadre = RTrim(@Código) While Right(@PosiblePadre,1) <> '.' Set @PosiblePadre = Left(@PosiblePadre,Len(@PosiblePadre)-1) Select @Resu = Código From Nomenclatura Where Código = @PosiblePadre End RETURN @Resu END
Padre y nivel Se pueden implementar con campos calculados Padre Nivel El valor devuelto por CuentaPadre Nivel El número de puntos más 1 Se puede hacer con una función que los cuente o aprovechando la función Like
Tabla con padre y nivel Nomenclatura Código Cuenta Padre Nivel CREATE TABLE Nomenclatura( Código nVarChar(13) NOT NULL CONSTRAINT Código_Nomenclatura Check ( (Código Like '[1-9]' Or Código Like '[1-9].[0-9][0-9]' Or Código Like '[1-9].[0-9][0-9].[0-9][0-9][0-9]' Or Código Like '[1-9].[0-9][0-9].[0-9][0-9][0-9].[0-9][0-9][0-9][0-9]') And (len(Código)=1 Or Not dbo.CuentaPadre(Código) Is Null)), Cuenta nVarChar(30) NULL, Padre As dbo.CuentaPadre(Código), Nivel As Case When Código Like '%.%.%.%' Then 4 When Código Like '%.%.%' Then 3 When Código Like '%.%' Then 2 Else 1 End, Constraint PK_Nomenclatura Primary Key Clustered ( Código ASC ) ) Nomenclatura Código Cuenta Padre Nivel
Datos de tabla Nomenclatura Select * From Nomenclatura Código Cuenta Padre Nivel ------------- ------------------------------ ------------- ----------- 1 Activo NULL 1 1.01 Circulante 1 2 1.01.001 Caja 1.01 3 1.01.002 Bancos 1.01 3 1.01.002.0001 Banco Industrial 1.01.002 4 1.01.002.0002 Banco Continental 1.01.002 4 1.01.002.0003 Banco Internacional 1.01.002 4 1.02 Fijo 1 2 1.03 Diferido 1 2 2 Pasivo NULL 1 2.01 Circulante 2 2 2.02 Fijo 2 2 2.03 Diferido 2 2 3 Capital NULL 1 3.01 Acciones al portador 3 2 3.02 Acciones preferentes 3 2 4 Gastos NULL 1 4.01 Fijos 4 2 4.02 Variables 4 2 5 Ingresos NULL 1 5.01 Fijos 5 2 5.02 Variables 5 2 (22 row(s) affected)
Otros tipos de jerarquía Por ruta o “path” Similar a la de directorios de windows Se tiene un nodo “raíz” y un separador C:, D:, etc., son raíces \ es el separador Todos los nodos de un mismo nivel tienen la misma cantidad de separadores en la ruta Todos los hijos de un mismo nodo comparten el mismo prefijo Generalización: jerarquía por prefijo Puede o no existir separador En cualquier caso se usa solo una tabla
Planteamiento Partiendo de una jerarquía de una sola tabla construir las consultas para: Obtener la lista de padres Registros sin padre Obtener la lista de registros en el nivel “n” Obtener la lista de registros descendientes del registro “R” Obtener la lista de registros que descienden del registro “P” y están en el nivel “m”
Tabla básica Solo tres campos El resto serán calculados Código CódigoPadre Descripción El resto serán calculados Padre Nivel Ruta Datos Código CódigoPadre Descripción
Creación de la tabla básica La tabla permite almacenar cualquier jerarquía En este ejemplo se usará para países y provincias geográficas (departamentos), municipios, etc. CREATE TABLE Datos( Código Int NOT NULL, CódigoPadre Int NULL Constraint FK_Datos_CódigoPadre Foreign Key References Datos ( Código ), Descripción nVarChar(Max), Constraint PK_Datos Primary Key Clustered ( Código ASC ) ) Datos Código CódigoPadre Descripción
Registros para pruebas Select * From Datos Código CódigoPadre Descripción ----------- ----------- ----------------------- 1 NULL Guatemala 2 NULL El Salvador 3 NULL Honduras 4 NULL Nicaragua 5 NULL Costa Rica 6 NULL Belice 7 NULL Panamá 8 1 Guatemala 9 1 Sacatepequez 10 1 Chimaltenango 11 1 Sololá 12 1 Totonicapán 13 1 Huehuetenango 14 1 Quetzaltenango 15 1 San Marcos 16 1 Retalhuleu 17 1 Suchitepequez 18 1 Escuintla 19 1 Santa Rosa 20 1 Jutiapa 21 1 Jalapa 22 1 Zacapa 23 1 Izabal 24 1 Baja Verapaz 25 1 Alta Verapaz 26 1 Quiché 27 1 Petén 28 1 El Progreso 29 8 Ciudad de Guatemala 30 8 Mixco 31 8 Villa Nueva 32 8 Jocotenango 33 9 San Juan Sacatepequez 34 9 San Raymundo 35 9 Antigua Guatemala 36 8 Amatitlán 37 11 Atitlán 38 11 San Pedro La Laguna 39 13 Chiantla 40 39 Los Regadillos 41 13 Huehuetenango 42 41 El Terrero 43 41 El Cambote (43 row(s) affected)
Lista de padres Padres: Registros sin padre CódigoPadre Is Null Select * From Datos Where CódigoPadre Is Null Código CódigoPadre Descripción ----------- ----------- ------------------ 1 NULL Guatemala 2 NULL El Salvador 3 NULL Honduras 4 NULL Nicaragua 5 NULL Costa Rica 6 NULL Belice 7 NULL Panamá (7 row(s) affected)
Lista de registros en nivel “n” Se necesita una función que calcule el nivel Puede ser recursiva Select * From Datos Where dbo.CalculaNivelDato(Código) = 3 Código CódigoPadre Descripción ----------- ----------- --------------------- 29 8 Ciudad de Guatemala 30 8 Mixco 31 8 Villa Nueva 32 8 Jocotenango 33 9 San Juan Sacatepequez 34 9 San Raymundo 35 9 Antigua Guatemala 36 8 Amatitlán 37 11 Atitlán 38 11 San Pedro La Laguna 39 13 Chiantla 41 13 Huehuetenango (12 row(s) affected) Create Function CalculaNivelDato ( @Código As Int ) Returns Int As Begin Declare @CódigoPadre Int Declare @Nivel Int Select @CódigoPadre = CódigoPadre From Datos Where Código = @Código If (@CódigoPadre) Is Null Set @Nivel = 1 Else Set @Nivel = dbo.CalculaNivelDato(@CódigoPadre) + 1 Return @Nivel End
Nivel como campo calculado Se puede incorporar el nivel como campo calculado Usando la función CalculaNivelDato Select * From Datos Where Nivel = 3 or Nivel = 4 Código CódigoPadre Descripción Nivel ----------- ----------- ---------------------- ------ 29 8 Ciudad de Guatemala 3 30 8 Mixco 3 31 8 Villa Nueva 3 32 8 Jocotenango 3 33 9 San Juan Sacatepequez 3 34 9 San Raymundo 3 35 9 Antigua Guatemala 3 36 8 Amatitlán 3 37 11 Atitlán 3 38 11 San Pedro La Laguna 3 39 13 Chiantla 3 40 39 Los Regadillos 4 41 13 Huehuetenango 3 42 41 El Terrero 4 43 41 El Cambote 4 (15 row(s) affected) CREATE TABLE Datos( Código Int NOT NULL, CódigoPadre Int NULL Constraint FK_Datos_CódigoPadre Foreign Key References Datos ( Código ), Descripción nVarChar(Max), Nivel As dbo.CalculaNivelDato(Código), Constraint PK_Datos Primary Key Clustered ( Código ASC ) )
Lista de descendientes de “R” Prerrequisito: Función que construye el “path” hacía la raíz También se puede hacer recursiva Usa delimitadores: “>” antes y “=“ después Para evitar el código 30 se confunda con el 3030 por ejemplo Facilita las búsquedas Ejemplo: path de 30: >1=>8=>30=
Función de ruta Select Código, dbo.ComponePathDato(Código) From Datos Where Nivel = 3 or Nivel = 4 Código ----------- ------------------- 29 >1=>8=>29= 30 >1=>8=>30= 31 >1=>8=>31= 32 >1=>8=>32= 33 >1=>9=>33= 34 >1=>9=>34= 35 >1=>9=>35= 36 >1=>8=>36= 37 >1=>11=>37= 38 >1=>11=>38= 39 >1=>13=>39= 40 >1=>13=>39=>40= 41 >1=>13=>41= 42 >1=>13=>41=>42= 43 >1=>13=>41=>43= (15 row(s) affected) Create Function ComponePathDato ( @Código As Int ) Returns nVarChar(Max) As Begin Declare @CódigoPadre Int Declare @Path nVarChar(Max) Set @Path = '>' + Convert(nVarChar(Max),@Código) + '=' Select @CódigoPadre = CódigoPadre From Datos Where Código = @Código If Not (@CódigoPadre) Is Null Set @Path = dbo.ComponePathDato(@CódigoPadre) + @Path Return @Path End
Ruta como campo calculado Similar al caso de Nivel CREATE TABLE Datos( Código Int NOT NULL, CódigoPadre Int NULL Constraint FK_Datos_CódigoPadre Foreign Key References Datos ( Código ), Descripción nVarChar(Max), Nivel As dbo.CalculaNivelDato(Código), Ruta As dbo.ComponePathDato(Código), Constraint PK_Datos Primary Key Clustered ( Código ASC ) ) Select Código, Nivel, Ruta From Datos Where Nivel = 4 Código Nivel Ruta ----------- ----------- ---------------- 40 4 >1=>13=>39=>40= 42 4 >1=>13=>41=>42= 43 4 >1=>13=>41=>43= (3 row(s) affected)
¡Ahora sí! Descendientes de “R” Descendientes de “R” tienen la ruta de “R” en su ruta Declare @Ruta nVarChar(Max) Select @Ruta = Ruta From Datos Where Código = 13 Select * From Datos Where Ruta Like @Ruta + '%' And Ruta <> @Ruta Código CódigoPadre Descripción Nivel Ruta ----------- ----------- --------------- ----------- ---------------- 39 13 Chiantla 3 >1=>13=>39= 40 39 Los Regadillos 4 >1=>13=>39=>40= 41 13 Huehuetenango 3 >1=>13=>41= 42 41 El Terrero 4 >1=>13=>41=>42= 43 41 El Cambote 4 >1=>13=>41=>43= (5 row(s) affected)
Descendientes de “P” en nivel “m” Igual que el anterior Pero con condición sobre el nivel Declare @Ruta nVarChar(Max) Select @Ruta = Ruta From Datos Where Código = 13 Select * From Datos Where Ruta Like @Ruta + '%' And Ruta <> @Ruta And Nivel = 3 Código CódigoPadre Descripción Nivel Ruta ----------- ----------- ------------- ----------- ------------ 39 13 Chiantla 3 >1=>13=>39= 41 13 Huehuetenango 3 >1=>13=>41= (2 row(s) affected) Select * From Datos Where Ruta Like @Ruta + '%' And Ruta <> @Ruta And Nivel = 4 Código CódigoPadre Descripción Nivel Ruta ----------- ----------- --------------- ----------- ---------------- 40 39 Los Regadillos 4 >1=>13=>39=>40= 42 41 El Terrero 4 >1=>13=>41=>42= 43 41 El Cambote 4 >1=>13=>41=>43= (3 row(s) affected)
Variaciones de las funciones Calcular el nivel a partir de la ruta El nivel es el número de “>” o “=“ en la ruta Transformar las funciones a formas no recursivas Usar vistas para evitar los campos calculados Poner el código en “Identity” generado automáticamente
Formas no recursivas Función de cálculo de nivel Create Function CalculaNivelDato ( @Código As Int ) Returns Int As Begin Declare @CódigoPadre Int Select @CódigoPadre = CódigoPadre From Datos Where Código = @Código Declare @Nivel Int Set @Nivel = 1 While Not @CódigoPadre Is Null Begin Set @Nivel = @Nivel + 1 Select @CódigoPadre = CódigoPadre From Datos Where Código = @CódigoPadre End Return @Nivel
Formas no recursivas Función de composición de rutas Create Function ComponePathDato ( @Código As Int ) Returns nVarChar(Max) As Begin Declare @CódigoPadre Int Declare @Path nVarChar(Max) Select @CódigoPadre = CódigoPadre, @Path = '>' + Convert(nVarChar(Max),Código) + '=' From Datos Where Código = @Código While Not @CódigoPadre Is Null @Path = '>' + Convert(nVarChar(Max),Código) + '=' + @Path From Datos Where Código = @CódigoPadre Return @Path End