El problema del Reenvío de punteros en tablas heap

Descubre el impacto del reenvío de punteros en tablas heap de SQL Server y cómo evitar este problema que degrada el rendimiento.

Hoy vamos a hablar de un fenómeno tan molesto como habitual en ciertos entornos: el reenvío de punteros en tablas heap de SQL Server. Sí, ese “detalle” que suele pasar desapercibido hasta que un día nuestras consultas empiezan a ir como un carro tirado por burros, y claro, toca ponerse el traje de bombero.

Antes de entrar en harina, pongamos un poco de contexto. Las tablas heap, esas nobles estructuras sin índice clustered, a veces se eligen por necesidad, otras por ignorancia y, en los peores casos, porque alguien tenía prisa por entregar y pensó que ya lo arreglaríamos después.Y claro, luego nunca se arregla. Y mientras tanto, los forwarded records o reenvíos de punteros campan a sus anchas.

¿Qué es un reenvío de punteros y por qué debería importarnos?

Cuando una tabla no tiene índice clustered, SQL Server guarda las filas donde buenamente puede. Sin ese ordenamiento y estructura que un clustered impone, el heap es un campo abierto. Hasta aquí, todo bien. El problema aparece cuando actualizamos una fila y esa nueva versión no cabe en la misma página. ¿Qué hace SQL Server? ¿Mover toda la fila a otra página y actualizar los punteros que la referencian? Ojalá. Lo que hace es dejar un puntero en la ubicación original que apunta a la nueva ubicación de la fila. Ese es el reenvío de puntero: un salto innecesario que se añade al acceso de la fila.

Esto, que puede parecer inofensivo en pequeñas dosis, se convierte en un verdadero problema cuando la tabla crece y las modificaciones son frecuentes. Cada reenvío implica un acceso extra a disco o memoria para encontrar la fila real. Y como bien sabemos, esos accesos extra no son gratuitos: incrementan el tiempo de lectura y degradan el rendimiento de las consultas. Especialmente en esas tablas que alguien decidió consultar con un SELECT * y sin WHERE, porque total, ¿qué podría salir mal?

Cómo se genera un reenvío de punteros

Veámoslo en acción. Tenemos una tabla heap. Insertamos filas. Todo perfecto. Llega el día en que un UPDATE aumenta el tamaño de una fila (añadimos datos a una columna VARCHAR, por ejemplo). La fila ya no cabe en su página. SQL Server mueve la fila a una nueva página y deja un puntero en la posición original apuntando a la nueva ubicación. Algo así como el cartel de nos hemos mudado que ponen algunos negocios en su antiguo local cuando cambian de ubicación. Ahora acceder a esa fila implica primero leer la página con el puntero, luego saltar a la nueva página y leer la fila real. Siguiendo con el ejemplo anterior es como si el GPS te llevase al local antiguo para que vieses el cartel y tuvieses que ir al nuevo.

Pero es que la fiesta no termina ahí. Si la fila se vuelve a actualizar y tampoco cabe en su nueva página, obtenemos un reenvío de un reenvío. ¿Bonito, verdad? Sí, tan bonito como ese fragmento lógico que nadie defragmenta porque “total, no pasa nada”.

Impacto en el rendimiento de los reenvíos de punteros

El impacto de los reenvíos de punteros se manifiesta principalmente en las operaciones de lectura. Cada reenvío supone al menos un salto adicional. Si tenemos un número considerable de ellos, nuestras lecturas se convierten en una gincana de páginas de datos, con sus correspondientes cache misses, latches y demás alegría. Pero no creas que las escrituras están exentas, recuerda que antes de escribir casi siempre lees.

Esto no solo afecta a la velocidad de las consultas. También incrementa el uso de CPU, el tráfico de I/O y la presión sobre el buffer pool. Y aquí viene el detalle que más nos gusta: el optimizador de consultas no tiene en cuenta el coste de los reenvíos al planificar. Así que podemos tener un plan que parecía estupendo sobre el papel y que en ejecución se arrastra como un SELECT con hints absurdos.

Cómo detectar el reenvío de punteros

