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

Colonnes virtuelles et index fonctionnels

Bien trop souvent, nous voyons des requêtes SQL complexes mal écrites s'exécuter sur les tables de la base de données. Ces requêtes peuvent prendre très peu ou très longtemps à s'exécuter, mais elles consomment énormément de CPU et d'autres ressources. Néanmoins, dans de nombreux cas, les requêtes complexes fournissent des informations précieuses à l'application/la personne. Par conséquent, il apporte des atouts utiles dans toutes les variétés d'applications.

Complexité des requêtes

Examinons de plus près les requêtes problématiques. Beaucoup d'entre eux sont complexes. Cela peut être dû à plusieurs raisons :

  1. Le type de données choisi pour les données ;
  2. L'organisation et le stockage des données dans la base de données ;
  3. Transformation et jointure des données dans une requête pour récupérer l'ensemble de résultats souhaité.

Vous devez bien réfléchir à ces trois facteurs clés et les mettre en œuvre correctement pour que les requêtes fonctionnent de manière optimale.

Cependant, cela peut devenir une tâche presque impossible pour les développeurs de bases de données et les administrateurs de bases de données. Par exemple, il peut être exceptionnellement difficile d'ajouter de nouvelles fonctionnalités aux systèmes Legacy existants. Un cas particulièrement compliqué est lorsque vous devez extraire et transformer les données d'un système hérité afin de pouvoir les comparer aux données produites par le nouveau système ou la nouvelle fonctionnalité. Vous devez y parvenir sans affecter les fonctionnalités de l'application héritée.

Ces requêtes peuvent impliquer des jointures complexes, telles que les suivantes :

  1. Une combinaison de sous-chaîne et/ou une concaténation de plusieurs colonnes de données ;
  2. Fonctions scalaires intégrées ;
  3. FDU personnalisées ;
  4. Toute combinaison de comparaisons de clauses WHERE et de conditions de recherche.

Les requêtes, comme décrit précédemment, ont généralement des chemins d'accès complexes. Pire encore, ils peuvent avoir de nombreux balayages de table et/ou balayages d'index complets avec de telles combinaisons de JOIN ou de recherches.

Transformation des données et manipulations dans les requêtes

Nous devons souligner que toutes les données stockées de manière persistante dans une table de base de données nécessitent une transformation et/ou une manipulation à un moment donné lorsque nous interrogeons ces données à partir de la table. La transformation peut aller d'une transformation simple à une transformation très complexe. Selon sa complexité, la transformation peut consommer beaucoup de CPU et de ressources.

Dans la plupart des cas, les transformations effectuées dans les JOINs se produisent après la lecture et le déchargement des données dans le tempdb base de données (SQL Server) ou fichier de travail base de données / temp-tablespaces comme dans d'autres systèmes de bases de données.

Étant donné que les données du fichier de travail ne sont pas indexables , le temps nécessaire pour exécuter des transformations combinées et des JOINs augmente de façon exponentielle. Les données récupérées deviennent plus grandes. Ainsi, les requêtes qui en résultent se transforment en un goulot d'étranglement des performances en raison de la croissance supplémentaire des données.

Alors, comment un développeur de base de données ou un administrateur de base de données peut-il résoudre rapidement ces goulots d'étranglement de performances et se donner plus de temps pour repenser et réécrire les requêtes pour des performances optimales ?

Il existe deux façons de résoudre efficacement ces problèmes persistants. L'une d'elles consiste à utiliser des colonnes virtuelles et/ou des index fonctionnels.

Index et requêtes fonctionnels

Normalement, vous créez des index sur des colonnes qui indiquent un ensemble unique de colonnes/valeurs dans une ligne (index uniques ou clés primaires) ou représentent un ensemble de colonnes/valeurs qui sont ou peuvent être utilisées dans les conditions de recherche de la clause WHERE d'une requête.

