Kezako “Kitchen Sink” en T-SQL ? C’est une technique qui consiste à essayer de remplir le plus de conditions possibles dans une requête. Cela se traduit par la répétition de clauses du type WHERE @variable IS NULL OR colonne = @variable. Et là c’est le drame, patatras, on attend encore le résultat… C’est une approche assez courante mais surtout coûteuse en terme de performance sur de gros volume de données. Cette problématique est d’autant plus intéressante qu’elle nous permettra d’aborder d’autres thèmes sous-jacents pour le tuning d’instance et de requête.
C’est un exemple courant de kitchen sink.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
USE [AdventureWorks2014] GO SET LANGUAGE English SET NOCOUNT ON DECLARE @CustomerId INT = NULL, @StartOrderDate DATETIME = '2006-05-01', @EndOrderDate DATETIME = NULL, @MinTotalDue MONEY = 1000 SELECT COUNT(*) FROM Sales.SalesOrderHeader WHERE 1 = 1 AND (@CustomerId IS NULL OR CustomerId = @CustomerId) AND (@StartOrderDate IS NULL OR OrderDate >= @StartOrderDate) AND (@EndOrderDate IS NULL OR OrderDate <= @EndOrderDate) AND (@MinTotalDue IS NULL OR TotalDue >= @MinTotalDue) GO |
L’exemple ci-dessous est une version un peu plus élégante de kitchen sink syntaxiquement mais qui se traduit quasiment par le même résultat en terme de performance, suivant l’indexation, on le verra plus tard.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
USE [AdventureWorks2014] GO SET LANGUAGE English SET NOCOUNT ON DECLARE @CustomerId INT = NULL, @StartOrderDate DATETIME = '2006-05-01', @EndOrderDate DATETIME = NULL, @MinTotalDue MONEY = 1000 SELECT COUNT(*) FROM Sales.SalesOrderHeader WHERE 1 = 1 AND CustomerId = ISNULL(@CustomerId, CustomerId) AND OrderDate >= ISNULL(@StartOrderDate, OrderDate) AND OrderDate <= ISNULL(@EndOrderDate, OrderDate) AND TotalDue >= ISNULL(@MinTotalDue, TotalDue) GO |
On pourrait se contenter de réécrire la requête comme ceci, en passant directement les valeurs littérales mais est-ce bien raisonnable ?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
USE [AdventureWorks2014] GO SET LANGUAGE English SET NOCOUNT ON DECLARE @CustomerId INT = NULL, @StartOrderDate DATETIME = '2006-05-01', @EndOrderDate DATETIME = NULL, @MinTotalDue MONEY = 1000, @SQLQuery AS NVARCHAR(1500) SET @SQLQuery = 'SELECT COUNT(*) ' SET @SQLQuery += CHAR(10) + 'FROM Sales.SalesOrderHeader ' SET @SQLQuery += CHAR(10) + 'WHERE 1 = 1 ' IF @CustomerId IS NOT NULL SET @SQLQuery += CHAR(10) + 'AND CustomerId = ' + CAST(@CustomerId AS VARCHAR) IF @StartOrderDate IS NOT NULL SET @SQLQuery += CHAR(10) + 'AND OrderDate >= ''' + CONVERT(CHAR(16), @StartOrderDate, 120) + '''' IF @EndOrderDate IS NOT NULL SET @SQLQuery += CHAR(10) + 'AND OrderDate <= ''' + CONVERT(CHAR(16), @EndOrderDate, 120) + '''' IF @MinTotalDue IS NOT NULL SET @SQLQuery += CHAR(10) + 'AND TotalDue >= ' + CAST(@MinTotalDue AS VARCHAR) PRINT @SQLQuery EXEC (@SQLQuery) GO |
Il y a une petite différence par rapport aux deux appels précédents en mode kitchen sink, dans une requête dynamique, le moteur est capable de détecter un index manquant qui compte tenu du prédicat est tout à fait justifié.
Voici une manière plus académique de faire du T-SQL dynamique.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
USE [AdventureWorks2014] GO SET LANGUAGE English SET NOCOUNT ON DECLARE @CustomerId INT = NULL, @StartOrderDate DATETIME = '2006-05-01', @EndOrderDate DATETIME = NULL, @MinTotalDue MONEY = 1000, @SQLQuery AS NVARCHAR(1500) DECLARE @ParameterDefinition AS NVARCHAR(100) = N'@CustomerId INT, @StartOrderDate DATETIME, @EndOrderDate DATETIME, @MinTotalDue MONEY' SET @SQLQuery = 'SELECT COUNT(*) ' SET @SQLQuery += CHAR(10) + 'FROM Sales.SalesOrderHeader ' SET @SQLQuery += CHAR(10) + 'WHERE 1 = 1 ' IF @CustomerId IS NOT NULL SET @SQLQuery += CHAR(10) + 'AND CustomerId = @CustomerId' IF @StartOrderDate IS NOT NULL SET @SQLQuery += CHAR(10) + 'AND OrderDate >= @StartOrderDate' IF @EndOrderDate IS NOT NULL SET @SQLQuery += CHAR(10) + 'AND OrderDate <= @EndOrderDate' IF @MinTotalDue IS NOT NULL SET @SQLQuery += CHAR(10) + 'AND TotalDue >= @MinTotalDue' PRINT @SQLQuery EXECUTE sp_executesql @SQLQuery, @ParameterDefinition, @CustomerId, @StartOrderDate, @EndOrderDate, @MinTotalDue |
Là encore, l’index manquant est détecté.
Voici le résultat final en terme de coût, les requêtes dynamiques sont “légèrement” plus performante, 2% de gain mais c’est loin d’être suffisant.
Comme indiqué, pour les requêtes dynamiques, la détection d’index manquant se fait bien. Ajoutons notre index et rejouons nos 4 requêtes. Ha là c’est beaucoup mieux, on obtient un gain significatif sur les 3 derniers appels à savoir :
– Kitchen Sink : version 2
– Requête dynamique : EXEC
– Requête dynamique : sp_executesql
Et si on en profitait pour prendre connaissance du paramètre d’instance Optimize For Ad Hoc Workload et exécutons la requête dynamique EXEC avec une valeur de paramètre différente, @StartOrderDate DATETIME = ‘2006-05-01’. A l’arrivée, on se retrouve avec autant de plans que de valeurs de paramètre distinctes, sans parler de la taille des plans et des compilations à répétition.
1 2 3 4 5 |
SELECT bucketid, size_in_bytes, plan_handle, text FROM sys.dm_exec_cached_plans ecp CROSS APPLY sys.dm_exec_sql_text(ecp.plan_handle) sql WHERE 1 = 1 AND sql.text LIKE 'SELECT COUNT(*) %FROM%Sales.SalesOrderHeader%' |
On pourrait se contenter d’activer le paramètre d’instance Optimize For Ad Hoc Workload mais est-ce bien suffisant ? Non car contrairement à la requête dynamique sp_executesql, on ne peut pas bénéficier du mécanisme de cache des requêtes préparées, ni profiter de paramètre en sortie et en plus on est sujet à l’injection SQL. Bref, cela fait beaucoup de concessions au final.
Bien que la syntaxe du T-SQL dynamique soit laborieuse à appréhender et le debug pénible en pratique, c’est une méthode redoutablement efficace au niveau performance. Même si la détection d’index manquant n’est pas à prendre au pied de la lettre, elle est fonctionnelle pour les requêtes dynamiques. Dans le cas présent, cette proposition d’index était pertinente. Et enfin la méthode utilisée pour recourir à une requête dynamique a aussi son importance. Pour finir voici une session co-animé avec David Barbarin sur l’optimisation de requêtes sur la base des plans d’exécution dans laquelle la kitchen sink est évoquée.