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:

  • 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 tener

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

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







Comentarios

  1. 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 ?

    ResponderBorrar
    Respuestas
    1. para hacer eso lo que hago es Crear una base datos limpia y generar las Tablas de la KB.
      Una 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

      Borrar
    2. compara tablas

      Declare @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;

      Borrar

Publicar un comentario

1) Lee el post
2) Poné tu opinión sobre el mismo.
Todos los comentarios serán leidos y la mayoría son publicados.

Entradas más populares de este blog

La nefasta influencia del golero de Cacho Bochinche en el fútbol uruguayo

Aplicación monolítica o distribuida?

Funcionalidades de GeneXus que vale la pena conocer: DATE Constants.