Monitoring d'un serveur à base de service broker

Bonjour à tous,

Changement de service + vacances = presque 2 mois sans posts.... :-(

Pour ma première tâche en tant que DBA d'étude chez mon client préféré, j'ai eu l'occasion de retoucher des scripts qui permettent de monitorer un serveur SQL, et ceci en essayant d'impacter un minimum les performances de notre serveur...
En nous focalisant sur les problèmes de LOCK que peut rencontrer notre serveur, nous allons voir une méthode assez efficace et qui, personnellement, m'a permis de voir une première véritable utilisation du service broker de SQL Server 2005...Alors c'est parti :

Il nous faut tout d'abord créer une base de données qui contiendra un filegroup dédié au stockage de la file d'attente service broker...


1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE DATABASE [$(DatabaseMonitoring)] ON  PRIMARY 
( NAME = N'DBMonitoring' , FILENAME=@DefaultData+N'\DBMonitoring_1.mdf' ,SIZE = 3072KB , MAXSIZE = UNLIMITED, FILEGROWTH = 20%),
FILEGROUP [QueueStorage]
( NAME = N'DBMonitoring_2', FILENAME=@DefaultData+'\DBMonitoring_2.ndf' ,SIZE = 3072KB , MAXSIZE = 2048GB, FILEGROWTH = 20%)
LOG ON
( NAME = N'DBMonitoring_log', FILENAME=@DefaultLog+'\DBMonitoring.ldf' ,SIZE = 1024KB , MAXSIZE = 2048GB , FILEGROWTH = 10%)
)

ALTER DATABASE [$(DatabaseMonitoring)]
    SET TRUSTWORTHY ON

ALTER DATABASE [$(DatabaseMonitoring)]
    SET RECOVERY SIMPLE

Ensuite, il nous faut activer le service broker sur cette base :

1
2
3
4
5
6
IF NOT EXISTS (SELECT * FROM sys.databases
WHERE name = 'DBMonitoring'
AND IS_BROKER_ENABLED = 1)
BEGIN
ALTER DATABASE $(DatabaseMonitoring) SET ENABLE_BROKER ;
END

Nous créons ensuite les composants essentiels du broker : le service, la queue (sur le bon filegroup)

1
2
3
4
5
6
7
8
9
10
CREATE QUEUE NotifyQueue
ON QueueStorage;
GO

