sql >> Base de Datos >  >> RDS >> Database

Malos hábitos:contar filas de la manera difícil

[Ver un índice de todas las publicaciones sobre malos hábitos/mejores prácticas]

Una de las diapositivas de mi presentación recurrente sobre malos hábitos y mejores prácticas se titula "Abuso de COUNT(*) ." Veo este abuso bastante en la naturaleza, y toma varias formas.

¿Cuántas filas hay en la tabla?

Suelo ver esto:

SELECT @count = COUNT(*) FROM dbo.tablename;

SQL Server tiene que ejecutar un análisis de bloqueo en toda la tabla para obtener este recuento. Eso es costoso. Esta información se almacena en las vistas de catálogo y DMV, y puede obtenerla sin todas esas E/S o bloqueos:

SELECT @count = SUM(p.rows)
  FROM sys.partitions AS p
  INNER JOIN sys.tables AS t
  ON p.[object_id] = t.[object_id]
  INNER JOIN sys.schemas AS s
  ON t.[schema_id] = s.[schema_id]
  WHERE p.index_id IN (0,1) -- heap or clustered index
  AND t.name = N'tablename'
  AND s.name = N'dbo';

(Puede obtener la misma información de sys.dm_db_partition_stats , pero en ese caso cambie p.rows a p.row_count (¡vaya consistencia!). De hecho, esta es la misma vista que sp_spaceused utiliza para derivar el conteo, y si bien es mucho más fácil de escribir que la consulta anterior, recomiendo no usarlo solo para derivar un conteo debido a todos los cálculos adicionales que realiza, a menos que también desee esa información. También tenga en cuenta que utiliza funciones de metadatos que no obedecen a su nivel de aislamiento externo, por lo que podría terminar esperando el bloqueo cuando llame a este procedimiento).

Ahora, es cierto que estas vistas no son 100% precisas al microsegundo. A menos que esté usando un montón, se puede obtener un resultado más confiable del sys.dm_db_index_physical_stats() columna record_count (¡vaya consistencia de nuevo!), sin embargo, esta función puede tener un impacto en el rendimiento, aún puede bloquear y puede ser incluso más costosa que SELECT COUNT(*) – tiene que hacer las mismas operaciones físicas, pero tiene que calcular información adicional dependiendo del mode (como la fragmentación, que no le importa en este caso). La advertencia en la documentación cuenta parte de la historia, relevante si está utilizando grupos de disponibilidad (y probablemente afecte a la creación de reflejo de la base de datos de manera similar):

Si consulta sys.dm_db_index_physical_stats en una instancia de servidor que aloja una réplica secundaria legible AlwaysOn, es posible que encuentre un problema de bloqueo de REDO. Esto se debe a que esta vista de administración dinámica adquiere un bloqueo IS en la vista o tabla de usuario especificada que puede bloquear las solicitudes de un subproceso REDO para un bloqueo X en esa vista o tabla de usuario.

La documentación también explica por qué este número podría no ser confiable para un montón (y también les da un cuasi-aprobado para la inconsistencia de filas frente a registros):

Para un montón, es posible que la cantidad de registros devueltos por esta función no coincida con la cantidad de filas que se devuelven al ejecutar SELECT COUNT(*) en el montón. Esto se debe a que una fila puede contener varios registros. Por ejemplo, en algunas situaciones de actualización, una única fila de almacenamiento dinámico puede tener un registro de reenvío y un registro reenviado como resultado de la operación de actualización. Además, la mayoría de las filas LOB grandes se dividen en varios registros en el almacenamiento LOB_DATA.