Si vous ne disposez pas de tels index et que vous avez développé des requêtes complexes comme décrit précédemment, vous remarquerez ce qui suit :

  1. Réduction des niveaux de performances lors de l'utilisation de l'explication interroger et voir des analyses de table ou des analyses complètes d'index
  2. Utilisation très élevée du processeur et des ressources causée par les requêtes ;
  3. Longs délais d'exécution.

Les bases de données contemporaines résolvent normalement ces problèmes en vous permettant de créer une base de données fonctionnelle ou basé sur la fonction index, tel qu'il est nommé dans SQLServer, Oracle et MySQL (v 8.x). Ou, il peut s'agir de Index sur expression/basé sur l'expression index, comme dans d'autres bases de données (PostgreSQL et Db2).

Supposons que vous ayez une colonne Purchase_Date du type de données TIMESTAMP ou DATETIME dans votre Commande table, et cette colonne a été indexée. Nous commençons à interroger l'Ordre table avec une clause WHERE :

SELECT ...
FROM Order
WHERE DATE(Purchase_Date) = '03.12.2020'

Cette transaction entraînera l'analyse de l'ensemble de l'index. Cependant, si la colonne n'a pas été indexée, vous obtenez un balayage de table.

Après avoir scanné l'intégralité de l'index, cet index se déplace dans tempdb / workfile (table entière si vous obtenez un balayage de table ) avant de faire correspondre la valeur 03.12.2020 .

Comme une grande table de commandes utilise beaucoup de CPU et de ressources, vous devez créer un index fonctionnel ayant l'expression DATE (Purchase_Date ) comme l'une des colonnes d'index et illustré ci-dessous :

CREATE ix_DatePurchased on sales.Order(Date(Purchase_Date) desc, ... )

Ce faisant, vous créez le prédicat correspondant DATE (Purchase_Date) ='03.12.2020' indexable. Ainsi, au lieu de déplacer l'index ou la table vers le tempdb / workfile avant la correspondance de la valeur, nous rendons l'index uniquement partiellement accessible et/ou scanné. Il en résulte une utilisation moindre du processeur et des ressources.

Jetez un œil à un autre exemple. Il y a un Client tableau avec les colonnes first_name, last_name . Ces colonnes sont indexées comme telles :

CREATE INDEX ix_custname on Customer(first_name asc, last_name asc),

De plus, vous avez une vue qui concatène ces colonnes dans le nom_client colonne :

CREATE view v_CustomerInfo( customer_name, .... ) as
select first_name ||' '|| last_name as customer_name,.....
from Customer
where ...

Vous avez une requête provenant d'un système de commerce électronique qui recherche le nom complet du client :

select c.*
from v_CustomerInfo c
where c.customer_name = 'John Smith'
....

Encore une fois, cette requête produira une analyse complète de l'index. Dans le pire des cas, ce sera une analyse complète de la table déplaçant toutes les données de l'index ou de la table vers le fichier de travail avant la concaténation du first_name et nom_de_famille colonnes et correspondant à la valeur "John Smith".

Un autre cas consiste à créer un index fonctionnel comme indiqué ci-dessous :

CREATE ix_fullcustname on sales.Customer( first_name ||' '|| last_name desc, ... )

De cette façon, vous pouvez transformer la concaténation dans la requête de vue en un prédicat indexable. Au lieu d'un parcours d'index complet ou d'un parcours de table, vous avez un parcours d'index partiel. Une telle exécution de requête entraîne une utilisation moindre du processeur et des ressources, excluant le travail dans le fichier de travail et garantissant ainsi un temps d'exécution plus rapide.

Colonnes et requêtes virtuelles (générées)

Les colonnes générées (colonnes virtuelles ou colonnes calculées) sont des colonnes qui contiennent les données générées à la volée. Les données ne peuvent pas être explicitement définies sur une valeur spécifique. Il fait référence aux données d'autres colonnes interrogées, insérées ou mises à jour dans une requête DML.

La génération de valeurs de ces colonnes est automatisée sur la base d'une expression. Ces expressions peuvent générer :

  1. Une séquence de valeurs entières ;
  2. La valeur basée sur les valeurs des autres colonnes du tableau ;
  3. Il peut générer des valeurs en appelant des fonctions intégrées ou des fonctions définies par l'utilisateur (UDF).