CREATE SERVICE NotifyService
ON QUEUE NotifyQueue
(
[http://schemas.microsoft.com/SQL/Notifications/PostEventNotification]
);
GO

Deux tables nous permettront de stocker les notifications au format brut (notification XML) [DLHistoric] et au format relationnel (découpé en champs) [DLEventsLog].
Sans décrire entièrement la procédure utilisée par le broker, nous allons en décrire les parties intéréssantes :
L'attente des messages :

1
2
3
4
5
6
7
 WAITFOR (
RECEIVE TOP(1)
@message_type_name=message_type_name,
@message_body=message_body,
@dialog = conversation_handle
FROM NotifyQueue
), TIMEOUT 2000

L'information intéréssante :

1
SET @EventType = @message_body.value('(//EventType)[1]','varchar(50)')


Nous capturons les évenements liés aux LOCK en XML :

1
2
IF (@EventType='DEADLOCK_GRAPH ' OR @EventType='LOCK_DEADLOCK' OR @EventType='LOCK_DEADLOCK_CHAIN' OR @EventType='LOCK_ESCALATION') 
    INSERT [DLHistoric] VALUES(getdate(),@EventType,@message_body)

Puis en relationnel :

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
IF @EventType='BLOCKED_PROCESS_REPORT'
    BEGIN
      INSERT INTO [dbo].[DLEventsLog]
         ([Session_ID]
         ,[EventTime]
         ,[Blocking_Spid]
         ,[Query_Statement]
         ,[WaitingForRessource]
         ,[Transaction_count]
         ,[ClientApplication]
         ,[Hostname]
         ,[LoginName]
         ,[WaitTime_ms]
         ,[LastBatchStartedTime]
           ,[lastBatchCompletedTime]
         ,[UpdateTime]
         ,[DB_Name]
         ,[lock_mode_request]
         ,[Isolationlevel]
         ,[Status]
         ,[PlanExec]
         ,[Transaction_id]
           ,[xactid]
           ,[BlockedProcesses]
           ,[UserConnections]
          )

    select   @message_body.value('(//blocking-process/process/@spid)[1]', 'int') as Session_ID
        ,@message_body.value('(//PostTime)[1]', 'datetime') as EventTime
        ,0 as Blocking_Spid
        ,@message_body.value('(//blocking-process/process/inputbuf)[1]', 'varchar(max)') as Query_Statement
        ,@message_body.value('(//blocked-process/process/@waitresource)[1]', 'varchar(50)') as WaitingForRessource
        ,@message_body.value('(//blocking-process/process/@transcount)[1]', 'int') as Transaction_count
        ,@message_body.value('(//blocking-process/process/@clientapp)[1]', 'varchar(50)') as ClientApplication
        ,@message_body.value('(//blocking-process/process/@hostname)[1]', 'varchar(50)') as Hostname
        ,@message_body.value('(//blocking-process/process/@loginname)[1]', 'varchar(50)') as LoginName
        ,@message_body.value('(//blocked-process/process/@waittime)[1]', 'int') as WaitTime_ms
        ,@message_body.value('(//blocking-process/process/@lastbatchstarted)[1]', 'datetime') as LastBatchStartedTime
        ,@message_body.value('(//blocking-process/process/@lastbatchcompleted)[1]', 'datetime') as LastBatchCompletedTime
        ,[UpdateTime]=getdate()
        ,db_name(@message_body.value('(//DatabaseID)[1]','int')) as [DB_Name]
        ,@message_body.value('(//blocked-process/process/@lockMode)[1]', 'varchar(50)') as Lock_Mode_Request
        ,@message_body.value('(//blocking-process/process/@isolationlevel)[1]', 'varchar(max)') as IsolationLevel
        ,@message_body.value('(//blocking-process/process/@status)[1]', 'varchar(50)') as [Status]
        ,(select qt.query_plan
          FROM sys.dm_exec_requests qs
          cross apply sys.dm_exec_query_plan(qs.plan_handle) as qt
          where qs.session_id=@message_body.value('(//blocking-process/process/@spid)[1]', 'int'))as PlanExec
        ,@message_body.value('(//TransactionID)[1]', 'int') as [Transaction_ID]
        ,@message_body.value('(//blocking-process/process/@xactid)[1]', 'int') as [xactid]
        ,(select top 1 cntr_value from sys.sysperfinfo where (object_name='SQLServer:General Statistics' OR object_name= (Select('MSSQL$'+convert(varchar,SERVERProperty ('InstanceName'))+':General Statistics'))) and counter_name ='Processes blocked') as BlockedProcesses
        ,(select top 1 cntr_value from sys.sysperfinfo where (object_name='SQLServer:General Statistics' OR object_name= (Select('MSSQL$'+convert(varchar,SERVERProperty ('InstanceName'))+':General Statistics'))) and counter_name ='User Connections') as USerConnections

A noter qu'un IF EXISTS permet à cette table de mettre à jour les événements déjà existants...Vous pouvez néanmoins disposer en parallèle d'une table d'historique (pensez à purger !).

Il nous reste a activer la file du broker :

1
2
3
4
5
6
ALTER QUEUE [dbo].[NotifyQueue]
WITH ACTIVATION (
STATUS = ON,
PROCEDURE_NAME = [LogEvents] ,
MAX_QUEUE_READERS = 2,
EXECUTE AS 'DBO'

C'est à l'aide de Notification Services que nous allons lever les évenements de LOCK et ainsi les capturer dans nos files service broker :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CREATE EVENT NOTIFICATION BlockedProcessNotification
  ON SERVER
  FOR BLOCKED_PROCESS_REPORT
  TO SERVICE 'NotifyService', 'current database';
  GO

  CREATE EVENT NOTIFICATION Deadlock_Graph_EventsNotification
  ON SERVER
  FOR DEADLOCK_GRAPH
  TO SERVICE 'NotifyService', 'current database';
  GO

  CREATE EVENT NOTIFICATION LOCK_EscalationNotification
  ON SERVER
  FOR LOCK_ESCALATION
  TO SERVICE 'NotifyService', 'current database';
  GO

Il ne restera plus qu'a créer des rapports ou créer des alertes sur le contenu de nos tables...

Nous verrons une autre fois une autre manière de monitorer de manière efficace un serveur, à l'aide des DMV...

Manipulation de bits

L'un des principaux avantages de posséder un langage riche est de pouvoir traiter certaines données à un niveau beaucoup plus bas que par des commandes classiques...

Les fonctions de manipulations de bits dans SQL Server en sont un bon exemple.

Pour rappel, on trouvera notamment :

  • le AND logique : &
  • le OR logique : |

Le gros avantage de ces fonctions bas niveau est un accroissement considérable des performances lors de traitements longs par exemple (insertion dans plusieurs tables en une seule procédure stockée).

Nous allons voir un exemple concret de manipulation de ces bits permettant d'exploser nos perfs (véridique sur un test à échelle réelle).

J'ai un formulaire de type "Dites-nous tout sur vous" dont je souhaite stocker en base les réponses des utilisateurs. Nous allons nous focaliser sur une question de ce questionnaire pour ne pas allonger ce post mais le principe restera le même pour l'ensemble du formulaire.

La question-exemple (à choix multiple) sera "comment occupez-vous vos loisirs ?". Les réponses seront les suivantes :

Sport, Culture-Découverte, Détente, Musique-Concerts, Auto-Moto, Cine-TV, Santé-Beauté-Forme, Bricolage, JeuxVideos, Voyages

Notre table d'utilisateur existe déjà :

T_Personne(PersonneId,Nom,Prenom,...)

Un cas classique voudrait que l'on conçoit nos tables de réponses de la façon suivante :

schéma1

On peut éventuellement se passer de la table T_Reponse dans notre cas puisque ses valeurs seront uniquement {Oui, Non, NULL}.

On voit néanmoins que pour chaque réponse de l'utilisateur, on insère une ligne dans la table d'association T_Reponse_Question. Idem pour la mise à jour voire la suppression...

La première étape de l'amélioration de nos perfs consiste à dénormaliser notre schéma, pour obtenir la table de réponses aux questions suivante (on conserve uniquement la table T_Personne et celle-ci) :

table_denormalized

Le champ PersonneId sera à la fois clé primaire et clé étrangère par rapport à la table T_Personne.

Côté code .NET, une enum "flags" dont la définition suit permettra de récupérer toutes les réponses :

[Flags] 
public enum Loisirs
{
Sport = 1
,CultureDecouverte = 2
,Detente = 4
,MusiqueConcert = 8
,AutoMoto = 16
,CineTV = 32
,SanteBeauteForme = 64
,Bricolage = 128
,JeuxVideo = 256
,Voyages = 512
}

Dans la page, si des CheckBox permettent à l'utilisateur de répondre, chaque réponse est récupérée comme suit :

monUser.Loisirs = ((chkReponse0.Checked ? Loisirs.Sport : 0) 
|(chkReponse1.Checked ? Loisirs.CultureDecouverte : 0)
| (chkReponse2.Checked ? Loisirs.Detente : 0)
| (chkReponse3.Checked ? Loisirs.MusiqueConcert : 0)
| (chkReponse4.Checked ? Loisirs.AutoMoto : 0)
| (chkReponse5.Checked ? Loisirs.CineTV : 0)
| (chkReponse6.Checked ? Loisirs.SanteBeauteForme : 0)
| (chkReponse7.Checked ? Loisirs.Bricolage : 0)
| (chkReponse8.Checked ? Loisirs.JeuxVideo : 0)
| (chkReponse9.Checked ? Loisirs.Voyages : 0));

Ainsi, côté SQL, pour une insertion ou une mise à jour, notre procédure stockée aura comme paramètre une variable @Loisirs de type INT, utilisée comme suit :

INSERT INTO [T_Question_Reponse_Denormalized] 
([PersonneId]
,[Sport]
,[Culture-Decouverte]
,[Detente]
,[Musique-Concert]
,[Auto-Moto]
,[Cine-TV]
,[Sante-Beaute-Forme]
,[Bricolage]
,[JeuxVideo]
,[Voyages])
VALUES
(1
,@Loisirs & 1
,@Loisirs & 2
,@Loisirs & 4
,@Loisirs & 8
,@Loisirs & 16
,@Loisirs & 32
,@Loisirs & 64
,@Loisirs & 128
,@Loisirs & 256
,@Loisirs & 512)


Pour récupérer nos réponse, notre SELECT ressemblera à ceci :

SELECT [PersonneId] 
,([Sport] * 1
|[Culture-Decouverte] * 2
|[Detente] * 4
|[Musique-Concert] * 8
|[Auto-Moto] * 16
|[Cine-TV] * 32
|[Sante-Beaute-Forme] * 64
|[Bricolage] * 128
|[JeuxVideo] * 256
|[Voyages] * 512) AS Loisirs
FROM [COACH].[dbo].[T_Question_Reponse_Denormalized]
WHERE PersonneId = 1

Enfin, côté code .NET, un simple :

(Loisirs)(int)reader["Loisirs"]; 

permettra de récupérer l'ensemble des réponses pour la question.

On remarquera donc comme principaux avantages :

  • manipulation de bits donc accroissement des performances générales
  • une seule ligne impactée pour l'insertion / la mise a jour / la suppression

L'inconvénient majeur à mon avis est l'extension du nombre de questions / réponses qui impose la création d'une nouvelle table par question et d'une nouvelle colonne par réponse....

Si les performances restent la contrainte numéro 1 (ce qui est le cas sur des applications Web), cette solution est à envisager...

Conventions de nommage

Il m'est arrivé, pour le compte de mon client préféré, de devoir rédiger une documentation sur les conventions de nommage des scripts et objets de BD.

Après maintes recherche sur le web....rien du tout ou presque ! Enfin...que des directives différentes, voire contradictoires.
J'ai donc repris mes bouquins MS que j'utilisais pour passer mes certifs et le point est effectivement abordé, résumée par la phrase suivante : ce qui importe, ce n'est pas la convention de nommage utilisée, c'est que votre système d'information dispose d'une convention de nommage et l'applique.

Fort de ce principe, je me suis donc attelé à la tâche. Certains principes que j'ai pu retrouver dans différents SI ont été repris. En voici une shortlist :
  • Utilisation de préfixes pour différencier les objets de bases de données
  • Références de mêmes noms
  • Suffixe pour les champs identifiants
  • ...
Voici donc un exemple assez restreint mais qui aborde l'essentiel d'une nomenclature sous SQL Server :
  • les bases : aucune préconisation particulière mise à part éviter les symboles non alphanumériques, souvent en majuscules...
  • les tables : T_Personnes
    • le modèle relationnel impose des noms d'entités au singulier mais bien souvent, pour raisons historiques, on conserve des noms au pluriel pour les tables
  • Les vues : V_PersonnesActives
  • les champs : PersonneId
    • pour le champ identifiant
  • Prenom
    • une majuscule puis minuscule pour les champs standards
  • les champs qui sont clés étrangères : PersonneId
    • même nom que les clés primaires référencées
  • Les clés :
    • PK_Personne ou PKC_Personne si l'index est clustered pour une clé primaire
    • FK_Personne_Addresse pour une clé étrangère (les noms des deux tables sont indiqués).
  • Les indexs :
    • IXF_Personne_Prenom pour un index non unique et non clustered
    • IXU_Personne_Surnom pour un index unique non clustered
  • Les triggers : TR_Personne_CheckPersonne
  • Les contraintes :
    • CK_Identity_CheckControle : contrainte de type CHECK, porte sur le type de vérification
    • DF_DateCreation : contrainte de type DEFAULT
    • U_Surnom ou UC_Surnom voire UK_Surnom mais je suis moins fan, pour les contraintes UNIQUE.
  • Les fonctions : F_PERSONNE_AjouteAmi,
    • le nom de la table peut être remplacé par le nom du domaine fonctionnel dans lequel s'exerce la fonction (ex : F_USERSECTION_AjouteAmi)
  • Les procédures stockées : idem fonctions : P_PERSONNE_AjouteAmis

Quoi qu'il en soit, le principal objectif d'une bonne nomenclature est de bien différencier les nombreux objets de BD utilisés, pour pouvoir identifier rapidement l'un d'entre eux, car très vite, on peut se retrouver noyer dans un flot de code SQL...

@@ROWCOUNT - 2eme Partie

Quelques  doutes m'assaillant depuis mon post sur cette variable, j'ai réalisé quelques tests très simples et consulté notre bible à tous..la MSDN :-)

Voici l'exemple très simple (la table T1 contient 5 lignes) :

DECLARE @i INT 
SET @i = 5 
SELECT * FROM T1 
IF(@i = 5) 
BEGIN  
    PRINT 'OK'  
    PRINT @@ROWCOUNT 
END 
ELSE 
BEGIN  
    PRINT 'KO'  
    PRINT @@ROWCOUNT 
END
END


Et le résultat :

 

(5 ligne(s) affectée(s))
OK
0


L'instruction IF renvoie donc O (ce qui paraît logique).
Je n'ajoute pas de PRINT @@ROWCOUNT avant ce IF car le PRINT affecterais la valeur 0 à notre variable (je l'ai fait au préalable de cet exemple pour vérifier que l'on ai bien la valeur 5 renvoyé).

 

De la même manière les instructions de type

 

  • d'affectation (SET, SELECT sans requêtes comme SELECT GETDATE()) renvoient un @@ROWCOUNT  = 1
  • toute requête de sélection ou retour de fonction renvoient un @@ROWCOUNT  = nombre de lignes affectées
  • EXECUTE permet de conserver la valeur antérieure de @@ROWCOUNT
  • Les débuts et commit de transaction réinitialisent notre @@ROWCOUNT à 0

Enfin notons que l'instruction SET ROWCOUNT int permet de définir le nombre de lignes maximum traitées par une requête. Dès que int est atteint, le moteur arrête de traiter le reste de la requête (incluant triggers et les types de curseurs KEYSET et INSENSITIVE).

Pour terminer avec ce ROWCOUNT, notons également le ROWCOUNT_BIG() qui renvoie un BIGINT au lieu d'un INT pour les valeurs > 2 milliards

une histoire de scope...

Il m'arrive souvent de répondre à la question suivante : Quelle est la différence entre @@IDENTITY et SCOPE_IDENTITY() ?? Est-ce la portée (locale à la connexion ou globale) ??
La différence n'est pas un problème de portée au sens environnement de connexion mais plus au niveau cascading.
Je m'explique. Tout d'abord rappelons que ces deux fonctions permettent de renvoyer la valeur de l'identity de la dernière insertion effectuée dans une connexion.

Soit deux table :

      CREATE TABLE [dbo].[T1](
    Id [int] IDENTITY(1,1) NOT NULL,
    name [nchar](10) NULL, 
CONSTRAINT [PK_T1] PRIMARY KEY CLUSTERED ([Id] ASC)

Et

CREATE TABLE [dbo].[T2](
    Id [int] IDENTITY(1,1) NOT NULL,
    name [nchar](10) NULL, 
CONSTRAINT [PK_T2] PRIMARY KEY CLUSTERED ([Id] ASC)

On souhaite faire une insertion dans T2 à partir d'un trigger de type AFTER INSERT dans T1. Notre trigger ressemblerait a ça :

CREATE TRIGGER [dbo].[TR_T1] ON [dbo].[T1]
AFTER INSERT 
AS
BEGIN  
    INSERT INTO T2(name) VALUES ('test') 
END

Nous insérons 5 lignes dans T2 directement pour avoir un numéro d'identity différent de celui de T1. A ce moment,

  • T1 contient 0 ligne sa valeur d'IDENTITY est de 1

    • à noter sa valeur est de 1 alors qu'aucune insertion n'a encore été faite.

  • T2 contient 5 lignes sa valeur d'IDENTITY est de 5.

Enfin, on effectue notre insertion dans T1 et on récupère l'identity avec nos deux méthodes : 

INSERT INTO T1(name) VALUES ('test') 
PRINT @@IDENTITY
PRINT SCOPE_IDENTITY()

Et le résultat :

(1 ligne(s) affectée(s)) 
(1 ligne(s) affectée(s))
6
1

Et oui ! le @@IDENTITY renvoie le dernier identity inséré quelque soit le niveau dans lequel cette insertion s'est faite. Le SCOPE_IDENTITY() va toujours faire référence à l'insertion du niveau dans lequel cette fonction est exécutée.
En conclusion, si vous n'êtes pas sûr de l'existence de triggers sous-jacents, préférez SCOPE_IDENTITY pour ne pas avoir de mauvaise surprise....
A noter également la fonction 

SELECT IDENT_CURRENT('nom_table') 

qui renvoie l'identity de la dernière insertion, toute session confondue pour une table nom_table donnée.

A bientôt...

IF et @@ROWCOUNT

Suite à des petits soucis que j'ai eu lors de l'exécution d'une procédure stockées (mauvaise valeur du nombre de lignes affectées),
arrêtons-nous quelques instants sur cette variable SQL Server qu'est le @@ROWCOUNT.
Nous savons qu'elle renvoie le nombre de lignes affectées dans une session donnée.
exemple :

UPDATE T_Personne
SET PersonneName = 'coach'
WHERE PersonneId = 1
SELECT @@ROWCOUNT

Nous renverra 1 (si PersonneId est la PK de la table T_Personne).
Maintenant retenons que la condition SQL IF que l'on peut trouver sous diverses formes telles que :

IF EXISTS(...)
IF(@var = 1)

est une instruction SQL (en tout cas vu comme tel par le moteur..)

et vous obtenez la mise à jour de votre variable @@ROWCOUNT (en général elle renvoie la valeur 1) lors d'un IF...

Il fallait le savoir....

Amis de SQL Server, bonjour !

Et bienvenue sur ce modeste blog à tous ceux qui auront la curiosité de venir le consulter .

D'un profil plutôt orienté data, je suis ingénieur consultant pour une SSII parisienne bien connue en ces lieux (qui a dit winwise :-)).

En poste chez un grand site marchand, je me suis retrouvé à travailler dans une équipe de développeurs (qu'ils sont forts ces commerciaux !!), pour me rendre compte qu'un certain nombre de petites astuces ou notions qu'ils venaient chercher de mon côté, leur rendaient la vie beaucoup plus facile...

J'ai donc eu l'idée de blogguer sur toutes ces petites choses qui permettent non pas aux grands gourou du SQL, mais aux développeurs, ou DBA juniors, d'y retrouver quelques articles utiles.

Comme tout a une fin, je vais bientôt quitter mes amis développeurs pour l'équipe DBA ce qui risque d'influencer la nature de mes posts : ceux-ci devraient être orientés développement dans un premier temps, puis probablement plus administration par la suite.

Etant junior et ayant encore énormément à apprendre sur ce magnifique outil qu'est SQL Server, ce blog suivra plutôt un modèle d'apprentissage initiatique, au fil de mes découvertes et je tenterai de vous faire partager au quotidien ma modeste expérience ainsi que diverses astuces, techniques ou infos sur SQL Server 2005 puis 2008 lorsque je commencerai à bien jouer avec...

En espérant qu'il puisse vous être utile sur une difficulté ponctuelle, ou simplement pour apprendre ensemble le fonctionnement interne du moteur, je vous souhaite bonne lecture....

Pour finir, mon mot d'ordre : partageons ! :-)...


Les 10 derniers blogs postés

- Office 365: Script PowerShell pour assigner des droits Full Control à un groupe défini par Blog Technique de Romelard Fabrice le 04-30-2017, 09:22

- SharePoint 20XX: Script PowerShell pour exporter en CSV toutes les listes d’une ferme pour auditer le contenu avant migration par Blog Technique de Romelard Fabrice le 03-28-2017, 17:53

- Les pièges de l’installation de Visual Studio 2017 par Blog de Jérémy Jeanson le 03-24-2017, 13:05

- UWP or not UWP sur Visual Studio 2015 ? par Blog de Jérémy Jeanson le 03-08-2017, 19:12

- Désinstallation de .net Core RC1 Update 1 ou SDK de Core 1 Preview 2 par Blog de Jérémy Jeanson le 03-07-2017, 19:29

- Office 365: Ajouter un utilisateur ou groupe dans la liste des Site collection Administrator d’un site SharePoint Online via PowerShell et CSOM par Blog Technique de Romelard Fabrice le 02-24-2017, 18:52

- Office 365: Comment créer une document library qui utilise les ContentTypeHub avec PowerShell et CSOM par Blog Technique de Romelard Fabrice le 02-22-2017, 17:06

- [TFS] Supprimer en masse les dépendances à SQL Enterprise ou Developer avant de procéder à une migration par Blog de Jérémy Jeanson le 02-20-2017, 20:30

- Office 365: Attention au volume utilisé par les fichiers de Thèmes de SharePoint Online par Blog Technique de Romelard Fabrice le 02-07-2017, 18:19

- [SCVMM] Supprimer une machine bloquée par Blog de Jérémy Jeanson le 01-31-2017, 21:22