indices

Búsqueda híbrida en SQL Server: Combinando FULLTEXT y Vectores

Llevamos años viendo aplicaciones que convierten la busqueda de textos en una trituradora de CPU. El usuario escribe tres palabras, la capa de aplicación las trocea como puede, el ORM genera una consulta digna de un informe forense y SQL Server acaba haciendo lo que siempre hace, intentar salvarnos de nuestras propias decisiones. A veces lo consigue. A veces no. Y cuando no lo consigue, alguien abre una incidencia diciendo que “la base de datos va lenta”, como si el motor hubiera decidido por iniciativa propia montar un festival de LIKE ‘%texto%’.

La búsqueda de texto completo ha sido durante mucho tiempo nuestra herramienta principal para resolver este tipo de problemas. Los índices full-text permiten buscar términos dentro de columnas extensas, ordenar por relevancia y evitar que cada búsqueda acabe en un escaneo indecente. Ya hablé de esto con más detalle en el artículo sobre índices de texto completo o full-text indexes, porque siguen siendo una herramienta muy válida cuando necesitamos precisión léxica.

Pero ahora tenemos otro actor en escena: la búsqueda vectorial. SQL Server 2025, Azure SQL Database y SQL Database en Fabric incorporan capacidades nativas para trabajar con vectores, calcular distancias y ejecutar búsquedas aproximadas de vecinos más cercanos mediante VECTOR_SEARCH. En SQL Server 2025 estas capacidades están en vista previa y requieren habilitar PREVIEW_FEATURES. Además, Microsoft documenta que la versión más reciente de los índices vectoriales está disponible actualmente en Azure SQL Database y SQL Database en Microsoft Fabric.

La tentación evidente consiste en pensar que la búsqueda vectorial sustituye al full-text. Es una conclusión cómoda y moderna, pero equivocada. Es muy de presentación ejecutiva, lo que debería hacernos sospechar inmediatamente. La realidad es bastante más interesante, full-text y vectores resuelven problemas distintos, y la búsqueda híbrida aparece precisamente cuando dejamos de tratarlos como enemigos.

Antes de lanzarnos a escribir T-SQL como si no hubiera mañana, conviene dejar claro el problema de fondo.

La búsqueda híbrida no es IA pegada con cinta americana

La búsqueda híbrida combina una búsqueda léxica tradicional con una búsqueda semántica vectorial y fusiona después ambos conjuntos de resultados mediante una técnica de reordenación. La idea parece sencilla, una rama encuentra coincidencias de palabras con full-text indexes, la otra encuentra documentos semánticamente cercanos mediante comparación de embeddings, y una fase final decide qué documentos merecen aparecer arriba.

La clave está en esa última frase. No basta con ejecutar dos consultas y hacer un UNION. Eso no es búsqueda híbrida. Tampoco basta con sumar una puntuación full-text y una distancia vectorial, porque ambas magnitudes viven en universos matemáticos diferentes. SQL Server no va a convertir una escala relativa de relevancia textual y una distancia de coseno en una verdad universal solo porque usemos alias bonitos.

El full-text devuelve señales de relevancia asociadas a coincidencias lingüísticas. CONTAINSTABLE y FREETEXTTABLE devuelven una columna RANK con valores ordinales entre 0 y 1000, pero esos valores indican orden relativo dentro del conjunto devuelto. Microsoft advierte que el valor real no debe tratarse como una puntuación absoluta y puede variar entre ejecuciones.

La búsqueda vectorial, en cambio, devuelve una distancia. Si usamos coseno, valores más bajos indican mayor cercanía cuando trabajamos con VECTOR_DISTANCE o con la columna distance generada por VECTOR_SEARCH. Esa distancia expresa proximidad geométrica entre embeddings, no coincidencia literal de términos. Comparar directamente ambas señales es como comparar páginas leídas con cuanto agua está lloviendo fuera. Puedes hacerlo, pero no significa nada útil.

Por eso la búsqueda híbrida necesita una fase de fusión por rango. No fusionamos puntuaciones brutas, fusionamos posiciones. Y ahí entra Reciprocal Rank Fusion, más conocido como RRF.

Para entender por qué esto importa tanto, primero hay que asumir que full-text sigue siendo necesario.

Full-text: precisión léxica, no comprensión semántica

Un índice full-text sigue siendo una pieza excelente cuando necesitamos encontrar términos, variantes lingüísticas, prefijos, expresiones ponderadas o documentos donde aparecen palabras concretas. No es una tecnología vieja por no llevar “AI” en el nombre. De hecho, suele ser mucho más útil que una demo de embeddings cuando el usuario busca un código, una referencia contractual, un identificador de error o una cláusula exacta.

Imaginemos una base de conocimiento de soporte. Si el usuario busca OOM-802, lo normal es que quiera documentos que contengan OOM-802. No quiere artículos “conceptualmente relacionados con problemas de memoria”. Quiere ese código. Exactamente ese. Si le devolvemos un documento sobre “optimización general de memoria en servidores de aplicaciones” porque está cerca en el espacio vectorial, hemos hecho una búsqueda muy sofisticada y muy inútil. Todo muy moderno, todo muy mal.

Búsqueda de texto completo

Aquí full-text brilla. CONTAINSTABLE permite buscar en una columna indexada y obtener una lista ordenada por relevancia. Además, podemos limitar el volumen con el parámetro top_n_by_rank, algo especialmente importante cuando la condición puede devolver decenas o cientos de miles de filas. La documentación de Microsoft indica que limitar resultados con top_n_by_rank puede mejorar drásticamente el rendimiento, porque SQL Server ordena las coincidencias por rango y devuelve solo las primeras.

Esto no significa que full-text entienda el significado. Si buscamos “vehículo económico”, no podemos asumir que devuelva “coche barato”. Puede haber cierta ayuda lingüística según idioma, tesauro y configuración, pero no estamos ante comprensión semántica general. Full-text trabaja con términos, proximidad, formas lingüísticas y ranking textual. Los embeddings trabajan con representación numérica del significado. Mezclarlos sin entender esta diferencia es una forma estupenda de construir una solución que falle justo cuando el usuario la necesite.

En artículos anteriores del blog ya traté los índices de texto completo desde la perspectiva del DBA. Esa base sigue siendo necesaria. La búsqueda híbrida no elimina el conocimiento de full-text, lo potencia. Si alguien quiere saltar directamente a vectores sin entender CONTAINS, FREETEXT, CONTAINSTABLE, catálogos full-text, stopwords y ranking, adelante. Pero luego no vale llorar.

La otra mitad del problema está en la búsqueda semántica.

Vectores: significado aproximado, no magia contractual

Un embedding representa un texto como un vector numérico. La idea práctica es que textos con significado parecido generen vectores cercanos. A partir de ahí, podemos medir distancias mediante coseno, producto escalar o distancia euclídea, y recuperar los documentos más cercanos al vector de consulta. SQL Server permite realizar búsquedas exactas con VECTOR_DISTANCE, calculando la distancia entre el vector de búsqueda y los vectores almacenados. Microsoft describe este enfoque exacto como un cálculo exhaustivo que garantiza recuperar los vecinos más cercanos, aunque puede resultar costoso en conjuntos grandes.

La búsqueda exacta tiene una ventaja fundamental, no aproxima. Calcula. Si filtramos primero por cliente, estado, idioma, producto o fecha, y después aplicamos VECTOR_DISTANCE sobre un conjunto pequeño, podemos obtener resultados muy buenos sin necesidad de crear un índice vectorial. Esta estrategia suele ser más sensata de lo que parece, sobre todo en tablas medianas o en escenarios donde los filtros relacionales reducen mucho el número de candidatos.

La búsqueda aproximada, por su parte, usa estructuras especializadas para evitar comparar contra todos los vectores. En SQL Server y Azure SQL hablamos de VECTOR_SEARCH, que busca vectores similares mediante un algoritmo aproximado de vecinos más cercanos. Con la versión más reciente de los índices vectoriales, la sintaxis recomendada usa SELECT TOP (N) WITH APPROXIMATE y deja atrás el antiguo parámetro TOP_N, que Microsoft marca como obsoleto para índices recientes.

Índices vectoriales: grafos de embeddings

Aquí aparece DiskANN. Un índice vectorial de tipo DiskANN acelera la recuperación aproximada navegando una estructura de grafo pensada para localizar vecinos cercanos sin recorrer todo el conjunto. Eso tiene un coste. Hay mantenimiento, requisitos, restricciones y decisiones de diseño. No creamos un índice vectorial porque la palabra “vector” aparezca en una reunión. Lo creamos cuando el volumen, la latencia esperada y el patrón de consulta lo justifican.

En el artículo sobre DiskANN y búsqueda vectorial en SQL Server 2025 ya expliqué por qué no conviene indexar vectores en cualquier tabla. Esta recomendación sigue siendo válida. Si tienes 3.000 filas y filtros buenos, probablemente VECTOR_DISTANCE con búsqueda exacta te dé una solución más simple, más predecible y más fácil de razonar. La complejidad innecesaria siempre encuentra la forma de facturarse más tarde.

Como véis, la búsqueda semántica resuelve muy bien consultas ambiguas. “Portátil ligero para viajar”, “incidencia parecida a error de memoria”, “cliente molesto por retrasos recurrentes” o “documentación sobre migración sin parada” son ejemplos donde la coincidencia literal se queda corta. Pero esa misma flexibilidad puede volverse peligrosa cuando la consulta contiene un literal crítico.

Por eso necesitamos una arquitectura híbrida.

El patrón correcto: recuperar candidatos y fusionar rangos

La búsqueda híbrida no debería ejecutar una búsqueda vectorial contra toda la tabla y otra full-text sin límite para después cruzar todo alegremente. Eso solo consumiría recursos a lo loco. El patrón correcto consiste en recuperar un número razonable de candidatos por cada rama y fusionar después esos candidatos mediante un ranking controlado.

La rama léxica puede usar CONTAINSTABLE o FREETEXTTABLE, según queramos una búsqueda más precisa o más lingüística. Para códigos, identificadores, términos técnicos y mensajes de error, normalmente prefiero CONTAINSTABLE. Para búsquedas más naturales o descriptivas, FREETEXTTABLE puede encajar mejor. No son intercambiables, aunque en muchas aplicaciones se usen como si lo fueran. Luego nos extraña que el buscador parezca tener resaca.

La rama semántica puede usar VECTOR_DISTANCE o VECTOR_SEARCH. Si queremos una búsqueda exacta sobre pocos candidatos, VECTOR_DISTANCE es una buena opción. Si tenemos cientos de miles o millones de embeddings, baja tolerancia a latencia y un índice vectorial adecuado, VECTOR_SEARCH pasa a tener sentido. Microsoft también documenta que los índices vectoriales recientes aplican predicados de WHERE durante el proceso de búsqueda vectorial mediante filtrado iterativo, no simplemente después de recuperar vecinos, pero eso, de momento solo está en Azure.

Unir resultados 

La fase final no debe sumar RANK full-text con distancia vectorial. Debe convertir cada lista en posiciones ordinales y aplicar RRF. La fórmula general es sencilla:

score(d) = Σ 1 / (k + rank_i(d))

Donde rank_i(d) es la posición del documento d en la lista i, y k es una constante de suavizado. Si un documento no aparece en una rama, esa rama aporta cero. No aporta un rango inventado de diez mil, salvo que queramos aplicar una penalización heurística deliberada. RRF canónico suma contribuciones de listas donde el documento existe, lo demás ya es cocina propia.

El valor k = 60 aparece a menudo como constante práctica. No es una ley divina, ni una recomendación que debamos tatuarnos pero si podríamos decir que es un estándar del mercado. Un k más bajo da más peso a las primeras posiciones; un k más alto suaviza la diferencia entre posiciones. En buscadores internos suelo empezar con 60, medir, revisar consultas reales y ajustar. Sí, medir. Esa costumbre extravagante que a veces evita reuniones.

Vamos a bajarlo a código.

Modelo de datos para un escenario realista

Supongamos una tabla de tickets de soporte. Cada ticket tiene una descripción textual, metadatos relacionales y un embedding generado a partir del contenido que queremos buscar. No voy a centrarme aquí en la generación del embedding porque ya traté las búsquedas semánticas con IA en SQL Server 2025 en otro artículo, y además dejé ejemplos prácticos en YouTube para quien quiera ver el flujo completo con más calma.

La tabla podría tener esta forma simplificada:

El índice relacional no es decorativo. En una arquitectura híbrida seria, los filtros siguen importando. Si buscamos tickets de un cliente concreto, de un producto concreto o de un rango temporal, no queremos que la parte vectorial se comporte como un turista perdido recorriendo toda la ciudad. Queremos acotar el problema antes de calcular distancias.

El índice full-text iría sobre las columnas textuales relevantes. En producción hay que cuidar idioma, catálogos, stoplists y fragmentación del índice full-text, pero el esqueleto sería este:

El LANGUAGE 3082 corresponde al español. En entornos multidioma, este detalle se complica bastante. Un catálogo global con textos mezclados en cinco idiomas y una sola configuración lingüística no es internacionalización; es una ruleta. Si el negocio necesita búsquedas multidioma, hay que diseñar para ello, no rezar para que el motor “lo entienda”.

Para la parte vectorial, un índice DiskANN puede crearse así cuando el volumen lo justifique:

Los índices vectoriales recientes exigen al menos 100 filas con vectores no nulos antes de poder crearse, requieren una clave primaria clustered y tienen limitaciones actuales como ausencia de particionado, restricciones con TRUNCATE TABLE, no replicarse a suscriptores y problemas de despliegue directo con DacPac o BACPAC porque el índice se crea antes de cargar datos.

Esto ya nos da una pista importante: no hablamos de un índice cualquiera. No es un IX_Tabla_Campo que añadimos a las tres de la tarde para callar una alerta. Un índice vectorial afecta al ciclo de vida de la tabla, al despliegue, al mantenimiento y a los procesos de carga. Si el plan de despliegue de tu base de datos ignora esto, no tienes un plan; tienes un problema.

Ahora sí, veamos una búsqueda híbrida exacta.

Búsqueda híbrida exacta con VECTOR_DISTANCE

El primer patrón usa VECTOR_DISTANCE. Es el más fácil de razonar porque calcula distancias reales contra los candidatos considerados. Conviene usarlo cuando el conjunto final no es enorme, cuando los filtros relacionales son selectivos o cuando la precisión importa más que la latencia extrema.

El siguiente ejemplo recibe una consulta textual, genera su embedding y combina la rama semántica con la rama full-text. Limitamos ambas ramas para no fusionar medio millón de filas, es la forma de decirle a SQL “no hagas barbaridades”.

Hay varias decisiones intencionadas aquí. La rama semántica no calcula distancia contra toda la tabla, sino contra CandidatosFiltrados. Esto permite que el optimizador use índices relacionales antes de aplicar la parte matemática. Si el cliente tiene 4.000 tickets abiertos del producto, calcular 4.000 distancias puede ser razonable. Si tiene 40 millones, probablemente estamos diseñando otra cosa.

La rama léxica usa CONTAINSTABLE con top_n_by_rank. Ese cuarto parámetro limita el número de resultados full-text devueltos. No quiero traer todos los tickets que contengan “memoria” para después descubrir que el usuario solo verá 20 filas. Esa es la diferencia entre una consulta y un castigo.

La fusión usa FULL OUTER JOIN porque un documento puede aparecer solo en la rama semántica o solo en la rama léxica. Después, COALESCE(1.0 / (@rrf_k + rango), 0.0) suma cero cuando el documento no aparece en una rama. Esto es importante, no inventamos rangos artificiales. Si queremos penalizar más o menos los resultados huérfanos, podemos hacerlo, pero entonces debemos reconocer que estamos introduciendo una heurística adicional.

También ordeno por criterios secundarios. En producción, los empates ocurren. Si no defines desempates, el motor puede devolverte órdenes distintos entre ejecuciones. Y cuando el usuario dice “ayer salía primero”, a nadie le divierte explicar que una ordenación parcial no garantiza estabilidad. Bueno, a nadie normal, que yo estoy aquí preparando el artículo más largo de todos los que he escrito para explicaros eso.

Este patrón exacto es muy útil cuando trabajamos con filtros fuertes. Pero si el volumen semántico es grande y necesitamos latencia baja, toca pasar a búsqueda aproximada.

Búsqueda híbrida aproximada con VECTOR_SEARCH

VECTOR_SEARCH representa el patrón de búsqueda vectorial aproximada. Con índices vectoriales recientes, la sintaxis exige SELECT TOP (N) WITH APPROXIMATE y ordenación ascendente por la columna distance, que es la única clave de ordenación válida para los resultados aproximados. Si existe un índice ANN compatible con la misma métrica y columna, puede usarse, si no existe, el motor puede recurrir a kNN y emitir una advertencia.

Una versión híbrida aproximada puede quedar así:

Este ejemplo es parecido al anterior, pero cambia la rama semántica. Ahora usamos VECTOR_SEARCH, que devuelve la columna distance. La consulta solicita explícitamente resultados aproximados con WITH APPROXIMATE. Esto no es un adorno sintáctico, en índices recientes, Microsoft separa claramente la búsqueda aproximada de la exacta mediante esta sintaxis.

La presencia del WHERE dentro de la rama semántica también importa. En versiones recientes de índices vectoriales, Microsoft documenta filtrado iterativo, es decir, los predicados se aplican durante el proceso de búsqueda vectorial, no únicamente después. En versiones antiguas, los filtros podían aplicarse solo después de recuperar vecinos, lo que provocaba resultados vacíos o incompletos si los primeros vecinos no cumplían el filtro. Esta diferencia no es teórica, afecta directamente a la calidad del resultado.

Si trabajas con índices vectoriales antiguos, conviene migrarlos. Microsoft indica que los índices creados con estructuras anteriores se mantienen en la versión actual, pero quedarán retirados en una versión futura, y para obtener las capacidades recientes hay que eliminar y recrear el índice. La propia DMV sys.vector_indexes, junto con sys.indexes y sys.tables, permite consultar la versión registrada en los parámetros de construcción.

Esto introduce una responsabilidad nueva para el DBA. Ya no basta con saber si el índice existe. Hay que saber qué versión tiene, qué sintaxis admite, cómo se comporta con filtros y si soporta mantenimiento DML completo. El “tenemos índice vectorial” se parece demasiado al “tenemos backup”: la frase tranquiliza hasta que preguntas cuándo se ha probado.

RRF con pesos: cuando no todas las señales valen lo mismo

