Database
 sql >> Base de données >  >> RDS >> Database

Mauvaises habitudes :Compter les lignes à la dure

[Voir un index de toutes les publications sur les mauvaises habitudes/meilleures pratiques]

L'une des diapositives de ma présentation récurrente sur les mauvaises habitudes et les meilleures pratiques s'intitule "Abusing COUNT(*) ." Je vois assez souvent cet abus dans la nature, et il prend plusieurs formes.

Combien de lignes dans le tableau ?

Je vois généralement ceci :

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

SQL Server doit exécuter une analyse bloquante sur l'ensemble de la table afin de dériver ce nombre. C'est cher. Ces informations sont stockées dans les vues de catalogue et les DMV, et vous pouvez les obtenir sans toutes ces E/S ou blocages :

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';

(Vous pouvez obtenir les mêmes informations à partir de sys.dm_db_partition_stats , mais dans ce cas changez p.rows à p.row_count (yay cohérence!). En fait, c'est la même vue que sp_spaceused utilise pour dériver le décompte - et bien qu'il soit beaucoup plus facile à taper que la requête ci-dessus, je déconseille de l'utiliser uniquement pour dériver un décompte en raison de tous les calculs supplémentaires qu'il effectue - à moins que vous ne vouliez également cette information. Notez également qu'il utilise des fonctions de métadonnées qui n'obéissent pas à votre niveau d'isolation externe, vous pourriez donc attendre le blocage lorsque vous appelez cette procédure.)

Maintenant, il est vrai que ces vues ne sont pas précises à 100 %, à la microseconde près. À moins que vous n'utilisiez un tas, un résultat plus fiable peut être obtenu à partir de sys.dm_db_index_physical_stats() colonne record_count (yay encore de la cohérence !), mais cette fonction peut avoir un impact sur les performances, peut toujours bloquer et peut être encore plus coûteuse qu'un SELECT COUNT(*) – il doit faire les mêmes opérations physiques, mais doit calculer des informations supplémentaires selon le mode (comme la fragmentation, dont vous ne vous souciez pas dans ce cas). L'avertissement dans la documentation raconte une partie de l'histoire, pertinente si vous utilisez des groupes de disponibilité (et affecte probablement la mise en miroir de bases de données de la même manière) :

Si vous interrogez sys.dm_db_index_physical_stats sur une instance de serveur qui héberge un réplica secondaire lisible AlwaysOn, vous pouvez rencontrer un problème de blocage REDO. En effet, cette vue de gestion dynamique acquiert un verrou IS sur la table ou la vue utilisateur spécifiée qui peut bloquer les demandes d'un thread REDO pour un verrou X sur cette table ou vue utilisateur.

La documentation explique également pourquoi ce nombre peut ne pas être fiable pour un tas (et leur donne également un quasi-passe pour l'incohérence des lignes par rapport aux enregistrements) :

Pour un tas, le nombre d'enregistrements renvoyés par cette fonction peut ne pas correspondre au nombre de lignes renvoyées en exécutant un SELECT COUNT(*) sur le tas. En effet, une ligne peut contenir plusieurs enregistrements. Par exemple, dans certaines situations de mise à jour, une seule ligne de tas peut avoir un enregistrement de transfert et un enregistrement transféré à la suite de l'opération de mise à jour. En outre, la plupart des lignes LOB volumineuses sont divisées en plusieurs enregistrements dans le stockage LOB_DATA.

Je pencherais donc vers sys.partitions comme moyen d'optimiser cela, en sacrifiant un peu de précision marginale.

    "Mais je ne peux pas utiliser les DMV ; mon décompte doit être super précis !"

    Un décompte "super précis" est en fait assez dénué de sens. Considérons que votre seule option pour un décompte "super précis" est de verrouiller l'intégralité de la table et d'interdire à quiconque d'ajouter ou de supprimer des lignes (mais sans empêcher les lectures partagées), par exemple :

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

    Ainsi, votre requête bourdonne, scanne toutes les données, travaille vers ce décompte "parfait". Pendant ce temps, les demandes d'écriture sont bloquées et attendent. Soudain, lorsque votre décompte précis est renvoyé, vos verrous sur la table sont libérés et toutes ces demandes d'écriture qui étaient en file d'attente et en attente commencent à déclencher toutes sortes d'insertions, de mises à jour et de suppressions sur votre table. À quel point votre comptage est-il "super précis" ? Cela valait-il la peine d'obtenir un décompte "précis" qui est déjà terriblement obsolète ? Si le système n'est pas occupé, alors ce n'est pas vraiment un problème - mais si le système n'est pas occupé, je dirais assez fermement que les DMV seront sacrément précis.

    Vous auriez pu utiliser NOLOCK à la place, mais cela signifie simplement que les rédacteurs peuvent modifier les données pendant que vous les lisez, et cela entraîne également d'autres problèmes (j'en ai parlé récemment). C'est correct pour beaucoup de stades, mais pas si votre objectif est la précision. Les DMV seront juste (ou du moins beaucoup plus proches) dans de nombreux scénarios, et plus éloignés dans très peu (en fait aucun auquel je puisse penser).

    Enfin, vous pouvez utiliser Read Committed Snapshot Isolation. Kendra Little a un article fantastique sur les niveaux d'isolement des instantanés, mais je vais répéter la liste des mises en garde que j'ai mentionnées dans mon NOLOCK articles :

    • Les verrous Sch-S doivent toujours être pris même sous RCSI.
    • Les niveaux d'isolement des instantanés utilisent la gestion des versions de ligne dans tempdb, vous devez donc vraiment tester l'impact là-bas.
    • RCSI ne peut pas utiliser des analyses d'ordre d'allocation efficaces ; vous verrez des balayages de plage à la place.
    • Paul White (@SQL_Kiwi) a d'excellents articles à lire sur ces niveaux d'isolement :
      • Lire l'isolement d'instantané validé
      • Modifications de données sous isolement d'instantané validé en lecture
      • Le niveau d'isolement SNAPSHOT

    De plus, même avec RCSI, obtenir le décompte "précis" prend du temps (et des ressources supplémentaires dans tempdb). Au moment où l'opération est terminée, le décompte est-il toujours exact ? Seulement si personne n'a touché la table entre-temps. Ainsi, l'un des avantages du RCSI (les lecteurs ne bloquent pas les écrivains) est perdu.

Combien de lignes correspondent à une clause WHERE ?

Il s'agit d'un scénario légèrement différent - vous devez savoir combien de lignes existent pour un certain sous-ensemble de la table. Vous ne pouvez pas utiliser les DMV pour cela, à moins que le WHERE la clause correspond à un index filtré ou couvre complètement une partition exacte (ou multiple).

Si votre WHERE clause est dynamique, vous pouvez utiliser RCSI, comme décrit ci-dessus.

Si votre WHERE n'est pas dynamique, vous pouvez également utiliser RCSI, mais vous pouvez également envisager l'une de ces options :

  • Index filtré – par exemple si vous avez un filtre simple comme is_active = 1 ou status < 5 , vous pouvez créer un index comme celui-ci :
    CREATE INDEX ix_f ON dbo.table_name(leading_pk_column) WHERE is_active = 1;

    Maintenant, vous pouvez obtenir des décomptes assez précis à partir des DMV, car il y aura des entrées représentant cet index (il vous suffit d'identifier l'index_id au lieu de vous fier à heap(0)/clustered index(1)). Cependant, vous devez tenir compte de certaines des faiblesses des index filtrés.

  • Vue indexée - par exemple, si vous comptez souvent les commandes par client, une vue indexée peut vous aider (bien que ne considérez pas cela comme une approbation générique selon laquelle "les vues indexées améliorent toutes les requêtes !") :
    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);

    Maintenant, les données de la vue seront matérialisées et le décompte est garanti pour être synchronisé avec les données de la table (il y a quelques bogues obscurs où ce n'est pas vrai, comme celui-ci avec MERGE , mais généralement c'est fiable). Vous pouvez désormais obtenir vos décomptes par client (ou pour un ensemble de clients) en interrogeant la vue, à un coût de requête bien inférieur (1 ou 2 lectures) :

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

    Il n'y a pas de déjeuner gratuit, cependant . Vous devez tenir compte de la surcharge de maintenance d'une vue indexée et de l'impact qu'elle aura sur la partie écriture de votre charge de travail. Si vous n'exécutez pas souvent ce type de requête, il est peu probable que cela en vaille la peine.

Est-ce qu'au moins une ligne correspond à une clause WHERE ?

Là aussi, c'est une question légèrement différente. Mais je vois souvent ceci :

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

Étant donné que vous ne vous souciez évidemment pas du nombre réel, vous ne vous souciez que du fait qu'au moins une ligne existe, je pense vraiment que vous devriez la modifier comme suit :

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

Cela a au moins une chance de court-circuiter avant que la fin de la table ne soit atteinte, et surpassera presque toujours le COUNT variation (bien qu'il y ait des cas où SQL Server est assez intelligent pour convertir IF (SELECT COUNT...) > 0 à un IF EXISTS() plus simple ). Dans le pire des cas, où aucune ligne n'est trouvée (ou la première ligne est trouvée sur la toute dernière page de l'analyse), les performances seront les mêmes.

[Voir un index de toutes les publications sur les mauvaises habitudes/meilleures pratiques]