Así que me inclinaría por sys.partitions como la forma de optimizar esto, sacrificando un poco de precisión marginal.

    "Pero no puedo usar los DMV; ¡mi conteo debe ser muy preciso!"

    Un conteo "súper preciso" en realidad no tiene mucho sentido. Consideremos que su única opción para un conteo "superpreciso" es bloquear toda la tabla y prohibir que alguien agregue o elimine filas (pero sin evitar lecturas compartidas), por ejemplo:

    SELECT @count = COUNT(*) FROM dbo.table_name WITH (TABLOCK); -- not TABLOCKX!

    Por lo tanto, su consulta está zumbando, escaneando todos los datos, trabajando hacia ese conteo "perfecto". Mientras tanto, las solicitudes de escritura se bloquean y esperan. De repente, cuando se devuelve su recuento exacto, se liberan los bloqueos en la tabla y todas esas solicitudes de escritura que estaban en cola y esperando, comienzan a disparar todo tipo de inserciones, actualizaciones y eliminaciones en su tabla. ¿Qué tan "superpreciso" es su conteo ahora? ¿Valió la pena obtener un conteo "preciso" que ya está horriblemente obsoleto? Si el sistema no está ocupado, entonces esto no es un gran problema, pero si el sistema no está ocupado, diría con bastante fuerza que los DMV serán bastante precisos.

    Podrías haber usado NOLOCK en cambio, pero eso solo significa que los escritores pueden cambiar los datos mientras los está leyendo, y también genera otros problemas (hablé de esto recientemente). Está bien para muchos estadios de béisbol, pero no si su objetivo es la precisión. Los DMV estarán justo (o al menos mucho más cerca) en muchos escenarios, y más lejos en muy pocos (de hecho, ninguno que se me ocurra).

    Finalmente, puede usar el aislamiento de instantáneas confirmadas de lectura. Kendra Little tiene una publicación fantástica sobre los niveles de aislamiento de instantáneas, pero repetiré la lista de advertencias que mencioné en mi NOLOCK artículo:

    • Los bloqueos Sch-S aún deben tomarse incluso bajo RCSI.
    • Los niveles de aislamiento de instantáneas utilizan el control de versiones de filas en tempdb, por lo que realmente necesita probar el impacto allí.
    • RCSI no puede utilizar escaneos de orden de asignación eficientes; verá escaneos de rango en su lugar.
    • Paul White (@SQL_Kiwi) tiene algunas publicaciones excelentes que debe leer sobre estos niveles de aislamiento:
      • Aislamiento de instantáneas confirmadas de lectura
      • Modificaciones de datos bajo el aislamiento de instantáneas confirmadas de lectura
      • El nivel de aislamiento SNAPSHOT

    Además, incluso con RCSI, obtener el recuento "preciso" lleva tiempo (y recursos adicionales en tempdb). Cuando finaliza la operación, ¿sigue siendo exacto el recuento? Solo si nadie ha tocado la mesa mientras tanto. Entonces, uno de los beneficios de RCSI (los lectores no bloquean a los escritores) se desperdicia.

¿Cuántas filas coinciden con una cláusula WHERE?

Este es un escenario ligeramente diferente:necesita saber cuántas filas existen para un determinado subconjunto de la tabla. No puede usar los DMV para esto, a menos que WHERE La cláusula coincide con un índice filtrado o cubre completamente una partición exacta (o múltiple).

Si tu WHERE cláusula es dinámica, puede usar RCSI, como se describe arriba.

Si tu WHERE cláusula no es dinámica, también podría usar RCSI, pero también podría considerar una de estas opciones:

  • Índice filtrado – por ejemplo, si tiene un filtro simple como is_active = 1 o status < 5 , entonces podría crear un índice como este:
    CREATE INDEX ix_f ON dbo.table_name(leading_pk_column) WHERE is_active = 1;

    Ahora, puede obtener conteos bastante precisos de los DMV, ya que habrá entradas que representen este índice (solo tiene que identificar index_id en lugar de depender del montón (0)/índice agrupado (1)). Sin embargo, debe considerar algunas de las debilidades de los índices filtrados.

  • Vista indexada - por ejemplo, si a menudo cuenta los pedidos por cliente, una vista indexada podría ayudar (aunque no tome esto como un respaldo genérico de que "¡las vistas indexadas mejoran todas las consultas!"):
    CREATE VIEW dbo.view_name
    WITH SCHEMABINDING
    AS
      SELECT 
        customer_id, 
        customer_count = COUNT_BIG(*)
      FROM dbo.table_name
      GROUP BY customer_id;
    GO
     
    CREATE UNIQUE CLUSTERED INDEX ix_v ON dbo.view_name(customer_id);

    Ahora, los datos en la vista se materializarán y se garantiza que el conteo se sincronizará con los datos de la tabla (hay un par de errores oscuros en los que esto no es cierto, como este con MERGE , pero generalmente esto es confiable). Así que ahora puede obtener sus recuentos por cliente (o para un conjunto de clientes) consultando la vista, a un costo de consulta mucho más bajo (1 o 2 lecturas):

    SELECT customer_count FROM dbo.view_name WHERE customer_id = <x>;

    Sin embargo, no hay almuerzo gratis . Debe considerar la sobrecarga de mantener una vista indexada y el impacto que tendrá en la parte de escritura de su carga de trabajo. Si no ejecuta este tipo de consulta con mucha frecuencia, es poco probable que valga la pena.

¿Al menos una fila coincide con una cláusula WHERE?

Esta también es una pregunta ligeramente diferente. Pero a menudo veo esto:

IF (SELECT COUNT(*) FROM dbo.table_name WHERE <some clause>) > 0 -- or = 0 for not exists

Dado que obviamente no le importa el recuento real, solo le importa si existe al menos una fila, realmente creo que debería cambiarlo a lo siguiente:

IF EXISTS (SELECT 1 FROM dbo.table_name WHERE <some clause>)

Esto al menos tiene la posibilidad de provocar un cortocircuito antes de que se alcance el final de la tabla, y casi siempre superará al COUNT variación (aunque hay algunos casos en los que SQL Server es lo suficientemente inteligente como para convertir IF (SELECT COUNT...) > 0 a un IF EXISTS() más simple ). En el peor de los casos, donde no se encuentra ninguna fila (o la primera fila se encuentra en la última página del escaneo), el rendimiento será el mismo.

[Ver un índice de todas las publicaciones sobre malos hábitos/mejores prácticas]