StmtText |--Nested Loops(Left Outer Join, OUTER REFERENCES:([SugarCRM].[dbo].[contacts].[reports_to_id]) OPTIMIZED) |--Nested Loops(Left Outer Join, OUTER REFERENCES:([a_c].[account_id]) OPTIMIZED) | |--Nested Loops(Left Outer Join) | | |--Index Scan(OBJECT:([SugarCRM].[dbo].[contacts].[idx_reports_to_id]), WHERE:(CONVERT_IMPLICIT(nvarchar(36),[SugarCRM].[dbo].[contacts].[id],0)=N'1A9C0C58A81C4195B854739635E435D5')) | | |--Nested Loops(Inner Join, OUTER REFERENCES:([a_c].[id]) OPTIMIZED) | | |--Index Scan(OBJECT:([SugarCRM].[dbo].[accounts_contacts].[idx_account_contact] AS [a_c]), WHERE:(CONVERT_IMPLICIT(nvarchar(36),[SugarCRM].[dbo].[accounts_contacts].[contact_id] AS [a_c].[contact_id],0)=N'1A9C0C58A81C4195B85473...')) | | |--Clustered Index Seek(OBJECT:([SugarCRM].[dbo].[accounts_contacts].[pk_accounts_contacts] AS [a_c]), SEEK:([a_c].[id]=[SugarCRM].[dbo].[...) | |--Clustered Index Seek(OBJECT:([SugarCRM].[dbo].[accounts].[pk_accounts] AS [acc]), SEEK:([acc].[id]=[SugarCRM].[dbo].[accounts_contacts]. ...) |--Clustered Index Seek(OBJECT:([SugarCRM].[dbo].[contacts].[pk_contacts] AS [con_reports_to]), SEEK:([con_reports_to].[id]=[SugarCRM].[dbo].[)...) Â
| t-sql performance : un filtre avec une chaine unicode sur une colonne varchar entraine un surcout cpu |
| Écrit par Romain Ferraton | |||||||||||||||||||||
|
Suite à un audit de performance chez une entreprise qui cherchait à améliorer les performances MSSQL Server (SSDE 2008 SE 64 bits sur W2K8R2) pour son application CRM (SugarCRM), j'ai constaté un comportement au premier abord étrange sur la gestion des chaînes unicode sur le SGBD de microsoft... La requête qui m'a posé problème était une requête très simple d'une durée de 200ms environ. Pas de problème de performance avec 200ms par requête me direz vous, sauf que cette requête était lancée plusieurs dixaines de fois pour certains écrans. Voici la requête :  requete initiale avec les chaines unicode
SELECT acc.id, acc.name, con_reports_to.first_name, con_reports_to.last_name FROM contacts LEFT JOIN accounts_contacts a_c ON a_c.contact_id = N'1A9C0C58A81C4195B854739635E435D5' AND a_c.deleted=0 LEFT JOIN accounts acc ON a_c.account_id = acc.id AND acc.deleted=0 LEFT JOIN contacts con_reports_to ON con_reports_to.id = contacts.reports_to_id WHERE contacts.id = N'1A9C0C58A81C4195B854739635E435D5'   On notera sur cette requête les prédicats=N'1A9C0C58A81C4195B854739635E435D5'. Le N qui préfixe la chaîne indique au moteur relationnel de sql server qu'il s'agit d'une chaîne de caractère unicode. Or les colonnes sur lesquelles portent les filtres ne sont pas en nvarchar mais en varchar.
En observant les statistiques et le plan d'éxécution on observe ceci :
A ce stade je me pose plusieurs questions :
En analysant plus avant les opérations d'index scan on notera une petite subtilité : l'utilisation d'une conversion implicite (CONVERT_IMPLICITE). Les valeurs de la colonne sont converties en unicode avant d'être comparer à la chaîne de caractère (unicode elle aussi car préfixée par N'). Plan d'execution avec chaine unicode
 C'est cette conversion qui consomme du cpu à outrance et qui implique de faire un index scan en balayant plusieurs milliers de pages et non un index seek.
Mais pourquoi diable une conversion de la colonne en nvarchar et pas plutôt une conversion de la chaîne passée en paramètre en varchar ? Que ce passe-t il si on retire les prefixes N' devant les chaîne de caractère en filtre ?Etudions le comportement de cette requête sans les chaînes unicode :
requete sans les chaines unicode
SELECT acc.id, acc.name, con_reports_to.first_name, con_reports_to.last_name FROM contacts LEFT JOIN accounts_contacts a_c ON a_c.contact_id = '1A9C0C58A81C4195B854739635E435D5' AND a_c.deleted=0 LEFT JOIN accounts acc ON a_c.account_id = acc.id AND acc.deleted=0 LEFT JOIN contacts con_reports_to ON con_reports_to.id = contacts.reports_to_id WHERE contacts.id = '1A9C0C58A81C4195B854739635E435D5' Â Â
 Plan d'execution sans les chaines unicodes
StmtText |--Nested Loops(Left Outer Join, OUTER REFERENCES:([SugarCRM].[dbo].[contacts].[reports_to_id]) OPTIMIZED) |--Nested Loops(Left Outer Join, OUTER REFERENCES:([a_c].[account_id]) OPTIMIZED) | |--Nested Loops(Left Outer Join) | | |--Clustered Index Seek(OBJECT:([SugarCRM].[dbo].[contacts].[pk_contacts]), SEEK:([SugarCRM].[dbo].[contacts].[id]='1A9C0C58A81C4195B854739635E435D5')...) | | |--Index Seek(OBJECT:([SugarCRM].[dbo].[accounts_contacts].[idx_contid_del_accid] AS [a_c]), SEEK:([a_c].[contact_id]='1A9C0C58A81C4195B854739635E435D5'... | |--Clustered Index Seek(OBJECT:([SugarCRM].[dbo].[accounts].[pk_accounts] AS [acc]), SEEK:([acc].[id]=[SugarCRM].[dbo].[accounts_contacts].[account_id]...)... |--Clustered Index Seek(OBJECT:([SugarCRM].[dbo].[contacts].[pk_contacts] AS [con_reports_to]), SEEK:([con_reports_to].[id]=[SugarCRM].[dbo].[contacts].[reports_ ...)   --> Il n'y a plus de CONVERT_IMPLICIT. Comment faire pour éviter que l'application de demande une recherche d'une chaine unicode sur un champ qui ne peut pas l'être puisque de type varchar ?Le consultant SugarCRM a alors recherché dans les fichiers de génération T-SQL de l'outil. Un des fichiers, mssql_manager.php, contenait une expression régulière (prereg_match) qui positionnait systématiquement un N' devant les chaînes de type string. En retirant le N', le sql généré était alors conforme par rapport au type de données dans les tables et la performance des écrans fut améliorée.   |
|||||||||||||||||||||
| Mis à jour ( Dimanche, 04 Septembre 2011 08:52 ) | |||||||||||||||||||||





Les ID de SugarCRM étant des UUID (36 caractères hexadécimeux séparés par des tirets) les champs ID en base de données sont donc des char(36) / varchar(36).
Nous avons donc apporté une correction pour ce cas précis de SQL Server mais nous sommes en attente d'une réponse officielle de l'éditeur afin de valider que la correction proposée s'officialise.
PS: Pour les puristes des preg_match à chaque requête est une solution un peu lourde j'en conviens