RRF básico da el mismo peso a cada rama. Eso suele ser un buen punto de partida, porque evita discusiones prematuras. Pero no todos los dominios tienen la misma necesidad de precisión literal y contexto semántico. En una base de conocimiento técnica, los códigos de error, nombres de procedimiento, versiones y referencias internas suelen pesar mucho. En un catálogo de productos o una búsqueda documental abierta, la semántica puede tener más protagonismo.

Podemos introducir pesos sin romper la estructura:

Esto ya no es RRF puro, sino RRF ponderado. No hay problema, siempre que seamos conscientes de ello. Lo peligroso no es ajustar el algoritmo, lo peligroso es hacerlo sin medir. Si damos demasiado peso a la rama semántica, un literal crítico puede quedar enterrado bajo documentos conceptualmente parecidos. Si damos demasiado peso a la rama léxica, perdemos recall semántico y volvemos al mundo de “no encuentro coche barato porque el documento dice vehículo económico”.

En entornos serios conviene registrar consultas reales, resultados seleccionados, clics, aperturas, tiempo hasta resolver la incidencia y feedback explícito si existe. Después podemos comparar configuraciones de pesos, tamaños de candidatos y valores de k. La relevancia no se calibra en una pizarra. Se calibra con datos y pruebas. Qué concepto tan revolucionario.

También podemos modificar pesos en función de la consulta. Si detectamos patrones de códigos, identificadores, GUIDs, números de versión o referencias internas, podemos favorecer full-text. Si la consulta es larga, descriptiva y sin literales críticos, podemos favorecer semántica. Este tipo de adaptación suele dar mejores resultados que una configuración fija para todos los casos.

La detección no tiene por qué ser perfecta. Basta con aplicar reglas prudentes. Una consulta con OOM-802, SQLSTATE 40001, KB500 o Procedure dbo.X probablemente necesita más precisión léxica. Una consulta como “el agente se queda bloqueado después de actualizar y consume mucha memoria” puede beneficiarse más del vector. El usuario no nos entrega una intención formal; nos entrega una frase. Nuestro trabajo consiste en no destrozarla.

El tamaño de los candidatos importa más de lo que parece

Uno de los errores más comunes en búsqueda híbrida consiste en recuperar pocos candidatos por rama. Si pedimos top 10 semántico y top 10 léxico para devolver top 10 final, apenas damos margen al algoritmo de fusión. RRF necesita espacio para encontrar documentos que quizá no sean número uno en ninguna rama, pero sí sean buenos en ambas.

Un documento que aparece en posición 12 en la rama semántica y posición 8 en la rama léxica puede ser mucho mejor resultado global que otro que aparece en posición 1 semántica y no aparece en léxica. Si solo recuperamos diez candidatos por rama, el primero puede sobrevivir y el segundo puede ni siquiera existir en la fusión. El algoritmo no puede ordenar documentos que nunca le hemos dado.

Por eso suelo trabajar con una ventana de candidatos bastante mayor que el resultado final. Si voy a mostrar 20 resultados, empezar con 100 o 200 por rama suele ser razonable. Luego hay que medir. No es lo mismo una tabla de tickets internos que una base documental de millones de artículos. Tampoco es lo mismo una búsqueda con filtros por cliente que un buscador global público.

El parámetro top_n_by_rank de full-text ayuda en la rama léxica. En la rama vectorial aproximada, TOP (N) WITH APPROXIMATE marca la ventana semántica. En la rama exacta con VECTOR_DISTANCE, TOP (N) limita el cálculo ordenado final, aunque el coste previo depende de cuántas filas sobrevivan a los filtros relacionales.

Aquí conviene vigilar planes de ejecución, memoria concedida, spills, paralelismo y lecturas lógicas. También hay que observar la distribución de distancias. Si todos los documentos tienen distancias muy parecidas, la señal semántica quizá no discrimina bien. Si el full-text devuelve rankings muy planos, la parte léxica quizá necesita mejorar sintaxis, tesauro, stopwords o normalización de contenido.

La búsqueda híbrida no elimina el tuning. Le añade dimensiones nuevas. Por si alguien echaba de menos complicarse la vida.

Exact search frente a ANN: elegir sin postureo

La búsqueda exacta con VECTOR_DISTANCE calcula distancias y ordena. Es simple, precisa y fácil de auditar. Si el conjunto candidato es pequeño, suele ser una gran elección. También es buena para validación, pruebas de calidad y comparación contra la búsqueda aproximada. En escenarios donde la exactitud es crítica, tener un modo exacto ayuda a detectar pérdida de recall introducida por ANN.

La búsqueda aproximada con VECTOR_SEARCH sacrifica exactitud teórica por velocidad. No tiene nada de malo. Muchos sistemas de búsqueda funcionan así porque el usuario necesita respuestas rápidas y suficientemente buenas. El problema aparece cuando alguien vende “aproximado” como si significara “igual pero más rápido”. No. Significa aproximado. La palabra está ahí, generosamente visible.

Microsoft documenta que VECTOR_SEARCH utiliza búsqueda aproximada de vecinos más cercanos, y que con índices recientes el optimizador puede decidir entre usar DiskANN o búsqueda kNN según las características de la consulta. También indica que, si no hay un índice ANN compatible con la métrica y la columna, se genera una advertencia y se usa kNN.

Esto tiene implicaciones operativas. Si una consulta esperada como aproximada cae a kNN por no tener índice compatible, puede cambiar radicalmente su coste. Si alguien crea el índice con métrica DOT y la consulta usa cosine, no esperes milagros. El motor no va a inventar un índice compatible porque el ticket esté marcado como urgente.

La métrica debe estar alineada con el modelo de embeddings y con la normalización aplicada. En muchos modelos textuales se usa coseno, pero no conviene asumirlo por costumbre. Hay que revisar cómo se generaron los embeddings, si están normalizados y qué distancia recomienda el proveedor o el patrón de uso. El DBA no tiene por qué entrenar el modelo, pero sí debe entender qué está almacenando y cómo se consulta.

Además, el índice vectorial no sustituye los índices relacionales. Microsoft recomienda combinar índices vectoriales con índices tradicionales en columnas usadas por filtros, especialmente cuando el filtrado iterativo puede aprovechar predicados selectivos.

Dicho de otra forma, seguimos necesitando buenos índices B-Tree. La IA no ha derogado la selectividad. Qué disgusto para algunos.

DML, versiones de índice y falsas verdades que caducan

Durante las primeras versiones, una de las limitaciones más agresivas de los índices vectoriales era que las tablas quedaban de solo lectura después de crear el índice. Para permitir DML había que aceptar índices obsoletos mediante configuración, con el riesgo de resultados no actualizados. Esa limitación existió y era importante.

Pero no conviene escribir arquitectura en piedra cuando la tecnología está en vista previa y cambia deprisa. Microsoft documenta que los índices vectoriales creados con la versión más reciente eliminan esa restricción: soportan INSERT, UPDATE, DELETE y MERGE, con mantenimiento automático y en tiempo real del índice. Los cambios son visibles para las búsquedas vectoriales después del commit.

Esto no significa que podamos olvidarnos del mantenimiento. Para reemplazos masivos de datos, por ejemplo al regenerar embeddings con otro modelo, Microsoft recomienda considerar eliminar y recrear el índice después de la carga para mantener calidad de búsqueda predecible. La estructura del índice se construyó con una distribución vectorial determinada; si cambiamos casi todos los embeddings, no deberíamos sorprendernos de que la calidad se resienta.

También siguen existiendo restricciones actuales. No hay soporte de particionado para índices vectoriales, la tabla necesita clave primaria clustered, TRUNCATE TABLE no puede ejecutarse directamente sobre tablas con índice vectorial y los despliegues con DacPac o BACPAC requieren cuidado porque el índice necesita datos no nulos existentes al crearse.

Esto cambia bastante el diseño de procesos ETL. Si regeneramos embeddings en bloque, quizá convenga cargar en una tabla staging, validar dimensiones, controlar nulos, medir duplicados y después hacer intercambio lógico. Si trabajamos en Azure SQL o Fabric con índices recientes, no tenemos que congelar la tabla para cada operación DML normal, pero sí debemos diseñar cargas masivas con cabeza.

También hay que monitorizar. La documentación menciona sys.dm_db_vector_indexes para observar salud del índice y tareas de mantenimiento. En sistemas donde el embedding cambia con frecuencia, esa DMV deja de ser curiosidad y pasa a ser parte del cuadro de mando operativo.

La conclusión práctica es sencilla, no repitas limitaciones antiguas sin mirar versión, pero tampoco vendas los índices vectoriales como índices normales. Ambas posturas son malas. Una por anticuada; la otra por ingenua.

Calidad de datos: embeddings duplicados y basura semántica

La búsqueda vectorial amplifica la calidad del contenido. Si generas embeddings sobre textos pobres, duplicados, genéricos o mal normalizados, el índice devolverá resultados pobres, duplicados, genéricos o mal normalizados. No hay misterio. El embedding no convierte documentación basura en conocimiento. Solo la representa numéricamente con mucha dignidad matemática.

Microsoft recomienda evitar conjuntos con alta proporción de embeddings duplicados, porque perjudican la calidad de resultados, desplazan vecinos más útiles y consumen recursos sin aportar valor.

Esto se ve mucho en bases de conocimiento donde múltiples artículos empiezan con la misma plantilla corporativa. “Estimado usuario, este documento describe el procedimiento para resolver incidencias relacionadas con…” Si generamos embeddings sobre todo el texto sin ponderar ni limpiar, la plantilla común puede contaminar la similitud. Después nos preguntamos por qué todos los documentos parecen iguales. Quizá porque hemos metido el mismo ruido en todos. Sorpresón.

