Quelqu'un d'autre a-t-il des mauvais projets lorsque vous appelez CHANGETABLE
dans SQL Server 2016?
J'ai une application qui utilise le suivi des modifications pour mettre à jour un cache. Les serveurs d'applications obtiennent les modifications pour plusieurs tables toutes les secondes. Le CHANGETABLE
est appelé si souvent qui retourne généralement quelques rangées. Cela fonctionne bien en 2008 R2 et 2012. Lorsque j'ai testé 2016, j'ai constaté que la CPU a raccordé et a constaté que le plan balaye les changements de suivi des tables internes dans lesquelles il faisait une recherche.
Si vous voulez voir ce comportement, voici les étapes pour le répliquer:
Créer une table:
CREATE TABLE [dbo].[ChangeTable_Test](
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[Name] [varchar](50) NOT NULL,
CONSTRAINT [PK_Test_ChangeTable_Id] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[ChangeTable_Test] ENABLE CHANGE_TRACKING
GO
INSERT INTO ChangeTable_Test (Name) VALUES ('Test')
GO 1000
Inclure le plan d'exécution réel et exécuter ce qui suit.
SET STATISTICS IO ON;
SET NOCOUNT OFF;
ALTER DATABASE SCOPED CONFIGURATION SET QUERY_OPTIMIZER_HOTFIXES = OFF;
GO
ALTER DATABASE [Test] SET COMPATIBILITY_LEVEL = 110
GO
DECLARE @CurrentVersion INT;
SELECT @CurrentVersion=CHANGE_TRACKING_CURRENT_VERSION ()-1;
SELECT * FROM CHANGETABLE(CHANGES ChangeTable_Test,@CurrentVersion) AS C
GO
ALTER DATABASE [Test] SET COMPATIBILITY_LEVEL = 130
GO
DECLARE @CurrentVersion INT;
SELECT @CurrentVersion=CHANGE_TRACKING_CURRENT_VERSION ()-1;
SELECT * FROM CHANGETABLE(CHANGES ChangeTable_Test,@CurrentVersion) AS C
La démonstration ci-dessus travaille pour moi sur SQL Server 2016 SP1-CU4. J'ai téléchargé deux plans pour coller le plan . Le premier est sous Compat 110 et le second est inférieur à 130.
Le support Microsoft a reconnu le problème mais ne prévoit pas de le réparer. Ils ont suggéré des travaux de contournement en utilisant un USE PLAN
Astuce ou guides de plan. Mais cela ferait un cauchemar de maintenance pour ce produit.
La chose amusante est que 2014 et 2017 ne choisissez pas les mauvais projets et utilisent le nouvel estimateur de cardinalité.
Avez-vous des suggestions à côté de celles que j'ai mentionnées?
Sur la base de votre question, il semble que vous ayez une application très concurrente qui appelle CHANGETABLE
sur une variété de tables dans de nombreuses requêtes différentes. Forcer chaque plan de requête à avoir une forme spécifique avec un USE PLAN
un indice ou un guide de plan serait un effort herculéen. Changer votre base de données active Pour avoir un niveau de compatibilité de 110 sans fixation de requête peut être une mauvaise option pour d'autres raisons.
La suggestion de cette réponse pourrait être difficile à mettre en œuvre, mais elle ressemble à une meilleure option que celles énumérées ci-dessus. L'idée est de remplacer l'appel à CHANGETABLE
avec une procédure stockée qui insère les données de CHANGETABLE
dans une table TEMP. Les requêtes qui nécessitent des données de changement peuvent utiliser la table Temp remplissante au lieu de CHANGETABLE
directement. Votre code d'application pourrait ressembler à ceci:
DECLARE @CurrentVersion INT = CHANGE_TRACKING_CURRENT_VERSION () - 1;
CREATE TABLE #changes (SYS_CHANGE_VERSION bigint NULL);
EXEC dbo.CHANGETABLE_API '#changes', 'TEST', 'dbo', 'ChangeTable_Test', @CurrentVersion;
-- run queries that need #changes temp table
DROP TABLE #changes;
Travailler autour de problèmes de cadrage nécessite quelques astuces. La procédure stockée a trois parties:
SYS_CHANGE_CREATION_VERSION
, SYS_CHANGE_OPERATION
, SYS_CHANGE_COLUMNS
, et SYS_CHANGE_CONTEXT
sys.columns
et d'autres DMV pour cela.CHANGETABLE
dans le contexte d'une base de données vide avec le niveau de compatibilité de 2012 et quels que soient les autres paramètres, vous devez obtenir de bonnes performances.Vous trouverez ci-dessous une mise en œuvre rapide et sale de la plupart des éléments ci-dessus. Il n'a aucun type de vérification ou de protection des erreurs contre les attaques d'injection SQL. N'hésitez pas à faire ce que vous voulez avec le code:
CREATE OR ALTER PROCEDURE dbo.CHANGETABLE_API (
@temp_table_name SYSNAME,
@changes_database_name SYSNAME,
@changes_schema_name SYSNAME,
@changes_table_name SYSNAME,
@last_sync_version BIGINT
)
AS
BEGIN
DECLARE @sql_to_add_static_cols NVARCHAR(4000),
@sql_to_add_table_cols NVARCHAR(4000),
@sql_to_insert_rows NVARCHAR(4000);
-- okay to hardcode these depending on how you call CHANGETABLE
SET @sql_to_add_static_cols = N'ALTER TABLE '
+ QUOTENAME(@temp_table_name)
+ N' ADD '
+ N'SYS_CHANGE_CREATION_VERSION bigint NULL, '
+ N'SYS_CHANGE_OPERATION nchar(1) NULL, '
+ N'SYS_CHANGE_COLUMNS varbinary(4100) NULL, '
+ N'SYS_CHANGE_CONTEXT varbinary(128) NULL';
EXEC (@sql_to_add_static_cols);
-- this should be dynamic based on sys.columns and other dmvs
SET @sql_to_add_table_cols = N'ALTER TABLE '
+ QUOTENAME(@temp_table_name)
+ N' ADD id BIGINT NOT NULL';
EXEC (@sql_to_add_table_cols);
-- key is to run the insert in a database with the settings that you need for a good query plan
SET @sql_to_insert_rows = N'USE DB_2012_COMPAT; '
+ N'INSERT INTO '
+ QUOTENAME(@temp_table_name)
+ N' SELECT * FROM CHANGETABLE(CHANGES '
+ QUOTENAME(@changes_database_name)
+ N'.' + QUOTENAME(@changes_schema_name)
+ N'.' + QUOTENAME(@changes_table_name)
+ N', ' + CAST(@last_sync_version AS NVARCHAR(30))
+ N') AS C';
EXEC (@sql_to_insert_rows);
END;
Lorsque j'appelle la procédure stockée dans une base de données avec compatibilité Niveau 130, je reçois un plan de requête qui ne recherche que si vous le souhaitez:
Dans la même session si j'exécute le code suivant:
DECLARE @CurrentVersion INT = CHANGE_TRACKING_CURRENT_VERSION ()-1;
SELECT * FROM CHANGETABLE(CHANGES ChangeTable_Test,@CurrentVersion) AS C
OPTION (RECOMPILE);
Je reçois un scan que vous vouliez éviter: