Limpiando una KB GeneXus - un ejemplo práctico.
Una empresa amiga me mando una KB, para que diagnosticara algunos problemas de performance bastante serios que estaban teniendo, en el proceso de build de la misma.
Es una KB recién migrada desde GeneXus 9.0 a GeneXus Evolution 3 U8. Me mandaron el MDF (archivo de la base de datos SQL Server) de la KB ya convertida por ellos y con eso me puse a trabajar.
Esta KB me dio la oportunidad de practicar una limpieza a fondo de la misma, en código escrito por otros, que me permite probar el KBDoctor en un entorno diferente al que tenemos en Concepto.
Al empezar la KB tenia:
13.872 objetos. 10.662 atributos y 674 tablas.
Con esos datos iniciales, me puse a trabajar.
Revisar propiedades.
Lo primero que hago cuando trabajo con KB grandes, es revisar algunas propiedades de las mismas.
En este caso lo que hice fue:
Habilite el profiler en mi SQL Server y vi que la causante era una sentencia sql que intenta copiar todos los registros de un modelo (diseño) al de produccion y la problematica era la tabla ModelCrossReference que tiene aproximadamente 19.000.000 registros (tengo 3 modelos con 6.392.000 cada uno aprox) y estaba bastante fragmentada.
Despues de esta primer limpieza quedo asi
13.872 objetos, 10.597 atributos y 674 tablas.
La KB no tenia llamadas dinámicas entre objetos, por lo que es seguro borrar todos los objetos que nadie referencia. La opción del KBDoctor que borra los objetos no referenciados, también inicializa la transacciones que nadie llama, borrando las variables, poniendo el formulario por defecto en WEB y WIN y borra todos los eventos.
Luego de este paso quedo con los siguientes números
13.121 objetos, 10.597 atributos y 674 tablas.
Limpieza de variables
Viendo que alguno de los problemas de performance venían por el lado de las referencias cruzadas entre objetos, me puse a pesar de que forma podía achicarse dichas referencias. Una forma de sacar referencias es borrar las variables no usadas, que hagan referencias a atributos. Para esto, nada mejor que usar la extensión Variables Cleaner de Dvelop.
Demoró muchísimo (mas de 9 horas) y terminó cambiando 9223 objetos y borró mas de 237.000 variables.
Total cleaned objects: 9223. Removed variables: 237885
Variables Cleaning Success
Nunca habia visto una KB que tuviera tantas variables definidas pero no usadas.
Al empezar esta limpieza, la tabla ModelCrossReference tenia mas de 6.300.000 registros para el ModelId = 1 (el modelo de diseño) y al terminar tenia 1.170.746. Eliminó mas de 5 millones de registros!!.
No conozco como se guardan las referencias, pero me parecen demasiados registros. Tal vez en la conversion de versiones viejas pueda quedar algo mal.
Fue la primera vez que me puse a pensar que tener muchas variables definidas en los objetos podia enlentecer el proceso de build all.
En esta KB encontré un folder BORRAR con muchos objetos y desmarque de ella todos los objetos main y volví a correr la opción que borra los objetos no referenciados.
Despues de la limpieza y el build all quedo
modelid registros
----------- -----------
1 1.150.140
3 1.150.140
4 6.391.870
El modelId=4 es una version que creé antes de empezar la limpieza.
El modelId=1 es diseño y el modelId=3 es el modelo en el que estoy trabajando y tiene la misma cantidad de registros que el de diseño pues acabo de hacer una reorganización.
12.938 objetos, 10.597 atributos y 674 tablas.
Para esto utilice el KBCompress de las KBTools, que abre la KB con la opcion de CompressData=true y ademas hace un CheckKnowledgeBase, que revisa si esta todo bien desde el punto de vista interno de la KB.
Demoró 18 minutos y si bien acomodó varios indices no encontro ningun error a corregir.
Decidí entonces ejecutar el KBCompressMDF de las KBTools, que permite achicar los archivos de la base de datos de la KB.
Despues de ejetuar esto, los archivos de la KB ocupaban 1.8Gb los datos y 500Kb el LDF.
De mas de 7Gb a menos de 2Gb, por lo que en la limpieza recuperamos unos 5Gb.
Hay que recordar que cada vez que se ejecuta una compresión de los archivos de la base de datos, se mueven muchas paginas de datos, lo cual puede llevar a tener fragmentación de las tablas. Conviene por eso, volver a correr la Defragmentacion de indices de las tablas de SQL server del paso inicial.
13.872 objetos. 10.662 atributos, 674 tablas, 3.8Gb, 6.391.870 registros en ModelCrossReference
12.938 objetos, 10.597 atributos, 674 tablas, 2.2Gb, 1.150.140 registros en ModelCrossReference
Diferencia
934 objetos, 65 atributos, 0 tabla, 1.6Gb, 5.241.730 registros menos.
Ademas, quedaron marcadas varias transacciones con la propiedad Generate Object=*NO lo cual achica aun mas los tiempos del Build All.
Todo esto, generando exactamente los mismos ejecutables, solo eliminando lo mas grueso.
Faltaría aun hacer otra limpieza mas profunda, donde borremos tablas no usadas, transacciones que no se necesitan, objetos no alcanzables (que se llaman entre si y por eso no se borraron la primer limpieza), atributos que están en las tablas pero no se usan, etc, pero todo eso puede quedar para otro post.
UPDATE:
Me preguntar cual era el error que daba al hacer el build all. Lo pongo aca abajo.
========== Build All started ==========
========== Target Environment update started ==========
error: System.Data.SqlClient.SqlException: Valor de tiempo de espera caducado. El período de tiempo de espera caducó antes de completar la operación o el servidor no responde.
en System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection)
en System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj)
en System.Data.SqlClient.TdsParserStateObject.ReadSniError(TdsParserStateObject stateObj, UInt32 error)
en System.Data.SqlClient.TdsParserStateObject.ReadSni(DbAsyncResult asyncResult, TdsParserStateObject stateObj)
en System.Data.SqlClient.TdsParserStateObject.ReadNetworkPacket()
en System.Data.SqlClient.TdsParserStateObject.ReadBuffer()
en System.Data.SqlClient.TdsParserStateObject.ReadByte()
en System.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
en System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
en System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async)
en System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, DbAsyncResult result)
en System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(DbAsyncResult result, String methodName, Boolean sendToPipe)
en System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
en Deklarit.Data.ReadWriteCommand.ExecuteStmt()
en gxioSQL.ModelCrossReferenceDataReader.DeleteByModelId(Int32 modelId)
en Artech.Udm.Layers.Data.SQL.Mappers.ModelCrossReferenceMapper.DeleteByUdmModel(UdmModelKey udmModel)
en Artech.Udm.Layers.BL.Managers.ModelCrossReferenceManager.DeleteByUdmModel(UdmModelKey udmModel)
en Artech.Udm.Framework.UdmKnowledgeBase.BatchCopyModel(Model source, Model target)
en Artech.Udm.Framework.UdmKnowledgeBase.CopyModel(Model source, Model target, CopyModelOptions options, IEnumerable`1 objectTypes, IEnumerable`1 copyOutputIds)
en Artech.Architecture.Common.Objects.KnowledgeBase.CopyModel(KBModel fromModel, KBModel toModel)
en Artech.Core.UI.Services.KBService.CopyModel(KBModel fromModel, KBModel toModel)
en Artech.Packages.Genexus.UI.Commands.CopyModelCommand.Do()
Target Environment update Failed
Build All Failed
Es una KB recién migrada desde GeneXus 9.0 a GeneXus Evolution 3 U8. Me mandaron el MDF (archivo de la base de datos SQL Server) de la KB ya convertida por ellos y con eso me puse a trabajar.
Esta KB me dio la oportunidad de practicar una limpieza a fondo de la misma, en código escrito por otros, que me permite probar el KBDoctor en un entorno diferente al que tenemos en Concepto.
Al empezar la KB tenia:
13.872 objetos. 10.662 atributos y 674 tablas.
Con esos datos iniciales, me puse a trabajar.
Revisar propiedades.
Lo primero que hago cuando trabajo con KB grandes, es revisar algunas propiedades de las mismas.
En este caso lo que hice fue:
- Deshabilitar el Indexado dentro de la KB (esto enlentence mucho cada vez que se salva un objeto)
- Poner el Generate Developer Menu en *NO
- No generar prompts.
Build all.
Hice un build all con la KB sin haberle cambiado nada y daba el SQL Server daba timeout, al intentar hacer el impacto de la base de datos.Habilite el profiler en mi SQL Server y vi que la causante era una sentencia sql que intenta copiar todos los registros de un modelo (diseño) al de produccion y la problematica era la tabla ModelCrossReference que tiene aproximadamente 19.000.000 registros (tengo 3 modelos con 6.392.000 cada uno aprox) y estaba bastante fragmentada.
Defragmentar las tablas SQLServer.
Para defragmentar tablas yo utilizo los scripts de https://ola.hallengren.com/sql-server-index-and-statistics-maintenance.html, que son faciles de usar y dan muy buen resultado. Luego de usarlo, no volvio a aparecer el problema del timeout al hacer el copymodel.Borrar los atributos que no estan en ninguna tabla.
Es comun que las KB que vienen de 9.0 tengan atributos definidos que se han dejado de usar, y ya no estan en ninguna tabla. Hay una opcion en KBDoctor para borrarlos y cambia (casi) todas las referencias de objetos a dichos atributos y luego los borra.Despues de esta primer limpieza quedo asi
13.872 objetos, 10.597 atributos y 674 tablas.
Borrar objetos no referenciados.
Use la opción de KBDoctor que borra todos los objetos que no son referenciados por nadie.La KB no tenia llamadas dinámicas entre objetos, por lo que es seguro borrar todos los objetos que nadie referencia. La opción del KBDoctor que borra los objetos no referenciados, también inicializa la transacciones que nadie llama, borrando las variables, poniendo el formulario por defecto en WEB y WIN y borra todos los eventos.
Luego de este paso quedo con los siguientes números
13.121 objetos, 10.597 atributos y 674 tablas.
Limpieza de variables
Viendo que alguno de los problemas de performance venían por el lado de las referencias cruzadas entre objetos, me puse a pesar de que forma podía achicarse dichas referencias. Una forma de sacar referencias es borrar las variables no usadas, que hagan referencias a atributos. Para esto, nada mejor que usar la extensión Variables Cleaner de Dvelop.
Demoró muchísimo (mas de 9 horas) y terminó cambiando 9223 objetos y borró mas de 237.000 variables.
Total cleaned objects: 9223. Removed variables: 237885
Variables Cleaning Success
Nunca habia visto una KB que tuviera tantas variables definidas pero no usadas.
Al empezar esta limpieza, la tabla ModelCrossReference tenia mas de 6.300.000 registros para el ModelId = 1 (el modelo de diseño) y al terminar tenia 1.170.746. Eliminó mas de 5 millones de registros!!.
No conozco como se guardan las referencias, pero me parecen demasiados registros. Tal vez en la conversion de versiones viejas pueda quedar algo mal.
Fue la primera vez que me puse a pensar que tener muchas variables definidas en los objetos podia enlentecer el proceso de build all.
Borrar objetos que no se usan mas.
Era común en la época anterior a GeneXus Server, tener carpetas BORRAR o similares, que tenian todos los objetos que debían borrarse o no se usaban mas, pero se dejaban para no perder la historia de los mismos.En esta KB encontré un folder BORRAR con muchos objetos y desmarque de ella todos los objetos main y volví a correr la opción que borra los objetos no referenciados.
Despues de la limpieza y el build all quedo
modelid registros
----------- -----------
1 1.150.140
3 1.150.140
4 6.391.870
El modelId=4 es una version que creé antes de empezar la limpieza.
El modelId=1 es diseño y el modelId=3 es el modelo en el que estoy trabajando y tiene la misma cantidad de registros que el de diseño pues acabo de hacer una reorganización.
12.938 objetos, 10.597 atributos y 674 tablas.
KBCompress.
Luego de toda la limpieza, conviene chequear si todo esata bien.Para esto utilice el KBCompress de las KBTools, que abre la KB con la opcion de CompressData=true y ademas hace un CheckKnowledgeBase, que revisa si esta todo bien desde el punto de vista interno de la KB.
Demoró 18 minutos y si bien acomodó varios indices no encontro ningun error a corregir.
KBCompressMDF
Con todas estas operaciones, la base de datos de la KB, había crecido hasta los 3.3Gb y el archivo LDF tenia mas de 4GB.Decidí entonces ejecutar el KBCompressMDF de las KBTools, que permite achicar los archivos de la base de datos de la KB.
Despues de ejetuar esto, los archivos de la KB ocupaban 1.8Gb los datos y 500Kb el LDF.
De mas de 7Gb a menos de 2Gb, por lo que en la limpieza recuperamos unos 5Gb.
Hay que recordar que cada vez que se ejecuta una compresión de los archivos de la base de datos, se mueven muchas paginas de datos, lo cual puede llevar a tener fragmentación de las tablas. Conviene por eso, volver a correr la Defragmentacion de indices de las tablas de SQL server del paso inicial.
Resumen
Pasamos de tener13.872 objetos. 10.662 atributos, 674 tablas, 3.8Gb, 6.391.870 registros en ModelCrossReference
12.938 objetos, 10.597 atributos, 674 tablas, 2.2Gb, 1.150.140 registros en ModelCrossReference
Diferencia
934 objetos, 65 atributos, 0 tabla, 1.6Gb, 5.241.730 registros menos.
Ademas, quedaron marcadas varias transacciones con la propiedad Generate Object=*NO lo cual achica aun mas los tiempos del Build All.
Todo esto, generando exactamente los mismos ejecutables, solo eliminando lo mas grueso.
Faltaría aun hacer otra limpieza mas profunda, donde borremos tablas no usadas, transacciones que no se necesitan, objetos no alcanzables (que se llaman entre si y por eso no se borraron la primer limpieza), atributos que están en las tablas pero no se usan, etc, pero todo eso puede quedar para otro post.
UPDATE:
Me preguntar cual era el error que daba al hacer el build all. Lo pongo aca abajo.
========== Build All started ==========
========== Target Environment update started ==========
error: System.Data.SqlClient.SqlException: Valor de tiempo de espera caducado. El período de tiempo de espera caducó antes de completar la operación o el servidor no responde.
en System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection)
en System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj)
en System.Data.SqlClient.TdsParserStateObject.ReadSniError(TdsParserStateObject stateObj, UInt32 error)
en System.Data.SqlClient.TdsParserStateObject.ReadSni(DbAsyncResult asyncResult, TdsParserStateObject stateObj)
en System.Data.SqlClient.TdsParserStateObject.ReadNetworkPacket()
en System.Data.SqlClient.TdsParserStateObject.ReadBuffer()
en System.Data.SqlClient.TdsParserStateObject.ReadByte()
en System.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
en System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
en System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async)
en System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, DbAsyncResult result)
en System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(DbAsyncResult result, String methodName, Boolean sendToPipe)
en System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
en Deklarit.Data.ReadWriteCommand.ExecuteStmt()
en gxioSQL.ModelCrossReferenceDataReader.DeleteByModelId(Int32 modelId)
en Artech.Udm.Layers.Data.SQL.Mappers.ModelCrossReferenceMapper.DeleteByUdmModel(UdmModelKey udmModel)
en Artech.Udm.Layers.BL.Managers.ModelCrossReferenceManager.DeleteByUdmModel(UdmModelKey udmModel)
en Artech.Udm.Framework.UdmKnowledgeBase.BatchCopyModel(Model source, Model target)
en Artech.Udm.Framework.UdmKnowledgeBase.CopyModel(Model source, Model target, CopyModelOptions options, IEnumerable`1 objectTypes, IEnumerable`1 copyOutputIds)
en Artech.Architecture.Common.Objects.KnowledgeBase.CopyModel(KBModel fromModel, KBModel toModel)
en Artech.Core.UI.Services.KBService.CopyModel(KBModel fromModel, KBModel toModel)
en Artech.Packages.Genexus.UI.Commands.CopyModelCommand.Do()
Target Environment update Failed
Build All Failed
Y la sentencia que daba problemas era
with tgt as (select * from ModelCrossReference where ModelId = @ModelIdTarget) merge tgt
using (select * from ModelCrossReference where ModelId = @ModelIdSource) src
on tgt.ToEntityTypeId = src.ToEntityTypeId and tgt.ToEntityId = src.ToEntityId and tgt.FromEntityTypeId = src.FromEntityTypeId and tgt.FromEntityId = src.FromEntityId and tgt.LinkType = src.LinkType and tgt.LinkTypeDetailId = src.LinkTypeDetailId
when not matched by target
then insert values(@ModelIdTarget, ToEntityTypeId, ToEntityId, FromEntityTypeId, FromEntityId, LinkType, LinkTypeDetailId, LinkTypeInfo, ReferenceType)
when matched and (tgt.LinkTypeInfo != src.LinkTypeInfo or tgt.ReferenceType != src.ReferenceType)
then update set tgt.LinkTypeInfo = src.LinkTypeInfo, tgt.ReferenceType = src.ReferenceType
when not matched by source then delete
Hola Enrique, excelente blog, sabes si existe una herramienta para comparar la estructura de la Base de datos física entre el Modelo KB Genexus Vs La DB en producción ?
ResponderBorrarpara hacer eso lo que hago es Crear una base datos limpia y generar las Tablas de la KB.
BorrarUna vez generada esa base datos limpia de todo a todo, hago un backup y lo llevo a produccion,
atravez de querys de SQL, comparo tablas primero, si existen, y luego por cada tabla checo estructuras y no existe un campo genero el query para crearlo.
todo esto buscnado en San Google, un pedazo de aqui y de alla
compara tablas
BorrarDeclare @TABLE_CATALOG varchar(50),@TABLE_SCHEMA varchar(50),@TABLE_NAME varchar(50),@TABLE_TYPE varchar(50),@Tbl varchar(50)
DECLARE @sizes TABLE
(
[name] NVARCHAR(128),
[rows] CHAR(11),
reserved VARCHAR(18),
data VARCHAR(18),
index_size VARCHAR(18),
unused VARCHAR(18)
)
DECLARE tbls_Cursor CURSOR FOR
SELECT TABLE_CATALOG,TABLE_SCHEMA,Upper(TABLE_NAME) as TableName,TABLE_TYPE
FROM Base Datos Limpia.INFORMATION_SCHEMA.TABLES
Order by TABLE_NAME
OPEN tbls_Cursor;
FETCH NEXT FROM tbls_Cursor
INTO @TABLE_CATALOG,@TABLE_SCHEMA,@TABLE_NAME,@TABLE_TYPE
WHILE @@FETCH_STATUS = 0
BEGIN
if Not exists(SELECT TABLE_NAME FROM Base Datos en PRoduccion .INFORMATION_SCHEMA.TABLES WHERE Upper(TABLE_NAME)=@TABLE_NAME)
begin
Set @Tbl=@TABLE_SCHEMA+'.'+@TABLE_NAME
INSERT into @sizes EXEC sp_spaceused @Tbl
end
-- Print '.'+@TABLE_SCHEMA+'.'+@TABLE_NAME
FETCH NEXT FROM tbls_Cursor
INTO @TABLE_CATALOG,@TABLE_SCHEMA,@TABLE_NAME,@TABLE_TYPE
END
CLOSE tbls_Cursor;
DEALLOCATE tbls_Cursor;
SELECT *
FROM @sizes
ORDER BY convert(int, substring(data, 1, len(data)-3)) desc
compara Esctuturas
--Creado by NACE
--DATE 25 JUL 2017
SET NOCOUNT ON;
DECLARE @table_name VarChar(50),@column_name VarChar(50) , @Data_type VarChar(50),@Character_Maximum_Length int,@Numeric_Precision int,@Numeric_Scale int
Declare @CharLon char(5)
DECLARE tbls_Cursor CURSOR FOR
SELECT table_name,column_name, Data_type,Character_Maximum_Length,Numeric_Precision,Numeric_Scale
FROM base datos producion.INFORMATION_SCHEMA.COLUMNS
OPEN tbls_Cursor;
FETCH NEXT FROM tbls_Cursor
INTO @table_name,@column_name, @Data_type,@Character_Maximum_Length,@Numeric_Precision,@Numeric_Scale
WHILE @@FETCH_STATUS = 0
BEGIN
if exists(SELECT table_name FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name=@table_name)
if not exists(SELECT column_name FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name=@table_name and column_name=@column_name)
begin
if @Data_type in ('char','varchar','nchar','nvarchar')
Begin
if @Character_Maximum_Length=-1 and @Data_type in('varchar','nvarchar')
Print 'ALTER TABLE ['+@table_name+'] ADD ['+@column_name+'] '+ @Data_type+'(MAX) NOT NULL CONSTRAINT '+ @column_name+@table_name+'_DEFAULT DEFAULT '+char(39)+char(39)
else
Print 'ALTER TABLE ['+@table_name+'] ADD ['+@column_name+'] '+ @Data_type+'('+Rtrim(Convert(char(5),@Character_Maximum_Length))+') NOT NULL CONSTRAINT '+ @column_name+@table_name+'_DEFAULT DEFAULT '+char(39)+char(39)
end
if @Data_type in ('money','int','smallint','float','smallmoney','tinyint')
Print 'ALTER TABLE ['+@table_name+'] ADD ['+@column_name+'] '+ @Data_type+' NOT NULL CONSTRAINT '+ @column_name+@table_name+'_DEFAULT DEFAULT 0'
if @Data_type in ('decimal')
Print 'ALTER TABLE ['+@table_name+'] ADD ['+@column_name+'] '+ @Data_type+'('+Rtrim(Convert(char(5),@Numeric_Precision))+','+Rtrim(Convert(char(5),@Numeric_Scale))+') NOT NULL CONSTRAINT '+ @column_name+@table_name+'_DEFAULT DEFAULT 0'
if @Data_type='datetime'
Print 'ALTER TABLE ['+@table_name+'] ADD ['+@column_name+'] '+ @Data_type+' NOT NULL CONSTRAINT '+ @column_name+@table_name+'_DEFAULT DEFAULT Convert(DATETIME,'+char(39)+'17530101'+char(39)+',112)'
if @Data_type='bit'
Print 'ALTER TABLE ['+@table_name+'] ADD ['+@column_name+'] '+ @Data_type+' NULL '
end
FETCH NEXT FROM tbls_Cursor
INTO @table_name,@column_name, @Data_type,@Character_Maximum_Length,@Numeric_Precision,@Numeric_Scale
END
CLOSE tbls_Cursor;
DEALLOCATE tbls_Cursor;