Hay que decidir qué texto se embebe. No siempre interesa usar todo el documento. A veces conviene usar título, resumen, etiquetas, producto, síntomas y causa raíz. En otros casos interesa trocear documentos largos en fragmentos y buscar sobre chunks, no sobre documentos completos. Si un artículo de diez páginas contiene una sección relevante en la página ocho, el embedding global puede diluirla.

También hay que versionar el modelo de embeddings. Si hoy generamos vectores con un modelo y mañana con otro, no debemos mezclar ambos alegremente en la misma columna sin saber qué estamos haciendo. Las distancias entre vectores generados por modelos distintos pueden dejar de tener sentido. Como mínimo, guardaría ModeloEmbedding, FechaEmbedding, HashContenido y estado de generación.

Un diseño más serio podría separar documento y fragmentos:

Este modelo permite buscar fragmentos, no solo documentos. Después podemos agregar resultados por DocumentoID, aplicar RRF sobre fragmentos y mostrar el documento con el fragmento más relevante. Es más trabajo, claro. También es más útil. La alternativa es devolver documentos enormes y confiar en que el usuario encuentre el párrafo bueno. Eso no es buscador; es externalizar el problema.

Reordenación por documento cuando trabajamos con fragmentos

Cuando buscamos sobre fragmentos, aparece un problema adicional. Un documento largo puede tener muchos fragmentos y, por tanto, muchas oportunidades de aparecer. Si no controlamos esto, los documentos extensos dominan el ranking simplemente porque tienen más boletos. Es el mismo vicio de siempre, confundir cantidad con relevancia.

Una estrategia razonable consiste en fusionar primero a nivel de fragmento y después colapsar por documento, quedándonos con la mejor puntuación o aplicando una agregación controlada. Por ejemplo:

Aquí MAX(PuntuacionRRF) evita que un documento se dispare solo por tener muchos fragmentos moderadamente relevantes. El contador de fragmentos coincidentes puede servir como desempate suave, pero no debería dominar la ordenación. Si queremos algo más sofisticado, podemos usar una combinación entre mejor fragmento, número de fragmentos relevantes y diversidad de secciones.

En sistemas RAG, este patrón es especialmente útil. El buscador no solo decide qué documento mostrar; decide qué fragmento alimentar al modelo generativo. Si metemos fragmentos malos en el contexto, el modelo responderá con seguridad y poca vergüenza. La seguridad sin fundamento ya la conocemos de algunas reuniones de arquitectura, pero no hace falta automatizarla.

También podemos añadir filtros de seguridad y visibilidad antes de la búsqueda. Si un usuario no tiene permiso sobre un documento, ese documento no debe aparecer ni por semántica ni por full-text. Esto parece obvio hasta que alguien implementa el filtro después de generar el contexto para el modelo. 

Seguridad, permisos y fuga semántica

La búsqueda híbrida no solo afecta al rendimiento. También afecta a la seguridad. Un embedding puede codificar información sensible del texto original. No es una copia literal, pero tampoco deberíamos tratarlo como un dato inocente. Si generamos embeddings de documentos confidenciales y permitimos búsquedas semánticas sin aplicar permisos correctamente, podemos filtrar conocimiento aunque no devolvamos el texto completo.

La seguridad debe aplicarse antes de formar candidatos o durante el proceso de búsqueda, no al final de manera cosmética. En VECTOR_SEARCH, con índices recientes y filtrado iterativo, los predicados pueden aplicarse durante la búsqueda vectorial. Eso ayuda, pero no sustituye un diseño de seguridad correcto.

En SQL Server podemos resolver parte del problema con predicados relacionales, Row-Level Security o filtros explícitos por tenant, organización, rol o ámbito documental. Lo importante es que la rama léxica y la rama semántica usen exactamente la misma lógica de visibilidad. Si full-text filtra por permisos y vector no, la fusión RRF puede devolver identificadores que la rama textual jamás habría permitido. Y entonces empieza el baile.

También hay que controlar quién puede generar embeddings. Si usamos AI_GENERATE_EMBEDDINGS, debemos revisar modelo, permisos, configuración del servicio y trazabilidad. Si los generamos fuera y los cargamos en SQL Server, debemos validar dimensiones, formato, modelo, nulos y correspondencia con el texto original. Un embedding desalineado con su documento es una mentira muy rápida de consultar.

En entornos multi-tenant, yo evitaría búsquedas vectoriales globales salvo que el aislamiento esté muy claro. Filtrar por tenant no es opcional. Y si el volumen por tenant permite búsqueda exacta con VECTOR_DISTANCE, quizá esa sea la opción más controlable. No todo problema necesita ANN global. A veces la arquitectura sencilla gana porque tiene menos formas de fallar.

La seguridad no debe aparecer como epígrafe al final del proyecto. Si el buscador ya está construido y alguien pregunta “¿cómo filtramos por permisos?”, lo más probable es que la respuesta correcta sea “reescribiendo más de lo que te gustaría”.

Observabilidad: medir relevancia y medir coste

Una búsqueda híbrida tiene dos planos de observabilidad. El primero es técnico: duración, lecturas, CPU, memoria, spills, número de candidatos por rama, uso de índice vectorial, uso de índice full-text, plan elegido, cardinalidades estimadas y reales. El segundo es funcional: consultas reales, documentos mostrados, documentos abiertos, posición del clic, reformulaciones y satisfacción del usuario.

Si solo medimos rendimiento, podemos construir un buscador rapidísimo que devuelve basura. Si solo medimos relevancia, podemos construir un buscador magnífico que tumba producción cada vez que se usa. Ninguna de las dos cosas debería darnos orgullo profesional.

A nivel técnico, conviene registrar para cada búsqueda el texto normalizado, tamaño de candidatos léxico, tamaño de candidatos semántico, top_final, valor de k, pesos aplicados, duración total y duración por rama si separamos ejecución. También podemos guardar si la consulta se trató como “literal crítica” o “semántica dominante”. Esta trazabilidad permite ajustar sin adivinar.

A nivel funcional, el dato más valioso suele ser qué resultado acaba usando el usuario. En soporte, podemos vincular búsqueda con ticket resuelto o artículo seleccionado. Si es documentación interna, podemos medir apertura y permanencia. En buscadores de catálogo, conversión o selección. La relevancia real no siempre coincide con nuestra intuición, especialmente cuando los usuarios escriben cosas que harían llorar a un parser.

También hay que comparar full-text puro, vector puro e híbrido. No para hacer una gráfica bonita, sino para demostrar que la complejidad añadida aporta valor. Si la híbrida no mejora precisión, recall o tiempo de resolución, quizá estamos decorando arquitectura. Y la decoración en producción suele ser cara.

Un enfoque serio consiste en crear un conjunto de consultas de evaluación con resultados esperados. No tiene que ser perfecto. Basta con consultas representativas, incluyendo códigos exactos, frases ambiguas, sinónimos, errores ortográficos, consultas largas y consultas absurdamente cortas. Sobre ese conjunto podemos probar distintos pesos, tamaños de ventana y valores de k.

Sin evaluación, la búsqueda híbrida se convierte en “a mí me parece que va mejor”. Esa frase ha causado más daño que algunos NOLOCK.

Casos donde no usaría búsqueda híbrida

No todo buscador necesita híbrida. Si el usuario busca exclusivamente códigos, referencias exactas, identificadores legales o claves de producto, full-text o incluso índices relacionales bien diseñados pueden bastar. Meter embeddings ahí puede añadir ruido. Una búsqueda de Factura 2024-000834 no necesita poesía semántica.

Tampoco usaría híbrida como primera respuesta a un problema de contenido mal estructurado. Si los documentos no tienen títulos decentes, categorías, producto, idioma, fecha, permisos o estado, la búsqueda híbrida no arreglará el desastre. Solo lo hará más caro. Primero se ordena el contenido; después se mejora la recuperación.

En tablas pequeñas, evitaría crear índices vectoriales salvo que exista una razón clara. Microsoft indica que VECTOR_SEARCH puede funcionar sin índice mediante escaneo brute-force, aunque el rendimiento se degrada con datasets grandes. Para desarrollo, pruebas y conjuntos pequeños, esto puede ser suficiente.

También sería prudente con cargas masivas frecuentes. Si regeneramos embeddings continuamente, cambiamos modelos a menudo o reemplazamos gran parte del dataset, el coste de mantenimiento y reconstrucción puede pesar más que la ventaja de ANN. En esos casos hay que diseñar ventanas de carga, staging, versiones de índice y validaciones. No conviene descubrirlo durante una migración nocturna con tres personas mirando una barra de progreso.

Por último, no usaría híbrida sin una estrategia de seguridad clara. La búsqueda semántica puede recuperar contenido inesperado porque no depende de palabras exactas. Eso es precisamente lo que la hace potente. También es lo que la hace peligrosa si los permisos se aplican tarde o mal.

El mejor buscador no es el que usa más tecnologías. Es el que devuelve lo correcto, rápido, dentro de permisos y con un coste operativo razonable. Parece una obviedad, pero hay arquitecturas enteras construidas para demostrar lo contrario.

Buenas prácticas para llevarlo a producción

La primera buena práctica es separar claramente recuperación y reordenación. La rama full-text recupera candidatos léxicos. La rama vectorial recupera candidatos semánticos. RRF fusiona posiciones. Si mezclamos estas fases sin orden, acabaremos con consultas imposibles de depurar.