Il est également important de noter que dans certaines bases de données (SQLServer, Oracle, PostgreSQL, MySQL et MariaDB), ces colonnes peuvent être configurées soit pour stocker de manière persistante les données avec l'exécution des instructions INSERT et UPDATE, soit pour exécuter l'expression de colonne sous-jacente à la volée. si nous interrogeons la table et la colonne économisant l'espace de stockage.

Cependant, lorsque l'expression est compliquée, comme avec la logique complexe de la fonction UDF, les économies de temps d'exécution, de ressources et de coûts de requête CPU peuvent ne pas être aussi importantes que prévu.

Ainsi, nous pouvons configurer la colonne afin qu'elle stocke de manière persistante le résultat de l'expression dans une instruction INSERT ou UPDATE. Ensuite, nous créons un index régulier sur cette colonne. De cette façon, nous économiserons le CPU, l'utilisation des ressources et le temps d'exécution de la requête. Encore une fois, il peut y avoir une légère augmentation des performances INSERT et UPDATE, en fonction de la complexité de l'expression.

Prenons un exemple. Nous déclarons la table et créons un index comme suit :

CREATE TABLE Customer as (
  customerID Int GENERATED ALWAYS AS IDENTITY,
  first_name VARCHAR(50) NOT NULL,
  last_name VARCHAR(50) NOT NULL,
  customer_name as (first_name ||' '|| last_name) PERSISTED
  ...
  );
CREATE ix_fullcustname on sales.Customer( customer_name desc, ... )

De cette façon, nous déplaçons la logique de concaténation de la vue de l'exemple précédent vers le bas dans la table et stockons les données de manière persistante. Nous récupérons les données à l'aide d'un balayage de correspondance sur un index régulier. C'est le meilleur résultat possible ici.

En ajoutant une colonne générée à une table et en créant un index régulier sur cette colonne, nous pouvons déplacer la logique de transformation jusqu'au niveau de la table. Ici, nous stockons de manière persistante les données transformées dans des instructions d'insertion ou de mise à jour qui, autrement, seraient transformées en requêtes. Les scans JOIN et INDEX seront beaucoup plus simples et rapides.

Index fonctionnels, colonnes générées et JSON

Les applications Web et mobiles mondiales utilisent des structures de données légères telles que JSON pour déplacer les données de l'appareil Web/mobile vers la base de données et vice versa. La faible empreinte des structures de données JSON rend le transfert de données sur le réseau rapide et facile. Il est facile de compresser JSON à une très petite taille par rapport à d'autres structures, c'est-à-dire XML. Il peut surpasser les structures dans l'analyse d'exécution.

En raison de l'utilisation accrue des structures de données JSON, les bases de données relationnelles ont le format de stockage JSON en tant que type de données BLOB ou type de données CLOB. Ces deux types rendent les données de ces colonnes non indexables telles quelles.

Pour cette raison, les fournisseurs de bases de données ont introduit des fonctions JSON pour interroger et modifier des objets JSON, car vous pouvez facilement intégrer ces fonctions dans la requête SQL ou d'autres commandes DML. Cependant, ces requêtes dépendent de la complexité des objets JSON. Ils sont très gourmands en CPU et en ressources, car les objets BLOB et CLOB doivent être déchargés en mémoire ou, pire, dans le fichier de travail avant d'interroger et/ou de manipuler.

Supposons que nous ayons un Client tableau avec les détails du client données stockées en tant qu'objet JSON dans une colonne appelée CustomerDetail . Nous avons configuré l'interrogation de la table comme ci-dessous :

SELECT CustomerID,
  JSON_VALUE(CustomerDetail, '$.customer.Name') AS Name,
  JSON_VALUE(CustomerDetail, '$.customer.Surname') AS Surname,
  JSON_VALUE(CustomerDetail, '$.customer.address.PostCode') AS PostCode,
  JSON_VALUE(CustomerDetail, '$.customer.address."Address Line 1"') + ' '
  + JSON_VALUE(CustomerDetail, '$.customer.address."Address Line 2"') AS Address,
  JSON_QUERY(CustomerDetail, '$.customer.address.Country') AS Country