Detectar reenvíos de punteros no es complicado, pero requiere mirar donde hay que mirar. Podemos usar sys.dm_db_index_physical_stats con la opción DETAILED para analizar las tablas heap y ver el número de forwarded_records. Si este número empieza a crecer, es hora de preocuparse.

Un ejemplo de consulta para los amigos de lo rápido y directo:

Esto nos da una idea de cuántos reenvíos tenemos por tabla. Si ese número no es cero, tenemos trabajo pendiente. Y si el número es alto, es probable que también tengamos un problema de rendimiento.

¿Como elimino los reenvíos de punteros?

La primera solución (y la más eficaz) es: no uses heap salvo que tengas un motivo sólido para hacerlo. Si la tabla tiene lecturas frecuentes y actualizaciones que modifican el tamaño de las filas, un índice clustered es casi siempre mejor elección. Sí, incluso aunque esa tabla sea de staging y “solo esté ahí un ratito”.

Si por algún motivo tenemos que seguir con el heap, toca plan de mantenimiento especial. Por norma general, los planes de mantenimiento de índices no nos van a servir. En su lugar, tenemos que usar el comando ALTER TABLE [NombreTabla] REBUILD que reconstruye el heap y elimina los reenvíos de puntero. Pero claro, eso implica bloqueo de tabla (en algunas versiones de SQL) y no es algo que uno quiera hacer en mitad de un horario productivo.

Otra opción es rediseñar el esquema de la tabla para minimizar las columnas de longitud variable que crecen sin control. Porque claro, si tenemos un VARCHAR(8000) para guardar un número de teléfono “por si acaso”, el problema no es el heap: el problema somos nosotros.

Y por supuesto, monitorizar. Tener un job que periódicamente revise el número de forwarded_records y alerte si se dispara es una medida sencilla que nos puede ahorrar muchas sorpresas.

¿Hay situaciones donde el heap tenga sentido?

Sí, existen. Por ejemplo, en tablas de staging para cargas de datos donde no se hacen actualizaciones y los datos se procesan y eliminan rápidamente. Ahí un heap puede funcionar bien. Pero claro, en cuanto se empieza a hacer algún UPDATE, la decisión de no poner un clustered se vuelve difícil de defender. Y no, el argumento de “es que así insertamos más rápido” no vale si luego el rendimiento de las consultas se va al traste.

Conclusión

El reenvío de punteros es un mecanismo interno de SQL Server para lidiar con el crecimiento de filas en tablas heap. No es un bug, es un diseño consciente. El problema es que suele aparecer porque tomamos decisiones de diseño pobres o porque descuidamos el mantenimiento. Si tenemos un heap, debemos ser conscientes de las implicaciones y monitorizar su estado. Y si vemos reenvíos, actuar antes de que el rendimiento se desplome.

Como siempre, lo más efectivo es evitar el problema desde el principio. Un índice clustered bien elegido elimina de raíz el riesgo de reenvíos. Y si alguien nos insiste en que un heap es la mejor opción para esa tabla con 500 millones de filas y actualizaciones frecuentes, siempre podemos sugerirle que haga las pruebas en producción. Total, ¿qué podría salir mal?

Espero que este artículo te haya resultado útil e interesante. Si tienes alguna duda o comentario, no dudes en contactarnos en Twitter o por mail o dejarnos un mensaje en los comentarios de aquí abajo. Y recuerda que también tenemos un grupo de LinkedIn al que te puedes unir.

Publicado por Roberto Carrancio

Mi nombre es Roberto Carrancio y soy un DBA de SQL server con más de 10 años de experiencia en el sector. Soy el creador del blog soydba.es donde intento publicar varios artículos a la semana (de lunes a viernes que los fines de semana me gusta estar con mi gente y disfrutar de mi moto) Espero que disfrutes leyendo este blog tanto como yo disfruto escribiendo y que te sea de utilidad. Si tienes alguna sugerencia, pregunta o comentario, puedes dejarlo al final de cada entrada o enviarme un correo electrónico. Estaré encantado de leerte y responderte. ¡Gracias por tu visita! Mi principal interés es compartir mi conocimiento sobre bases de datos con todo el que quiera aprenderlo. Me parece un mundo tan apasionante como desconocido. Fuera de lo profesional me encanta la cocina, la moto y disfrutar de tomar una cervecita con amigos.

Deja una respuesta