La segunda es limitar candidatos por rama. Ni full-text ni vector deberían devolver volúmenes masivos si el resultado final tendrá 20 filas. top_n_by_rank en CONTAINSTABLE y TOP (N) WITH APPROXIMATE en VECTOR_SEARCH son herramientas básicas para controlar el tamaño del problema.

La tercera es conservar filtros relacionales fuertes. Cliente, tenant, producto, estado, idioma, fecha y permisos deben formar parte del diseño. No todo debe resolverse por similitud. A veces una igualdad bien indexada vale más que un grafo vectorial entero. No queda tan bien en una presentación, pero ejecuta mejor.

La cuarta es versionar embeddings. Guarda modelo, fecha, hash del texto y estado de generación. Si cambias el texto, regenera el vector. Cuando cambies el modelo, no mezcles vectores sin control. Si falla la generación, no insertes basura silenciosa. La base de datos recordará tu negligencia con una precisión admirable.

La quinta es medir calidad, no solo latencia. Registra consultas reales y resultados usados. Ajusta pesos y ventanas con datos. RRF funciona muy bien como punto de partida, pero cada dominio tiene sus rarezas. En soporte técnico, los literales pesan mucho. Si es búsqueda documental, la semántica puede dominar. En comercio, atributos estructurados y disponibilidad pueden importar más que ambas.

La sexta es revisar versión y comportamiento de los índices vectoriales. Los índices antiguos tienen limitaciones distintas a los recientes. La sintaxis con TOP_N queda obsoleta para índices nuevos, mientras que SELECT TOP (N) WITH APPROXIMATE es el patrón actual documentado para la búsqueda aproximada con índices recientes.

La séptima es preparar despliegues y mantenimiento. Si usas DacPac, BACPAC, replicación, particionado o cargas masivas, revisa restricciones antes de comprometer la arquitectura. Los índices vectoriales no son invisibles para la operación diaria. El DBA va a convivir con ellos, y conviene que no sea en régimen de sorpresa permanente.

Todo esto suena menos emocionante que “hemos metido IA en el buscador”. También es bastante más probable que funcione.

Conclusión

La búsqueda híbrida no sustituye al full-text ni convierte la búsqueda vectorial en una solución universal. Lo interesante aparece precisamente al combinar ambas señales con criterio, full-text aporta precisión léxica, los vectores aportan contexto semántico y RRF permite fusionar resultados sin mezclar puntuaciones incompatibles.

El diseño correcto no consiste en lanzar dos consultas y rezar. Hay que limitar candidatos, aplicar filtros relacionales, elegir entre búsqueda exacta y aproximada, revisar versiones de índice, controlar permisos, medir relevancia y entender el coste operativo. La parte bonita de la demo dura diez minutos. La parte seria vive dia a dia en producción.

SQL Server está incorporando capacidades muy potentes para búsqueda semántica e híbrida, pero potencia no significa simplicidad. Un índice vectorial mal usado, una rama full-text sin límite o una fusión de puntuaciones mal planteada pueden convertir una gran idea en otra incidencia con prioridad alta.

La búsqueda híbrida merece sitio en arquitecturas modernas, especialmente en bases de conocimiento, soporte, documentación interna y sistemas RAG. Pero exige DBAs que entiendan el motor, no operadores que copien sintaxis de una demo. Como casi siempre, la diferencia entre una solución elegante y una chapuza cara está en los detalles. Y los detalles, por suerte o por desgracia, siguen sin indexarse solos.

 

Publicado por Roberto Carrancio en Cloud, SQL Server, 0 comentarios
DiskANN: búsqueda vectorial en SQL Server 2025

DiskANN: búsqueda vectorial en SQL Server 2025

SQL Server 2025 ya no mira los vectores desde la barrera. Tenemos tipo vector, búsqueda exacta con VECTOR_DISTANCE y búsqueda aproximada con CREATE VECTOR INDEX y VECTOR_SEARCH, todo ello sobre una implementación que Microsoft vincula de forma explícita con DiskANN. No es una curiosidad de laboratorio ni una feature ornamental para demos con palabras como “semántico” y “copilot” repetidas ocho veces por minuto. Es una apuesta técnica real, todavía con partes en preview, pero real.

Ahora bien, una cosa es que el motor ya hable vectores y otra muy distinta que haya dejado de ser SQL Server. Cuando mezclamos embeddings con filtros relacionales, claves, joins y diseño físico serio, aparecen las costuras. Y eso es precisamente lo interesante. Porque el problema no está en que exista DiskANN, sino en cómo aterriza dentro de un motor que no nació para resolver búsquedas de proximidad semántica sobre datos de negocio.

Antes de entrar en el índice, conviene situar el problema. No para repetir una introducción de manual sobre IA, sino para dejar claro por qué esta funcionalidad no se comporta como un índice relacional clásico con mejor prensa.

Vectores, embeddings y kNN: no buscamos igualdad, buscamos cercanía

Un vector es una secuencia ordenada de números. En SQL Server se almacena con el tipo vector, que internamente usa un formato binario optimizado, aunque se presente como array JSON por comodidad. Cada dimensión ocupa 4 bytes en float32 y SQL Server 2025 añade soporte preview para float16, lo que abre una vía interesante para reducir espacio cuando la precisión lo permita.

Un embedding es un caso particular de vector: una representación numérica que intenta capturar características relevantes de un texto, una imagen o cualquier otro objeto. La consecuencia práctica es que dejamos de preguntar “¿es igual?” o “¿está entre A y B?” y pasamos a preguntar “¿qué está cerca de esto?”. Ese cambio parece pequeño hasta que toca ejecutar consultas. Entonces deja de ser filosofía y pasa a ser coste.

Cuando resolvemos el problema de forma exacta, hacemos un kNN exacto: calculamos distancia contra todos los candidatos y nos quedamos con los más próximos. Microsoft lo describe así en su documentación y, además, deja una recomendación bastante útil: la búsqueda exacta sigue teniendo sentido cuando el conjunto efectivo a revisar es pequeño, con una orientación general de unas 50.000 filas o menos después de aplicar predicados. Ese matiz importa mucho, porque evita uno de los errores más comunes: asumir que lo aproximado siempre es mejor solo porque tiene nombre moderno.

Hasta aquí el problema. La siguiente pregunta es evidente: cuando el volumen crece, ¿cómo evitamos que cada consulta termine pareciendo una inspección completa del universo conocido?

DiskANN: el “cómo” de Microsoft para escalar búsqueda vectorial

La respuesta de Microsoft para búsqueda aproximada es DiskANN. Conviene decirlo bien para no arrastrar una imagen mental equivocada: DiskANN no ordena vectores como si fuera un B-tree extraño. Es una familia de algoritmos de ANNS basada en grafos, pensada para combinar precisión razonable, baja latencia y uso eficiente de memoria y SSD. Microsoft Research describe Project Akupara precisamente como una línea de trabajo orientada a escalar ANNS para búsqueda y recomendación a gran escala, y la publicación original de 2019 presenta DiskANN como un sistema capaz de indexar y buscar mil millones de puntos en una sola máquina usando 64 GB de RAM y SSD.

La gracia de ese enfoque no es solo académica. Microsoft ha ido consolidando DiskANN como base tecnológica en su ecosistema, y SQL Server 2025 lo expone directamente en CREATE VECTOR INDEX con TYPE = ‘DISKANN’. En otras palabras, no estamos ante una abstracción vaporosa; el motor te deja tocarla. Y eso siempre es más útil que una promesa de roadmap adornada con iconos de nube.

Pero aquí aparece el primer matiz importante para cualquiera que haya peleado con diseño físico de tablas. Que SQL Server pueda almacenar vectores no significa que debamos meterlos sin pensar en la tabla principal, como si una fila de negocio de 100 bytes y un embedding de 1.536 dimensiones fuesen vecinos naturales. No lo son.

La decisión de modelado que más sentido tiene

Mi recomendación sigue siendo separar los embeddings en una tabla satélite 1:1 respecto a la tabla relacional principal. No por postureo arquitectónico, sino porque el coste físico de mezclar ambas cosas en la misma fila es demasiado alto para fingir que no existe. Microsoft lo explica con un ejemplo muy claro en su FAQ: una página de datos admite hasta 8.060 bytes (8 Kb) y un vector de 1.024 dimensiones en float32 ocupa 4.104 bytes contando cabecera, lo que ya limita a un solo vector por página. Si subimos dimensiones, el efecto no mejora precisamente.

Eso no significa solo “más fragmentación”, que es la forma rápida de contarlo. Significa también menos densidad de página, más I/O, peor aprovechamiento de caché y más penalización para accesos relacionales que no necesitan tocar el embedding. La guía de diseño de índices de SQL Server insiste en mantener los índices, en especial el clustered, lo más estrechos posible. Y aquí conviene recordar algo bastante básico, si la tabla principal sirve para OLTP o para consultas relacionales normales, meter una columna vectorial enorme ahí dentro porque “ya la usamos en algunas búsquedas” es una forma muy creativa de castigar todo lo demás.