FROM Customer
WHERE ISJSON(CustomerDetail) > 0
  AND JSON_VALUE(CustomerDetail, '$.customer.address.Country') = 'Iceland'
  AND JSON_VALUE(CustomerDetail, '$.customer.address.PostCode') IN (101,102,110,210,220)
  AND Status = 'Active'
ORDER BY JSON_VALUE(CustomerDetail, '$.customer.address.PostCode')

Dans cet exemple, nous interrogeons les données des clients résidant dans certaines parties de la région de la capitale en Islande. Tous Actifs les données doivent être récupérées dans le fichier de travail avant d'appliquer le prédicat de recherche. Néanmoins, la récupération entraînera une utilisation trop importante du processeur et des ressources.

En conséquence, il existe une procédure efficace pour accélérer l'exécution des requêtes JSON. Cela implique d'utiliser la fonctionnalité via des colonnes générées, comme décrit précédemment.

Nous obtenons l'amélioration des performances en ajoutant des colonnes générées. Une colonne générée rechercherait dans le document JSON des données spécifiques représentées dans la colonne à l'aide des fonctions JSON et stockerait la valeur dans la colonne.

Nous pouvons indexer et interroger ces colonnes générées à l'aide des conditions de recherche de clause SQL standard. Par conséquent, la recherche de données particulières dans des objets JSON devient très rapide.

Nous ajoutons deux colonnes générées - Pays et Code Postal :

ALTER TABLE Customer
ADD Country as JSON_VALUE(CustomerDetail,'$.customer.address.Country');
ALTER TABLE Customer
ADD PostCode as JSON_VALUE(CustomerDetail,'$.customer.address.PostCode');

CREATE INDEX ix_CountryPostCode on Country(Country asc,PostCode asc);

De plus, nous créons un index composite sur les colonnes spécifiques. Maintenant, nous pouvons remplacer la requête par l'exemple affiché ci-dessous :

SELECT CustomerID,
  JSON_VALUE(CustomerDetail, '$.customer.customer.Name') AS Name,
  JSON_VALUE(CustomerDetail, '$.customer.customer.Surname') AS Surname,
  JSON_VALUE(CustomerDetail, '$.customer.address.PostCode') AS PostCode,
  JSON_VALUE(CustomerDetail, '$.customer.address."Address Line 1"') + ' '
  + JSON_VALUE(CustomerDetail, '$.customer.address."Address Line 2"') AS Address,
  JSON_QUERY(CustomerDetail, '$.customer.address.Country') AS Country
FROM Customer
WHERE ISJSON(CustomerDetail) > 0
  AND Country = 'Iceland'
  AND PostCode IN (101,102,110,210,220)
  AND Status = 'Active'
ORDER BY JSON_VALUE(CustomerDetail, '$.customer.address.PostCode')

Cela limite la récupération des données aux clients actifs uniquement dans certaines parties de la région de la capitale islandaise. Cette méthode est plus rapide et plus efficace que la requête précédente.

Conclusion

Dans l'ensemble, en appliquant des colonnes virtuelles ou des index fonctionnels aux tables qui causent des difficultés (CPU et requêtes gourmandes en ressources), nous pouvons éliminer les problèmes assez rapidement.

Les colonnes virtuelles et les index fonctionnels peuvent aider à interroger des objets JSON complexes stockés dans des tables relationnelles régulières. Cependant, nous devons évaluer attentivement les problèmes au préalable et apporter les modifications nécessaires en conséquence.

Dans certains cas, si la requête et/ou les structures de données JSON sont très complexes, une partie de l'utilisation du CPU et des ressources peut passer des requêtes aux processus INSERT / UPDATE. Cela nous donne moins d'économies globales de CPU et de ressources que prévu. Si vous rencontrez des problèmes similaires, une refonte plus approfondie des tables et des requêtes peut être inévitable.