Hay además un segundo argumento, todavía más relevante porque los índices vectoriales aproximados y VECTOR_SEARCH siguen en preview dentro de SQL Server 2025. La tabla que tiene un vector index debe tener una PK clustered entera de una sola columna y, mientras el índice exista, esa tabla queda de solo lectura. En SQL Server 2025 no está disponible ALLOW_STALE_VECTOR_INDEX, así que si cambian los datos toca recrear el índice. Separar embeddings en una tabla 1:1 aísla esa rigidez donde corresponde y evita congelar la tabla relacional principal.

De hecho, el propio material de Microsoft ya deja entrever ese patrón. En los ejemplos de VECTOR_SEARCH aparece una tabla llamada wikipedia_articles_embeddings, lo que sugiere un diseño separado para los vectores frente al contenido relacional o documental. No es una norma escrita en piedra, pero tampoco parece casualidad.

Una vez aceptado ese modelo, la consulta cambia. Y ahí es donde aparece la parte realmente delicada: el JOIN mejora el diseño físico, pero no convierte mágicamente la búsqueda vectorial en algo filter-aware.

¿Qué cambia en la consulta cuando el embedding vive fuera?

Con un diseño 1:1, lo lógico es que VECTOR_SEARCH se ejecute sobre la tabla de embeddings y luego se haga JOIN con la tabla relacional para recuperar atributos de negocio y aplicar filtros. Ese patrón encaja perfectamente con el estado actual del producto, porque VECTOR_SEARCH solo admite tablas base; no puede apuntar a una vista ni a una tabla temporal intermedia ya filtrada. Además, si encuentra un índice ANN compatible en esa columna y con la misma métrica, lo usa; si no, vuelve a kNN.

La consulta natural sería algo así:

Esto está bien desde el punto de vista del modelo físico. La tabla principal sigue estrecha, el índice vectorial vive donde debe y el JOIN 1:1, sobre una clave entera, normalmente será barato. Lo que no cambia es el orden lógico de la operación ANN respecto a los predicados de negocio. Y ahí vuelve a entrar el jarro de agua fría.

El JOIN no arregla el post-filter: solo mueve la frontera

La documentación de VECTOR_SEARCH es bastante clara, la búsqueda vectorial ocurre antes de aplicar cualquier predicado, y los filtros adicionales se evalúan después de devolver los vecinos más similares. Microsoft lo llama post-filter only. Si pides TOP_N = 10 y luego filtras por TenantId o IsActive, puedes acabar con menos de 10 filas o incluso con ninguna. Eso vale igual si el filtro cae sobre la misma tabla que contiene el embedding o si llega desde una tabla relacionada mediante JOIN. El JOIN no cambia esa semántica.

Eso significa que separar embeddings sí mejora el diseño físico, pero no resuelve la principal limitación actual del ANN en SQL Server 2025. El trabajo más caro sigue siendo la navegación del índice DiskANN sobre el conjunto indexado. Si después un filtro de negocio es muy selectivo, la forma de compensarlo suele ser sobremuestrear, subiendo TOP_N para tener suficientes candidatos válidos tras el JOIN y el WHERE. Y cuanto más selectivo sea el filtro, más se erosiona la ventaja del índice aproximado.

Aquí conviene evitar una confusión habitual. El problema no es que el JOIN sea especialmente caro. En una relación 1:1 bien indexada, no suele serlo. El problema es que el filtro relacional llega tarde para ayudar a la fase ANN. Y eso no lo arregla un buen modelo lógico, porque es una consecuencia de cómo está implementado el acceso vectorial hoy en el motor.

La pregunta entonces ya no es si separar la tabla, que para mí sí, sino cuándo conviene dejar de insistir con ANN y volver, sin complejos, a la búsqueda exacta.

¿Por qué no puedes filtrar antes sin romper algo?

La base técnica está bien documentada en la investigación de Microsoft sobre Filtered-DiskANN. El problema de los filtros en ANNS no se resuelve bien tocando solo la fase de búsqueda. El paper critica precisamente el enfoque de postprocesado, porque para filtros de baja especificidad puede obligar a recuperar muchísimos candidatos antes de encontrar uno que cumpla el predicado. Pero también deja claro que la solución trivial de “un índice por filtro” no escala.

La razón de fondo es que un índice tipo DiskANN es un grafo global construido sobre todos los vectores. Durante la búsqueda, algunos nodos no son resultados finales interesantes, pero sí son cruciales como puentes para navegar hacia la zona correcta del espacio vectorial. Si yo aplico un filtro arbitrario antes de buscar y elimino parte de esos nodos, puedo romper la navegabilidad efectiva del grafo. Dicho de otra manera, el subconjunto filtrado no conserva necesariamente las propiedades del índice original. Y eso afecta al recall, al coste o a ambas cosas.

Por eso filtered ANN de verdad no consiste en “empujar el WHERE hacia abajo” y quedarse tan ancho. Requiere índices diseñados con consciencia de filtro, estrategias de construcción distintas o, al menos, estructuras auxiliares que permitan no destrozar el recorrido. Ahí es donde la investigación va por delante de la implementación actual del motor. Y sinceramente, mejor admitirlo que fingir que un predicado sobre TenantId va a integrarse solo por buena voluntad. También podríamos confiar en MERGE en producción, pero cada uno gestiona sus traumas como puede.

A partir de aquí, la pregunta útil deja de ser “siempre ANN o siempre exacto” y pasa a ser mucho más adulta: “¿cuándo me compensa cada enfoque en SQL Server 2025?”.

¿Cuándo DiskANN tiene sentido y cuándo es mejor volver a VECTOR_DISTANCE?

Si el corpus es grande y los filtros relacionales son débiles, VECTOR_SEARCH con DiskANN es la opción natural. Para eso existe, para reducir latencia y coste frente a un barrido completo. Pero si el patrón real de consulta siempre filtra fuerte por cliente, estado, región o cualquier dimensión de negocio que reduzca mucho el conjunto efectivo, entonces la recomendación oficial de Microsoft cobra todo el sentido del mundo: cuando acabas buscando sobre 50.000 vectores o menos, la búsqueda exacta con VECTOR_DISTANCE vuelve a ser una alternativa muy seria.

Y aquí, precisamente, la separación 1:1 juega a favor. Puedes filtrar primero en la tabla relacional, quedarte con el subconjunto de negocio que realmente importa, hacer el JOIN hacia la tabla de embeddings y calcular distancias solo sobre esos candidatos. Es una estrategia mucho más alineada con el optimizador relacional clásico y, además, evita pedirle a un índice ANN global que resuelva con elegancia algo para lo que no ha sido diseñado. Sí, podemos pedirle heroicidades a cualquier índice. También podemos confiar en que un SELECT * no acabará mal. Pero a cierta altura de la película conviene distinguir entre esperanza y diseño.

Conclusión

SQL Server 2025 ha dado un paso importante con búsqueda vectorial nativa y DiskANN como base técnica para ANN. Eso merece atención, pruebas serias y bastante menos folklore del habitual. Pero también conviene poner cada pieza en su sitio. El mejor diseño, en la práctica, pasa por separar embeddings en una tabla 1:1, mantener la tabla relacional principal estrecha y aislar ahí las restricciones operativas del vector index. Eso es buena ingeniería de datos, no una preferencia estética.

Ahora bien, ese diseño no convierte VECTOR_SEARCH en filtered ANN. Hoy seguimos teniendo ANN global más post-filter relacional, aunque el filtro llegue por JOIN. Esa diferencia condiciona rendimiento, recall y criterio de uso. Entenderla no es un detalle menor. Es la diferencia entre usar la novedad con cabeza o descubrir, demasiado tarde, que el motor no estaba haciendo exactamente lo que tú dabas por supuesto. Y en bases de datos, como sabemos todos, dar cosas por supuestas suele salir carísimo.

Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de LinkedIn y un canal de YouTube a los que te puede unir. ¡Hasta la próxima!

Publicado por Roberto Carrancio en Cloud, Índices, Rendimiento, SQL Server, 0 comentarios

Sargabilidad: el arte de no cabrear al optimizador

En SQL Server hay conceptos que separan al junior entusiasta del profesional que ha pasado noches enteras mirando planes de ejecución con el ceño fruncido. La sargabilidad es uno de ellos. Es esa palabra fea que no encontrarás en la RAE, pero que puede hacer que tus consultas vuelen… o se arrastren como un SELECT * sin índice por una tabla de 500 millones de filas.

Si llevas tiempo escribiendo T-SQL y nunca te has parado a pensar en si tus consultas son sargables o no, probablemente estés dejando rendimiento encima de la mesa. Y si trabajas con datos en serio, sabes que eso es inaceptable.

Qué es la sargabilidad (de verdad)

Sargabilidad viene del acrónimo SARG: Search ARGument-able. Es decir, que el motor de base de datos puede usar un índice para resolver la consulta de forma eficiente, sin escanear toda la tabla como si no hubiera mañana.

En SQL Server, una consulta es sargable cuando el predicado (la condición del WHERE, JOIN, HAVING, etc.) puede aprovechar un índice para filtrar datos directamente, sin tener que procesar fila por fila. En otras palabras: si el optimizador ve que puede usar un índice de forma directa, lo hará. Si no, te prepara un bonito Index Scan o Table Scan con aroma a cuello de botella.

Un ejemplo básico:

  • Sargable
  • No sargable

La diferencia es simple pero letal. En el primer caso, SQL Server puede usar un índice sobre la columna Nombre. En el segundo, como le has puesto una función encima, el optimizador no puede buscar directamente. Tiene que aplicar la función a cada fila y luego comparar. Y claro, eso escala tan bien como un script que hace DELETE sin WHERE.

Las funciones son el enemigo (cuando no sabes usarlas)

El caso anterior ilustra la regla de oro de la sargabilidad: si transformas la columna del predicado, estás condenado a un escaneo. Las funciones como LEFT, SUBSTRING, CONVERT, DATEDIFF, COALESCE o incluso simples operadores matemáticos, rompen la posibilidad de usar índices si se aplican sobre la columna.

Veamos otro clásico:

Esto es tan común como ineficiente. Lo correcto sería:

Y sí, es más feo. Pero el plan de ejecución será infinitamente más feliz. Y tú también, cuando no tengas que explicar por qué la consulta tarda 30 segundos con un índice que, supuestamente, estaba para eso.

ISNULL y COALESCE: los rompeíndices silenciosos

Uno de esos consejos que se repite como mantra en el mundo SQL es: «no uses funciones en columnas si quieres mantener la sargabilidad». Y, como todo buen mantra, es útil… pero no siempre cierto.

Tomemos el caso de ISNULL() y COALESCE(). Ambos permiten sustituir valores nulos por un valor por defecto. A efectos prácticos:

Parecen lo mismo, ¿no? Pero el optimizador de SQL Server no los trata igual. En la demo real que estás viendo, con un índice sobre DisplayName, la diferencia es cristalina.

ISNULL(): sorprendentemente sargable

Cuando usas ISNULL(DisplayName, ») = ‘Juan’, SQL Server aplica un Index Seek sin despeinarse. No le molesta la función. ¿Por qué? Porque el motor puede predecir exactamente lo que hace ISNULL() en este contexto: evalúa la columna, y si es NULL, la reemplaza por ». Como el predicado sigue siendo evaluable con un valor constante, puede derivarlo internamente como un predicado OR:

Y esa es la clave. Porque si escribes ese predicado tú directamente, es igual de eficiente y, de paso, más claro:

Mismo Index Seek. Misma eficiencia. Pero sin necesidad de funciones.

COALESCE(): aquí sí se complica

Ahora bien, si usas COALESCE(DisplayName, ») = ‘Juan’, el plan cambia. SQL Server ya no puede garantizar que la expresión sea determinista y sencilla de evaluar. Más que nada porque podría haber más de dos argumentos o que el orden de evaluación tuviera implicaciones semánticas. El resultado: Index Scan.

Así que no, ISNULL() y COALESCE() no son intercambiables en el WHERE (importante esto) cuando te importa el rendimiento. En teoría devuelven lo mismo, en la práctica afectan al plan de ejecución de formas distintas.

El OR: ese sospechoso habitual

Otro rompe-sargabilidad por excelencia es el OR, que puede obligar a escanear incluso si una de las condiciones sí es sargable. Imagina esto:

A menos que tengas un índice compuesto que cubra ambas columnas, el optimizador suele rendirse y tirar de escaneo. 

Así que ya sabes, si usas con frecuencia este tipo de filtros con OR asegurate de tener un índice compuesto bien creado.

Las columnas calculadas como tabla de salvación

A veces no hay forma humana de evitar una función sobre la columna. En esos casos, una solución decente (aunque no milagrosa) puede ser usar columnas calculadas indexadas. Si sabes que todo el mundo va a seguir escribiendo:

Puedes añadir una columna calculada persistente:

Y ahora sí, el predicado puede ser sargable, porque la función ya se calculó y se indexó. No es magia negra, es simplemente darle al optimizador algo que pueda usar sin pensar.

El caso especial del LIKE

El LIKE también puede ser sargable o no, dependiendo del patrón. Esta es sargable:

Esta no:

Si el comodín está al principio, el índice no sirve. No puede buscar «desde» ningún valor. El patrón tiene que permitir una búsqueda por rango, como ‘Juan%’ o incluso ‘J%’. Todo lo demás se traduce en escaneo.

Aquí no hay mucha escapatoria salvo usar Full-Text Search si de verdad necesitas búsquedas internas. O pensar si realmente necesitas buscar así, que a veces el problema no es técnico sino de requisitos mal planteados.

Cuando CONVERT(DATE, datetime) sí es sargable (sí, lo has leído bien)

Aunque llevamos un buen rato diciendo, con razón, que aplicar funciones sobre columnas rompe la sargabilidad, hay una excepción curiosa (y bastante útil) que merece su propio rincón en este artículo: la conversión de un datetime a DATE usando CONVERT o CAST.

Mira este caso donde CreationDate es un campo DATETIME:

Lo lógico sería pensar: “acabo de meterle una función a la columna del WHERE, esto va directo a un Index Scan, ¿verdad?”. Pues no. SQL Server es capaz de usar un Index Seek, incluso con esa conversión.

¿Por qué? Porque el motor es más listo de lo que a veces creemos. Internamente, evalúa que convertir un datetime a date reduce la precisión pero no cambia la semántica de ordenación, y por tanto puede generar un predicado de rango equivalente: desde 2010-06-05 00:00:00.000 hasta justo antes del 2010-06-06. Si hay un índice sobre la columna CreationDate, lo usa. Sin mirar atrás.

Ahora bien: esto no lo convierte en buena práctica universal.

Porque si haces esto:

El motor no te va a perdonar. Ahí sí hay Index Scan, porque ya estás forzando una conversión de tipo y de formato, y encima con ambigüedad regional. No hay forma de hacer un seek con eso.

Así que, como en todo, hay que ser precisos: CONVERT(DATE, datetime) es una excepción válida a la regla general, y SQL Server lo optimiza sin necesidad de escanear. Pero eso no quiere decir que cualquier CONVERT o CAST sea seguro.

JOINs y sargabilidad: el otro lado del desastre

La sargabilidad no solo importa en los WHERE. También afecta a los JOIN. Si haces cosas como:

Estás rompiendo toda posibilidad de usar un índice sobre B.Codigo. La función LEFT en el lado derecho del JOIN es como ponerle una venda en los ojos al optimizador. Y luego esperar que encuentre el camino solo. Mejor no.

Reescribe la lógica o introduce columnas auxiliares si hace falta, pero no pongas funciones en las condiciones de unión. A menos que tu hobby sea revisar planes de ejecución a las tres de la mañana.

Cómo detectar consultas no sargables

El mejor chivato es el plan de ejecución. Si ves Index Scan o Table Scan donde debería haber un Seek, es que tienes un problema de sargabilidad. También puedes usar las estadísticas de ejecución o SET STATISTICS IO ON para ver si se están leyendo más páginas de las que tocan.

Además, en SQL Server 2019+ puedes usar Intelligent Query Processing que, en algunos casos, mitiga errores de diseño como este… pero no es una excusa para escribir mal. Es un parche, no una solución.

Y si quieres una ayuda más visual, herramientas como SQL Sentry Plan Explorer o el propio SSMS con su plan gráfico te permiten ver rápidamente si estás tirando índices a la basura.

Scan no siempre es sinónimo de error (aunque duela admitirlo)

Otro punto clave: un Index Scan no siempre es el villano. Sí, lo hemos demonizado durante años, pero como todo DBA que ha peleado con planes de ejecución sabe, hay ocasiones en las que el escaneo es simplemente lo más eficiente.

Por ejemplo, si tu predicado devuelve un porcentaje alto de filas (pongamos, más del 25-30% de la tabla), al optimizador le puede salir más a cuenta ir a buscar los datos de una vez que intentar ser selectivo con un Seek y acabar con mil Key Lookup de regalo.

Lo mismo pasa cuando el índice cubre poco y el motor tendría que hacer lookups constantes para recoger el resto de columnas. En ese caso, el coste de los seeks individuales más los lookups supera con creces el coste de un buen escaneo. El optimizador no es tonto: elige lo que más conviene a nivel global de coste estimado.

Y aquí viene lo peligroso: si tu consulta está perfectamente escrita y sargable, pero el plan muestra un Scan, no asumas automáticamente que está mal. Revisa el plan, el número de filas estimadas, y el acceso a columnas. Lo que queremos evitar son los scans necesarios por diseño deficiente, no los que el motor elige porque son lo más razonable.

Conclusión

La sargabilidad no es un capricho del optimizador ni un tema menor. Es la base sobre la que se construye el rendimiento de cualquier sistema OLTP que merezca la pena. Ignorarla es como ir a una entrevista con el pantalón del pijama: técnicamente puedes, pero no deberías.

Escribir consultas sargables exige disciplina, conocimiento y cierta humildad. Hay que dejar de pensar en lo que «funciona» y empezar a pensar en lo que escala. Y cuando la diferencia entre una consulta sargable y otra que no lo es se mide en millones de lecturas lógicas… no hay excusa.

Así que la próxima vez que te enfrentes a un WHERE, piensa como el optimizador. No le pongas trabas, no le escondas la columna tras una función, y no le pidas milagros con predicados imposibles. Dale lo que necesita. Y si aún así se queja, entonces sí: culpemos al cardinality estimator.

Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de Telegram y un canal de YouTube a los que te puede unir. ¡Hasta la próxima! 

Publicado por Roberto Carrancio en Cloud, Índices, Rendimiento, SQL Server, 0 comentarios

El libro de SQL Server que estabas esperando

Después de más de una década trabajando con SQL Server y ahora compartiendo contenido en SoyDBA, estoy escribiendo un libro pensado para profesionales que quieren ir más allá de la documentación oficial. Un libro técnico y muy didáctico.

Más de 40 capítulos en 7 partes, desde los fundamentos del modelo relacional hasta las herramientas de diagnóstico más avanzadas. Un recorrido completo, pensado para DBAs, analistas y desarrolladores que quieren entender cómo funciona SQL Server de verdad.

Portada Libro

El prólogo lo firma Fernando G. Guerrero, pionero y referente en nuestra comunidad. La contraportada, Juanjo Luna, MVP de Access y más apasionado de SQL de lo que quiere reconocer. Os dejo aquí el texto de la contraportada:

Si quieres conseguirlo corre a Amazón.

El libro está disponible en Amazon en formato papel y Kindle en todo el mundo.

Mantente al día de las novedades con mi newsletter gratuita

Logo SoyDBA

Únete a la newsletter de SoyDBA

Regístrate gratis para no perderte ninguna novedad. Te avisaré de noticias y eventos importantes

¡No hacemos spam! Lee nuestra política de privacidad para obtener más información.

Publicado por Roberto Carrancio en Alta Disponibilidad, Cloud, Índices, Rendimiento, SQL Server, 0 comentarios

¿Cuál es el problema que quieres resolver?

Si hay una pregunta que debería resonar en la cabeza de todo DBA antes de tocar nada es esta: ¿cuál es el problema que quieres resolver?. Parece de sentido común, pero basta con ver la cantidad de “optimizaciones” fallidas que nos encontramos a diario para entender que muchos prefieren actuar primero y pensar después.

No es cuestión de señalar a los juniors recién llegados. También he visto seniors con experiencia aplicar consejos a ciegas porque lo leyeron en un blog (sí, incluso en este). Optimizar sin tener claro el objetivo es la mejor forma de acabar con más índices de los que puedes contar y un servidor que corre peor que antes.

Cuando dos índices son un problema

Hace un tiempo publiqué un artículo en el blog que mostraba un comportamiento extraño en SQL Server: si creas dos índices con los mismos campos pero en orden inverso, el optimizador puede entrar en bucle y empeorar el rendimiento.

No es que uno de los índices fuera inútil, es que la mera coexistencia de ambos confundía al motor. El resultado eran planes de ejecución incoherentes, lecturas disparadas y un servidor que parecía castigar la osadía de haber creado ese segundo índice.

Lo comprobé en mi propio laboratorio y sí, el problema existe en distintas versiones, desde 2014 hasta 2022. No hay explicación oficial, pero el efecto es evidente: un índice que no se usa puede penalizar tanto como uno mal diseñado.

La moraleja es clara: no todo lo que parece buena idea lo es. Crear un índice extra “por probar” puede desatar un problema que no estaba ahí antes.

El optimizador también se equivoca

A ese escenario extraño se suma otro que me he encontrado demasiadas veces: el optimizador sugiriendo crear un índice que ya existe.

¿Cómo puede ser? Muy simple: lo que realmente falla son las estadísticas. Si los datos de distribución están obsoletos, el optimizador cree que no tiene un índice adecuado y recomienda uno nuevo. Y como “buenos DBAs obedientes”, lo creamos. Resultado: en el mejor de los casos acabamos con dos índices redundantes, el motor igual de confundido y el verdadero problema (las estadísticas) intacto. En el peor, con el problema que hemos descrito en el apartado anterior y con las estadísticas aún desactualizadas.

La historia se repite: en vez de preguntarnos qué queremos resolver, nos dejamos llevar por la sugerencia de turno y complicamos aún más el entorno.

Optimizar sin objetivo: el problema más común

Ambos casos ponen de manifiesto lo mismo: aplicar soluciones sin entender el problema real es un deporte de riesgo.

Ya lo comentamos en este otro artículo, los entornos de bases de datos no se arreglan con recetas universales ni con “scripts definitivos” de Internet. Cada entorno es distinto, con sus cargas, sus decisiones históricas y sus miserias. Copiar y pegar soluciones genéricas es más postureo técnico que administración seria.

La optimización sin objetivo es como disparar con los ojos cerrados, con suerte no das en nada, pero la mayor parte del tiempo acabas dañando algo.

La pregunta de Brent Ozar

Aquí entra la insistencia de Brent Ozar en su mantra favorito y que da titulo a este artículo:
¿Cuál es el problema que quieres resolver?

Antes de crear índices, actualizar estadísticas o aplicar hints a lo loco o cambiar configuraciones en el servidor hay que responder a esa pregunta. ¿El problema es el tiempo de respuesta? ¿Consumo excesivo de CPU? ¿Bloqueos? ¿O solo que alguien quiere que una consulta de 3 segundos baje a 0,3 porque sí?

Uno de esos es un problema técnico. El otro es un problema de expectativas. Y mezclar ambos lleva a dedicar horas a optimizar lo que no importa mientras lo urgente sigue sin atenderse.

Si no mides, no sabes si mejoras o empeoras

Aquí está el punto que muchos olvidan, si no sabes que tienes un problema y no mides antes, no puedes saber si lo que hiciste fue una mejora o un desastre.

Un cambio sin línea base no es optimización, es puro azar. Y en producción, jugar al azar es abrir la puerta a que mañana alguien te pregunte por qué el sistema está peor y no tengas respuesta. Sin métricas antes y después, la optimización es indistinguible del postureo.

Y ojo, también tienes que medir cuando todo funciona bien, sin esa linea base es imposible saber si de verdad algo va mal o es solo la sensación de los usuarios. Ya sabemos que adoran decir: “esto ahora va más lento” sin pruebas.

Diagnóstico antes de cirugía

La única forma de trabajar en serio es diagnosticar antes de tocar. Eso implica medir, observar y recopilar evidencia. Query Store, planes de ejecución, Extended Events, DMVs… las herramientas están ahí, pero hay que usarlas con criterio.

El motor se equivoca, los consejos de Internet también, y a veces hasta nuestras “buenas prácticas” son contextuales y no universales. La diferencia entre un DBA que arregla problemas y otro que los multiplica está en hacerse siempre la pregunta incómoda antes de optimizar.

Conclusión

Cambiar configuraciones del servidor, crear índices o aplicar consejos sin diagnosticar el problema real no es optimización, es azar.

SQL Server no premia las soluciones automáticas. Premia el análisis. Y la diferencia entre un DBA que arregla cosas y otro que las rompe está en hacerse siempre esa pregunta incómoda antes de tocar nada: ¿cuál es el problema que quiero resolver?

Si no sabes que tienes un problema y no lo mides antes, no tienes forma de saber si tu cambio fue mejora o desastre. Y en bases de datos, la duda casi siempre juega en tu contra.

Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de LinkedIn y un canal de YouTube a los que te puede unir. ¡Hasta la próxima!

Publicado por Roberto Carrancio en Cloud, Rendimiento, SQL Server, 0 comentarios

Novedades sobre el libro

Después de meses de trabajo, revisiones y mucha paciencia, puedo compartir tres novedades clave del libro SQL Server: La NO guía práctica de optimización:

  • El prólogo lo firma Fernando G. Guerrero, pionero en SQL Server y miembro histórico de la comunidad. Además de escribir unas palabras introductorias, ha contribuido en la revisión técnica.

  • Aquí está el índice completo, con los más de 40 capítulos organizados en 7 partes.

  • Y sí: la fecha de lanzamiento ya es oficial. El libro estará disponible a mediados de septiembre. Pronto os diré el día exacto

Índice completo

Parte 1 – Fundamentos del modelo relacional y la arquitectura
Desde la definición de base de datos relacional hasta la estructura física de páginas y archivos. El punto de partida sólido para entender qué hay detrás de cada consulta.

Parte 2 – T-SQL y construcción de consultas eficientes
SELECT, INSERT, UPDATE y DELETE como base, y a partir de ahí subconsultas, CTEs, funciones de ventana, vistas, procedimientos y ejecución de consultas. Todo lo que necesitas para escribir SQL que funcione en producción.

Parte 3 – Transacciones, concurrencia y aislamiento
Propiedades ACID, niveles de aislamiento, bloqueos, deadlocks y el nuevo modelo de bloqueos optimizados. Una sección clave para quien de verdad administre entornos críticos.

Parte 4 – Internals, configuración y mantenimiento
TempDB, almacenamiento, memoria, CPU, cardinalidad, parametrización, índices, estadísticas y particionado. La cocina interna del motor y cómo configurarlo sin hipotecar el rendimiento.

Parte 5 – Backup, recuperación y disponibilidad
Modelos de recuperación, estrategias de backup, restore, Log Shipping, Database Mirroring y Availability Groups. Todo lo que sostiene la continuidad de un entorno SQL Server serio.

Parte 6 – Seguridad y control de acceso
Principales, usuarios, roles, permisos y técnicas avanzadas de seguridad. Desde lo básico hasta RLS, cifrado y enmascaramiento de datos.

Parte 7 – Control, monitorización y diagnóstico
Herramientas internas y externas: Resource Governor, Activity Monitor, DMVs, Profiler, Extended Events, Query Store y PerfMon. Para que no solo administres, sino que entiendas qué ocurre bajo el capó.

Con este índice ya podéis ver que no es un manual de recetas rápidas, sino una guía estructurada de principio a fin para profesionales de SQL Server.

El lanzamiento será a mediados de septiembre y os avisaré en cuanto esté disponible en Amazon.

Publicado por Roberto Carrancio en Cloud, Rendimiento, SQL Server, 0 comentarios

El problema del Reenvío de punteros en tablas heap

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 en Cloud, Rendimiento, SQL Server, 0 comentarios