vendredi 7 février 2014

[GUSS] Election du board

Si vous êtes professionnel de SQL Server vous connaissez sans doute le GUSS dont l'objectif principal est de réunir les acteurs de la communauté en organisant un certains nombres d'événements autour de sujets techniques ou tout simplement conviviaux pour discuter de tout et de rien.

Le GUSS se modernise et pour la première fois propose une élection des membres de son board qui auront comme charge de décider de l'avenir du GUSS. Cette élection est importante car elle vous permet de choisir vos représentants pouvant soutenir et défendre vos propositions.

élection board guss

Vous trouverez la liste des différents candidats à l'adresse suivante :

http://guss.pro/candidats-au-board-du-guss-2014/

Les votes se dérouleront du 10 au 24 Février, il y aura bientôt plus d'informations sur le déroulement du scrutin. Restez attentifs aux publications sur le site du GUSS.

Ca y est vous savez tout. Maintenant il ne vous reste plus qu'à voter. On compte sur vous.

samedi 7 décembre 2013

[JSS2013] - Merci aux participants de ma session

Merci à tous les participants à la session "SSAS : Test de montée en charge avec Visual Studio. Ce fut un plaisir pour moi de présenter ce sujet orienté performance et optimisation, sujets qui sont ô combien important pour moi. J'espère avoir suscité l'envie d'utiliser la fonctionnalité load test de visual studio dans le cadre des tests de montée en charge. Pour ceux qui ne l'ont pas déjà fait, il ne vous reste plus qu'à acquérir une licence Ultimate de Visual Studio pour vous lancer dans l'aventure smiley.

WAISSO était très présent pour cet opus 2013 avec la présentation à la Keynote "10 ans de DBA" de la 2ème journée de Sham Unmar et Philippe Pimenta, la présentation sur l'agilité "le TDD avec DbFit et l'intégration continue avec TFS" présenté par Damien Maresma et la mienne. Un grand bravo à eux pour leur prestation.

Je vous laisse en téléchargement ma présentation au format PDF, dès que le webcast sera disponible je vous le mettrais à disposition.

JSS 2013

jeudi 7 novembre 2013

Venez nous rencontrer aux journées SQL Server 2013.

JSS 2013

Nous approchons à grand pas du plus gros événement autour de SQL Server en France, les Journées SQL Server. Elle se tiendront le 2 et 3 décembre au centre de conférence de Microsoft, à Issy-Les-Moulineaux.

Pour la troisième année consécutive, WAISSO sera partenaire Platinum de cet événement. Nous aurons le plaisir de présenter 2 sessions orientées productivité de développement BI.

Pour ma première participation en tant que speaker à un événement, j'ai choisi de remettre au goût du jour une bonne vieille recette, apparu en 2005, les stress tests SSAS à partir de Visual Studio. Dans cette session on parlera de design, d'optimisation, de performance et d'architecture sur SSAS bien évidemment, Multidim bien sûr et Tabular peut-être (teasing... smiley). Bien sûr nous travaillerons sur les dernières versions, SQL Server 2014 CTP 2 et Visual Studio Ultimate 2013.

Si vous avez des problématiques en lien avec ce sujet que vous auriez aimé aborder vous pouvez toujours me les soumettre. J'essaierais tant que possible de prendre en compte vos remarques.

Au plaisir de vous rencontrer le 2 et 3 décembre prochain.

JSS 2013

mardi 23 juillet 2013

[GUSS] Afterwork d'août 2013

Vous ne le saviez peut-être pas, mais le GUSS, la communauté francophone des utilisateurs de SQL Server organise des afterworks tous les 2 mois afin de permettre à tous les professionnels du produit de se réunir et d’échanger sur les nouveautés, sur des retours d’expériences, des missions ou même des opportunités.

GUSS

Le prochain se tiendra le mercredi 7 août à partir de 19H00 dans un petit bar choisi avec soin, l’Imprévu situé au 35, rue Didot 75014 Paris.

afterwork guss

Venez nous y rencontrer, le thème « Paris, SQL & Sun » servira de base à de bons échanges très techniques et enrichissant j’en suis sûr.

Pour vous y rendre : Metro 13 station Pernety (pas très loin de Montparnasse).

Pour vous y inscrire : http://guss.fr/2013/08/07/afterwork-aout-2013/

vendredi 10 mai 2013

[SSIS] Lookup paramétré en mode Full Cache

La non équi-jointure est-elle possible en mode ETL pure avec SSIS ?

Nombreux sont les cas d'utilisation où la jointure peut être un peu plus complexe qu'une égalité entre les colonnes de 2 tables. Dans le cas simple et répandu de la récupération d'un ID d'une dimension SCD2 dans le chargement d'une table de fait, il est nécessaire de recourir à une non équi-jointure. Et pourtant cette simple tâche parait complexe à faire de manière performante à l'aide de SSIS.

Rappel: La non équi-jointure consiste en l'utilisation d'opérateur de jointure différent de l'égalité comme par exemple : <, <=, >, >=, LIKE, BETWEEN ... AND ..., etc ...

Comment met-on en place le lookup paramétré dans SSIS

Prenons un exemple simple de la récupération d'un seul ID sur une dimension que l'on appellera Produit.

tables SQL Server
Prenons 2 tables simples pour illutrer le lookup paramétré
dataflow
Créons un dataflow simple avec un lookup pour récupérer l'ID d'une dimension
lookup nocache
Commençons par choisir le mode nocache pour l'exemple
lookup connection
On écrit une requête avec dans la sélection uniquement les colonnes dont on a besoin, l'ID et les colonnes impliqués dans la paramétrisation. Le but est d'avoir des metadata identique avec la requête custom
lookup columns
On map les colonnes sources qui seront les paramètres de la requête custom
lookup custom query
On utilise le ? pour signifier un paramètre
lookup parametres
Et on map les paramètres dans l'ordre d'apparition des ?

ATTENTION: Vous l'avez sans doute remarqué. Les performances de cette méthode ne sont pas satisfaisante. Mais pourquoi me direz-vous ? C'est ce que l'on va tenter d'éclaircir.

Je vous propose d'allumer notre SQL Server Profiler et lancer une trace standard pour capturer les classes d'évenement RPC:Completed, SQL:BatchStarting et SQL:BatchCompleted

sql server profiler
Voilà le résultat de la trace

L'intégralité des traces seraient trop long et indigeste. Je vais en choisir quelques extraits pour les commenter.

  • La trace commence par une phase préparatoire
    exec sp_prepare @p1 output,NULL,N'SELECT [IdProduit]
          ,[CodeProduit]
          ,[Valid_From]
    FROM [dbo].[DimProduit] dp',1
    select @p1
    go
    exec [sys].sp_describe_first_result_set N'SELECT [IdProduit]
          ,[CodeProduit]
          ,[Valid_From]
    FROM [dbo].[DimProduit] dp',NULL,1
    go
    exec sp_unprepare 3
    go
    exec [sys].sp_describe_undeclared_parameters N'SELECT [IdProduit]
          ,[CodeProduit]
          ,[Valid_From]
    FROM [dbo].[DimProduit] dp
    WHERE
     @P1 >=  dp.[Valid_From]
    and @P2 <= ISNULL(dp.[Valid_To],''9999-12-31'')
    and   dp.[CodeProduit]=@P3'
    
  • Puis elle termine par un appel de procédure par ligne
    exec sp_executesql N'SELECT [IdProduit]
          ,[CodeProduit]
          ,[Valid_From]
    FROM [dbo].[DimProduit] dp
    WHERE
     @P1 >=  dp.[Valid_From]
    and @P2 <= ISNULL(dp.[Valid_To],''9999-12-31'')
    and   dp.[CodeProduit]=@P3',N'@P1 date,@P2 date,@P3 char(10)','2013-05-02','2013-05-02','FR00000001'
    go
    

Un appel par ligne ? Un traitement séquentiel sur un moteur relationnel ? On est loin de l'état de l'art de l'ETL et même de l'état de l'art du relationnel. Vous me direz : Oui mais ... là nous avons paramétré le lookup en mode NoCache.

    Il existe effectivement 2 autres modes :
  • le mode Partial Cache
  • et le mode Full Cache

Ce que va changer le mode Partial Cache par rapport au mode NoCache, c'est qu'en début d'exécution le cache sera vide et au fur et à mesure des lectures, les lignes seront ajoutés au cache, ce qui permettra aux nouvelles lignes identiques d'être servies par le cache. Ce mode est très légerement plus performant que le mode NoCache, mais reste très peu performant et loin de la philosophie ETL.

Le mode FullCache permet de monter en cache l'intégralité des données et de traiter les jointures en mémoire de manière performante. Cependant, SSIS ne permet faire des non équi-jointure dans le cache.

A savoir: L'interface empêche de paramétrer des requêtes custom en mode FullCache aussi bien par l'interface que par les propriétés du composant ou que par les expressions du dataflow.

Propriété en PartialCache

lookup parametres

Propriété en FullCache

lookup parametres






On constate que les propriétés de paramétrisation ont disparu.

Mais alors quelle solution choisir dans le cas d'une non équi-jointure ?

En SSIS pure il reste une méthode à base de Merge Join. Mais dont l'inconvénient serait la nécessité de trier les datasets amont ce qui serait parfaitement contre-performant et vraiment non recommandable à partir du moment ou il y a plus d'une jointure de ce type à faire dans un même dataflow.

On pourrait imaginer essayer de bluffer SSIS par une expression pour lui passer dans sa propriété SqlCommand une variable contenant une requête dynamique. Mais cette feinte ne suffirait pas. Il faudrait aller plus loin à coup de requêtes paramétrés, de jointure avec une date et de boucle, pour essayer de se retrouver en situation d'équi-jointure. Une machine à gaz indigeste, contre-performante et de surcroît inmaintenable.

Force est de constater que d'utiliser SSIS en tant qu'ETL (plutôt qu'en ELT ou en séquenceur de procédures stockées) pour traiter ce genre de problématique n'est pas une solution sérieusement envisageable si l'on a beaucoup de volumétrie. Dans ce cas, il faut de manière délibérer utiliser le moteur SQL, qui est de toute façon utilisé contre nature par SSIS. Donc tant qu'à utiliser le moteur sans le savoir et de manière non maitrisé autant le faire à fond avec toute la puissance du moteur (requêtes ensemblistes, utilisation d'indexes et de statistiques) afin d'obtenir des résultats performants.

A bientôt!

jeudi 4 avril 2013

[SSAS][AMO] Construire son propre outil de gestion dynamique de cubes

Le modèle Tabular est arrivé. Mais le modèle dimensionnel n’a pas dit son dernier mot. Bon nombre d’entre vous possède encore des cubes dans ce mode. Et il faudra encore un peu de temps avant que les cubes volumineux puissent monter intégralement en mémoire.

Et justement, dans le cas où les cubes sont volumineux, il est indispensable d’avoir une stratégie de partitionnement précise afin de stabiliser les performances de calcul des cubes mais aussi d’obtenir de bonnes performances d’accès aux données.

Le partitionnement doit être ajusté aux spécificités de chaque partition et doit être aussi dynamique. Il doit maîtriser le nombre de partitions afin de ne pas saturer l’instance par un nombre de fichiers trop important qui aurait un effet contraire à celui escompté.

ATTENTION : Un trop grand nombre de partitions peut avoir des inconvénients comme le ralentissement des mises en production en alourdissant le script XMLA de déploiement, ou comme un temps de démarrage plus long du service SSAS, ou encore un ralentissement du temps de traitement des dimensions, ou bien encore un discover plus long des partitions, etc …

La modération dans la création du nombre de partition est de mise. Une stratégie réfléchie doit être appliquée.

Je vous donne un lien vers le « Analysis Services 2008 R2 Performance Guide » http://sqlcat.com/sqlcat/b/whitepapers/archive/2011/10/10/analysis-services-2008-r2-performance-guide.aspx.

Ce guide vous permettra d’affiner votre stratégie de partitionnement ( et plus encore).

    Rendre la gestion de son partitionnement dynamique est essentiel :
  • Chaque partition possède des spécificités propres (clé de partitionnement et granularité (requête source), type de stockage (MOLAP, ROLAP, HOLAP), emplacement de stockage, design d’agrégation) 
  • Chaque partition nécessite un mode de rafraichissement spécifique: Recalcule sur une période glissante, calcul de données dans le futur.
  • Les exigences de performances peuvent être variables en fonction de l’ancienneté des données. Il faut pouvoir profiter de cette exigence pour fusionner les partitions et pourquoi pas changer le type de stockage de MOLAP vers ROLAP. 
  • Créer les partitions à l’avance n’est pas une bonne chose. Il faut que l’application soit capable de créer les partitions quand il en a besoin et seulement dans ce cas.

Le traitement des dimensions doit lui aussi être dynamique. Dans un mode de croisière en fonction du type de modification on préferera le mode de processing ProcessAdd au ProcessUpdate et réciproquement. Néanmoins, il faudra que l’application soit capable de détecter le statut de processing de la dimension pour faire un ProcessFull quand ce dernier est obligatoire.

Pour toutes ces raisons, il faut rendre dynamique la gestion des processing des cubes. La meilleur approche pour le faire c’est d’utiliser l’API AMO via un language tel que Powershell, C# ou VB. Je pense que pour des raisons de maintenabilité et de cohérence d’infrastructure avec le reste de l’application ETL, il est préférable d’embarquer les scripts dans un package SSIS. De ce fait, mieux vaut préférer C# ou VB qui peuvent s’écrire dans des Scripts Tasks SSIS. Pour des raisons d’affinité avec le language C#, je construirais mes exemples autour ce celui-çi.

L’idée n’est pas de vous assister totalement mais de vous donner les clés pour construire votre outil. Il va de soi que toutes les règles de développement s’appliquent à savoir gestion des erreurs, variabilisation, création de classes, de méthodes, d’utilisation (ou pas) de listes génériques, …

Oui même en BI on soigne son code.

  1. Référence à la librairie.
  2. La première chose est de référencer notre librairie.
    using AMO = Microsoft.AnalysisServices;
    
  3. Connexion à la base :
  4. AMO.Server srv = new AMO.Server();
    srv.Connect("MonServeur");
    AMO.Database db = srv.Databases.FindByName(initialcatalog);
    
  5. Capture de la commande XMLA au serveur :
  6. Pour générer une commande à lancer au serveur il suffit de capturer un ensemble d’action entre 2 bornes marquées par la propriété CaptureXml. La valeur true signifie que la capture commence. La valeur false que la capture s’arrête.

    srv.CaptureXml = true;
    … (Voir dans le 3)
    srv.CaptureXml = false;
    
    
  7. Les différentes commandes :
  8. Voici quelques commandes possibles pour gérer vos cubes.

    1. suppression des partitions:
    2. Soit il est possible de supprimer l’intégralité des partitions de tous les cubes comme suit :
      foreach (AMO.Cube Cube in db.Cubes){
           foreach (AMO.MeasureGroup Meg in Cube.MeasureGroups){
                foreach (AMO.Partition part in Cube.Partitions){
                        part.Drop();     
                }
           }
      }
      
      

      Soit il est possible de supprimer une partition en fonction d’une liste :

      AMO.Cube cube = db.Cubes.FindByName("BaseASNomCube");
      AMO.MeasureGroup Meg = cube.MeasureGroups.FindByName("BaseASNomGroupeMesure");
      AMO.Partition part = Meg.Partitions.FindByName("BaseASNomPartition");
      part.Drop();
      
    3. La création des partitions :
    4. AMO.Partition part = Meg.Partitions.FindByName("NomPartition");
      if (part == null)
      {
      part = Meg.Partitions.Add("NomPartition");
      switch ("MonModeStockage")
      {
      case "molap":
      part.StorageMode = AMO.StorageMode.Molap;
      break;
      case "holap":
      part.StorageMode = AMO.StorageMode.Holap;
      break;
      case "rolap":
      part.StorageMode = AMO.StorageMode.Rolap;
      break;
      default:
      part.StorageMode = AMO.StorageMode.Molap;
      break;
      }
      
      datasource = db.DataSources.FindByName("MonDataSourceName");
      part.Source = new AMO.QueryBinding(datasource.ID, "MaRequête"]);
      
      // affectation du premier design d'aggregation trouve
                         
      if (Meg.AggregationDesigns.Count > 0)
      {
      part.AggregationDesignID = Meg.AggregationDesigns[0].ID;
      }
      }
      
      part.Update(AMO.UpdateOptions.ExpandFull);
      
    5. Le processing des dimensions :
    6. AMO.Dimension dim = db.Dimensions.FindByName("NomDimension");
      
      if (dim == null)
      {
      
      // Si les partitions ne sont pas processer alors on les process FULL sinon on applique le paramétrage.
      if (dim.State.ToString() == "Processed")
      {
      switch (row["ProcessingType"].ToString())
      {
      case "U":
      dim.Process(AMO.ProcessType.ProcessUpdate);
      break;
      case "F":
      dim.Process(AMO.ProcessType.ProcessFull);
      break;
      case "A":
      dim.Process(AMO.ProcessType.ProcessAdd);
      break;
      default:
      dim.Process(AMO.ProcessType.ProcessDefault);
      break;
      }
      }
      else
      dim.Process(AMO.ProcessType.ProcessFull);
      
      
    7. Le processing des partitions :
    8. AMO.Partition part = Meg.Partitions.FindByName(p.NomPartition);
      part.Process(AMO.ProcessType.ProcessFull);
      
      

      Il est possible de faire une ProcessData + un ProcessIndex, qui reviendrait au même que le process Full et serait même plus performant.

    9. Le processing des groupes de mesures liés :
    10. Le processing des groupes de mesure liés n’est pas automatique. Traiter le groupe de mesures auquel il est lié ne suffit pas.

      // parcours des cubes à la recherche des groupes de mesures liés
      foreach (AMO.Cube cube2 in db.Cubes)
      {
      foreach (AMO.MeasureGroup Meg2 in cube2.MeasureGroups)
      {
      if (Meg2.IsLinked)
      Meg2.Process(AMO.ProcessType.ProcessDefault);
      }
      }
      
      
  9. Lancement des commandes :
  10. Les commande d’exécute grâce à la méthode ExecuteCaptureLog qui prends 2 arguments booléen : Transaction et Parallel afin de jouer sur la parallélisation et sur la gestion des transactions (par lot ou à la fin).

    Exemple de code pour l’exécution. Attention j’ai utilisé une méthode et une variable non définit dans cet article.

    AMO.XmlaResultCollection results = srv.ExecuteCaptureLog(, parallel);
    
    foreach (AMO.XmlaResult result in results)
    {
    foreach (AMO.XmlaMessage message in result.Messages)
    {
    if (message.GetType().Name == "XmlaError")
    {
    statutexecution = false;
    ScriptResult(srv, statutexecution,message.Description);
    }
    else
    {
    statutexecution = true;
    ScriptResult(srv, statutexecution, message.Description);
    }
    }
    }
    
    
  11. Récupération du contenu d’une variable de type object dans un script task :
  12. Il peut s’avérer utile dans ce type de tâche de ramener des informations de listes dans un script et de les récupérer.

    Pour se faire :

    DataTable dt = new DataTable();
    OleDbDataAdapter adapter = new OleDbDataAdapter();
    adapter.Fill(dt, Dts.Variables["ListePartitionASupprimer"].Value);
    
    //Puis on fini par un dispose
    adapter.Dispose();
    dt.Dispose();
    
    

    Ensuite vous pouvez récupérer les valeurs des colonnes pour chaque ligne comme suit :

    foreach (DataRow row in dt.Rows)
    {
          row["BaseASNomCube"].ToString())
    //etc …
    }
    
    
  13. Ajout de journalisation dans votre package :
  14. Si vous souhaitez récupérer des informations de jounalisation dans votre table de log (sys.ssislog). Voilà ce que vous pouvez ajouter dans votre code :

    Dts.Log("Création de la partition machin", 0, null);
    Dts.Events.FireWarning(0,Dts.Variables["System::TaskName"].Value.ToString(),"Mon message", "", 0);
    

    Si vous souhaitez faire échouer votre package vous pouvez utiliser cette commande :

    Dts.Events.FireError(0, Dts.Variables["System::TaskName"].Value.ToString(), description, "", 0);
    
  15. Ecriture de la commande XMLA dans un fichier :
  16. Il peut être intéressant de générer le fichier XMLA lancé au serveur dans une optique de Debug.

    Voici un code qui pourrait vous être utile.

    StringCollection CapturedXmla = new StringCollection();
    CapturedXmla = srv.CaptureLog;
    if (CapturedXmla.Count > 1){
    StreamWriter sw = new StreamWriter(FileName);
    sw.AutoFlush = true;
    sw.Write(srv.ConcatenateCaptureLog(true, true));
    sw.Close();
    }
    
    

J’espère que tous ces petits bout de code pourront vous permettre d’aller au bout de vos projets ou bien que cet article vous aura permis de réfléchir autour de votre stratégie d’exploitation de vos cubes SSAS.

jeudi 21 février 2013

[SSIS] Legacy mode vs modèle de déploiement de projet.

Vous ne le savez peut-être pas, mais la dernière version de SSIS a connu de nombreuses modifications. L’une d’entre elle est l’unité de déploiement. Cette modification va chambouler nos habitudes de développeurs en amenant la notion de modèle de déploiement projet.

Pour les nouveaux projets démarrant sur 2012, le choix du type de projet se pose dès la phase de conception (voir même avant). Et pour ceux qui aimeraient migrer leur projets SSIS en 2012, la question de migration dans le nouveau mode se pose, bien qu’elle n’ait rien d’obligatoire.

Ce mode de déploiement est le mode par défaut. Cependant, à tout moment, il est possible de convertir son projet SSIS de mode projet vers mode package et réciproquement. Bien que la conversion ne sera sans doute pas sans douleur en fonction des spécifités d’organisation et devariabilisation inhérente à votre projet.

Le projet SSIS à la mode 2005 qui aura tenu 3 versions a été relégué au rang de mode Legacy. Il n’est cependant pas supprimé (bonne nouvelle). Les 2 types de projets existent et peuvent coexister. Cependant avec un nom comme Legacy, sa pérennité n’est vraisemblablement pas assurée.

Voici un petit listing des + et des – du nouveau mode. Cela n’engage que moi. N’hésitez pas partager votre retour d’expérience sur vos implémentations du nouveau mode en commentant mon article.


    Avantages du nouveau mode :
  1. Le logging est natif et un jeu de rapports standards est fourni.
  2. Les déploiements sont versionnés il est possible de revenir à une version précédente sans effort en cas d’anomalie.
  3. La paramétrisation des propriétés offre plus de stabilité que la version précédente, dans laquelle les propriétés étaient identifiées par leur nom.
    Points faibles et manquement :
  1. L’impossibilité de partager des paramètres au-delà du projet empêche de découper le projet ETL en unité plus fine. Avec une paramétrisation projet, il faut définir et maintenir tous les paramètres partagés entre tous les différents projets de l’instance.
  2. Le travail en équipe est rendu moins facile. Il devient difficile de faire travailler des développeurs sur des sujets séparés au sein de la même solution. Ou sinon de bien faire attention à ne jamais récupérer du gestionnaire de code source (Source Safe, TFS, etc …) une autre version de package que la dernière valide afin de pouvoir builder son projet et ne pas causer de régression. Et si jamais un développeur doit faire des modifications au niveau du projet (paramètre, ajout d’un package, etc ….), cela peut s’avérer compliqué.
  3. L’impossibilité d’appeler un même package avec plusieurs environnements d’exécution. On peut comprendre qu’il ne soit possible de n’exécuter qu’un seul environnement par exécution du fait de son nom. Mais dans les versions précédente il était possible d’affecter plusieurs configurations pour traiter le spécifique et réutiliser des configurations communes. Ceci aurait pu donner plus de flexibilité pour le développeur.
  4. L’impossibilité de classer les environnements dans des répertoires. Dans des projets de grande ampleur, cela peut devenir vite indigeste.

Le nouveau mode offre de nouvelles possibilités, mais impose aussi beaucoup de contraintes. A mon sens, il a plutôt vocation à faciliter les échanges entre une équipe de développement et l’exploitant (ou la personne qui réceptionne le livrable). Néanmoins, dans des projets d’envergures, pour des projets d’alimentation sur des domaines fonctionnelles indépendant mais mutualisable, ou pour la maintenance évolutive, ce mode dans son état actuelle est moins adapté que le mode Legacy.

Pour les nouveaux projets la question est légitime : partir en mode projet ou legacy. Pour les projets de migration vers 2012, pour ceux qui ont investi beaucoup de temps et d’argent dans une structuration de leur projet, je leur conseillerais de migrer leur solution ISO sans passer au mode projet. Rien ne presse. Microsoft dévoilera bien assez tôt ses intentions sur le mode Legacy. Et là ce sera le moment d’agir.

vendredi 18 janvier 2013

[SSAS][PowerShell] Vérifier la cohérence de type entre DSV et Attribut de dimensions

Bien que l’on puisse imaginer qu’avec les investissements faits par MS sur Tabular, techno alternative, lemultidim SSAS que l’on a connu jusqu’à présent pourrait disparaitre dans une version prochaine, j’ai tout de même envie de continuer à bloguer sur le sujet pour les nombreux projets existants qui restent encore à maintenir et à faire évoluer.

Dans un projet décisionnel incluant la brique multidim de Microsoft, il est important de mettre tous les garde-fous pour prévenir tout problème de traitement des cubes qui pourrait engendrer soit une interruption de service ou un éventuel retard de livraison des données. Sur les projets sur lesquels je suis intervenu, il m’est arrivé suffisamment souvent d’être confronté à un problème de traitement de cube dû à un mauvais type de données sur les attributs de dimensions pour que je partage une solution pour éviter ce problème avec vous.

Les causes du problème d’incohérence de type:

Les évolutions successives que peut connaitre une application décisionnelle sur ses sources de données peuvent amener à modifier les types de données des données sources.

Par exemple changement d’un type smallint pour pour un int sur une clé primaire de dimension car le dimensionnement n’avais pas prévu de stocker au-delà de 32767.

SSAS permet de mettre à jour le DSV en fonction des modifications sur les sources de données. Cependant les modifications du DSV ne s’appliquent pas sur la structure du cube. Il est donc important de vérifier que les tous les types soit cohérent.

Ce qu’il faut vérifier :

Dans une dimension chaque attribut possède 3 propriétés : KeyColumns, NameColumn et ValueColumn.

Ce qui importe pour nous c’est de savoir si le type de chaque colonne de la collection KeyColumns possède des types permettant de stocker les données sources.

Il peut aussi être utile de vérifier pour ValueColumn.

Vérifier NameColumn est inutile cette propriété n’accepte que le type WChar qui peut accueillir aussi bien des int, que des dates, que du textes …

Une solution (parmi d’autres) :

Voici une implémentation à base de Powershell pour manipuler l’API AMO. L’objectif est d’intérroger un cube déployer sur une instance SSAS et d’afficher dans la console les différences (s’il y en a) entre type d’attributs et des colonnes sources du DSV.

La difficulté de cet exercice est de comparer le type du DSV de type OLEDB avec celui des attributs de dimensions de type system.

$CorrespondanceType=@{
      "BigInt"="Int64"
      "Binary"="Byte[]"
      "Boolean"="Boolean"
      "BSTR"="String"
      "Char"="String"
      "Currency"="Decimal"
      "Date"="DateTime"
      "DBDate"="DateTime"
      "DBTime"="TimeSpan"
      "DBTimeStamp"="DateTime"
      "Decimal"="Decimal"
      "Double"="Decimal"
      "Empty"=""
      "Error"="Exception"
      "Filetime"="DateTime"
      "Guid"="Guid"
      "IDispatch"="Object"
      "Integer"="Int32"
      "IUnknown"="Object"
      "LongVarBinary"="Byte[]"
      "LongVarChar"="String"
      "LongVarWChar"="String"
      "Numeric"="Decimal"
      "PropVariant"="Object"
      "Single"="Single"
      "SmallInt"="Int16"
      "TinyInt"="SByte"
      "UnsignedBigInt"="UInt64"
      "UnsignedInt"="UInt32"
      "UnsignedSmallInt"="UInt16"
      "UnsignedTinyInt"="Byte"
      "VarBinary"="Byte[]"
      "VarChar"="String"
      "Variant"="Object"
      "VarNumeric"="Decimal"
      "VarWChar"="String"
      "WChar"="String"
}

N’ayant pas trouvé de meilleur solution, je suis parti d’un hash table associant les types OleDB avec les types system. J’avoue que l’élégance de cette méthode est discutable. Si quelqu’un à une meilleure solution je suis preneur.

Afin de pouvoir utiliser l’API il faut commencer par charger l’assembly Microsoft.AnalysisServices, comme suit :

[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.AnalysisServices") >$NULL
 
$ServerSSAS="Nomduserveur"
$DatabaseName="NomdelabaseSSAS"

Declaration d'une fonction

function get_colonne_info([Microsoft.AnalysisServices.Database] $db, [string] $DataSourceViewID, [string] $TableId,[string] $ColonneId)
{
      $dsv=$db.DataSourceViews.get_Item($DataSourceViewID)
      $table=$dsv.Schema.Tables.get_Item($TableId)
      $colonne=$table.Columns.get_Item($ColonneId)
      return $colonne
}

Code principale

# instanciation de l'objet serveur afin de se connecter à l'instance.
$srv = new-Object Microsoft.AnalysisServices.Server
$srv.Connect($ServerSSAS)
 
# si la base passé en paramètre existe alors on continue sinon on abandonne
if($srv.Databases.Contains("$DatabaseName") -eq $true)
{
$db=$srv.Databases.get_Item($DatabaseName)
 
# parcours des de toutes les dims de la base
      foreach($dim in $db.Dimensions)
      {
            # parcours de chaque attributs
            foreach($attribute in $dim.Attributes)
            {
                  # parcours de toutes les colonnes de la collectionKeyColumn
                  foreach($keycolumn in $attribute.KeyColumns)
                  {    
                        # récupération des types et size des colonnes composant la clés à partir du DSV
                        $DataSourceViewID=$dim.Source.get_DataSourceViewID()
                        $colonne = get_colonne_info -db $db$DataSourceViewID -TableId $keycolumn.Source.TableID -ColonneId$keycolumn.Source.ColumnID
                        $Type_Dsv=$colonne.DataType.Name
                        $Size_DSV=$colonne.MaxLength
                       
                        # récupération des types et size des clés
                        $type_dim =$CorrespondanceType[$keycolumn.get_DataType().ToString()]
                        $Size_Dim=$keycolumn.DataSize
                       
                       
                        # affichage dans la console des types qui ne matchent pas
                        if($Size_DSV -eq 0){$Size_DSV=-1}
                       
                        if ( $Type_Dsv -ne $type_dim -or ($Size_DSV -ne $Size_Dim -and ($Type_Dsv -eq "String" -or $type_dim -eq "String")) )
                        {
                             '**** ' + $dim.Name.ToString() + ' | ' +$keycolumn.Parent.Name +' | ' + " KeyColumn " +' | ' + " Type DSV : " +$Type_Dsv + ", Taille DSV : " + $Size_DSV + " | Type Attribut : " +$keycolumn.get_DataType().ToString() + ", Taille Attribut : " +$Size_Dim                            
                        }
                  }
 
                 
                  # Si la propriété ValueColumn a été définit alors
                  if($attribute.ValueColumn -ne $null)
                  {
                        # même mécanisme que précédemment, on commence par les types du DSV
                        $valuecolonne= $attribute.ValueColumn
                        $DataSourceViewID=$dim.Source.get_DataSourceViewID()
                        $colonne = get_colonne_info -db $db$DataSourceViewID -TableId $valuecolonne.Source.TableID -ColonneId$valuecolonne.Source.ColumnID
                        $Type_Dsv=$colonne.DataType.Name
                        $Size_DSV=$colonne.MaxLength
                       
                         # puis ceux de la propriété de l'attribut
                        $type_dim =$CorrespondanceType[$valuecolonne.get_DataType().ToString()]
                        $Size_Dim=$valuecolonne.DataSize
                       
                        # puis on termine par l'affichage dans la console
                        if($Size_DSV -eq 0){$Size_DSV=-1}
                       
                        if ( $Type_Dsv -ne $type_dim -or ($Size_DSV -ne $Size_Dim -and ($Type_Dsv -eq "String" -or $type_dim -eq "String")) )
                        {
                             '**** ' + $dim.Name.ToString() + ' | ' +$valuecolonne.Parent.Name +' | ' + " ValueColumn " +' | ' + " Type DSV : " +$Type_Dsv + ", Taille DSV : " + $Size_DSV + " | Type Attribut : " +$valuecolonne.get_DataType().ToString() + ", Taille Attribut : " +$Size_Dim                         
                        }
                  }
            }
      }
}

vendredi 4 janvier 2013

[SSRS] [PowerShell] Désactiver les caches de tous les datasets partagés d'une instance Reporting Services.

Quand on est développeur, il peut parfois être agaçant de chercher à comprendre des heures durant l'origine d'un bug dans le code. Il arrive parfois que notre développement soit juste et que le résultat affiché dans les rapports soit faux. Une des causes courante peut-être une version ancienne du cache qui n'a pas été rafraichi. Pour cette raison, je recommande à tous les développeurs de travailler avec cache désactivé. (Pour cette raison et aussi pour être capable de détecter une requête mal écrite ou non optimisée).

Lorsqu'on travaille en intégration continue, il est commun de synchroniser l'environnement de production avec l'environnement de développement. Cependant, ce qui est souhaitable en production ne l'est pas forcément en développement. C'est pourquoi j'ai trouvé un moyen automatique, planifié à la suite de la synchronisation, pour désactiver en masse tous les caches des datasets partagés.

Grâce à PowerShell et à la cmdlet New-WebServiceProxy, on va pouvoir attaquer le web service de Reporting Services et manipuler les objets déployés sur l'instance en question. Il suffit d'instancier un objet de type WebServiceProxy et de récupérer l'objet dans une variable.

$ReportServerUri = "http://MonServeur/ReportServer/ReportService2010.asmx"
 
$Proxy = New-WebServiceProxy -Uri $ReportServerUri -Namespace SSRS.ReportingService2010 -UseDefaultCredential ;

Puis, il suffit de parcourir tous les objets de type DataSet à partir de la racine. La commande SetCacheOptions va permettre de désactiver le cache du dataset.

foreach($item in $Proxy.ListChildren("/",$true) | Where-Object{$_.TypeName -eq "DataSet"})
{
      $Proxy.SetCacheOptions($item.Path,$false,$null)
}

Voilà il reste plus qu'à appeler ce petit bout de code dans une step Powershell de l'agent à la suite des autres step de synchronisation. J'espère que cet article vous aura été utile.

dimanche 12 février 2012

[SMO][PowerShell] Automatiser la restauration des bases de données avec fichiers de données multiples

Bien sûr, il est tout à fait possible d’utiliser Transact-SQL pour restaurer une base de données, néanmoins lorsque vous souhaiterez restaurer une base qui dispose d’un nombre pluriel et variable de fichiers de données sur un environnement différent que celui où le backup a été réalisé, la restauration risque d’être un peu laborieuse du fait de devoir réallouer les fichiers vers un emplacement existant du serveur cible.

Comme je vous le disais, l’automatisation en Transact-SQL reste et Christophe Laporte le prouve ici :

http://conseilit.wordpress.com/2009/06/24/la-galere-des-restore-database-with-move-%E2%80%A6/

Cependant, cette méthode oblige à avoir accès sur le serveur où sont déployés les bases à backuper et à utiliser un curseur pour générer le scripts de restore.

En mission chez un client, j’ai dû trouver une solution pour répondre au besoin d’ « automatiser la restauration de bases de données sur un environnement de DEV à partir de fichiers .bak sans avoir accès au serveur de PROD. » En effet, on cherche à cloisonner l’environnement de PROD de celui de développement donc pas d’accès en PROD à partir de la DEV.

Pour répondre à ce besoin, j’ai fait le choix d’utiliser SMO via un script PowerShell en exécutant le fichier ps1 partir d'une step PowerShell de l’agent SQL.

Le gros avantage que j’ai trouvé à utiliser SMO est de pouvoir lire le contenu d’un fichier de backup et de pouvoir réallouer tous les fichiers avant restauration, grâce aux classes Restore et RelocateFile. Ainsi, si la base à restaurer évolue, que de nouveaux fichiers sont ajoutés ou supprimés, du fait de l'allocation dynamique la base sera restaurer sans devoir modifier le script.

Attention, par défaut, le processus de restore généré par le script PowerShell est considéré comme processus distant et le Timeout par défaut de 600 secondes (10min) s'applique. Ce qui amène les requêtes de restauration à être interrompu et à laisser les bases de données dans l'état Restoring.

La parade est de forcer la propriété StatementTimeout à 0 grâce à la commande.$server.ConnectionContext.set_StatementTimeout(0). Le script ci-dessous applique ce paramètre.

param(
        ## Serveur où l'on souhaite restaurer le backup
        [string]$ServerDestination = $(throw "-ServerDestination est requis")
       ## La restauration se fera à partir de cette emplacement
       ,[string]$BackupLocalPath = $(throw "-BackupLocalPath est requis")
       ## Listes des base à restaurer
       ,[array]$DBtoRestore = $(throw "-DBtoRestore est requis")
)

##Chargement des assemblies
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.SMO") | Out-Null
  
## Fonction de restauration
## Prends en charge les fichiers multiple et les relocalise en fonction du DataPath et LogPath par défaut du serveur

function Restore([string]$srv,[string]$bck)
{

try{
$server = New-Object Microsoft.SqlServer.Management.Smo.Server $srv
$backupDevice = New-Object Microsoft.SqlServer.Management.Smo.BackupDeviceItem $bck, "File"
$smoRestore = New-Object Microsoft.SqlServer.Management.Smo.Restore

## permet d'outrepasser le timeout du serveur afin de pouvoir restaurer les grosses bases
$server.ConnectionContext.set_StatementTimeout(0)

## Paramétrage du restore
$smoRestore.NoRecovery = $false;
$smoRestore.ReplaceDatabase = $true;
$smoRestore.Action = "Database"

$smoRestore.Devices.Add($backupDevice)

#Gestion de la relocalisation des fichiers
$filelist=$smoRestore.ReadFileList($server)
foreach( $file in $filelist.get_DefaultView())
{
       if($file.PhysicalName -like "*mdf")
       {
             $physicalpath=$server.DefaultFile + "\" + $file.LogicalName + ".mdf"
             $relocateFile = new-object Microsoft.SqlServer.Management.Smo.RelocateFile $file.LogicalName, $physicalpath
             $smoRestore.RelocateFiles.Add($relocateFile)
       }
       elseif($file.Type -like "D")
       {
             $physicalpath=$server.DefaultFile + "\" + $file.LogicalName + ".ndf"
             $relocateFile = new-object Microsoft.SqlServer.Management.Smo.RelocateFile $file.LogicalName, $physicalpath
             $smoRestore.RelocateFiles.Add($relocateFile)
       }
       else{
             $physicalpath=$server.DefaultLog + "\" + $file.LogicalName + ".ldf"
             $relocateFile = new-object Microsoft.SqlServer.Management.Smo.RelocateFile $file.LogicalName, $physicalpath
             $smoRestore.RelocateFiles.Add($relocateFile)
       }
}

# On lit le contenu du backup
$smoRestoreDetails = $smoRestore.ReadBackupHeader($server)

#Afin de donner à la base que l'on va restaurer le même nom qu'elle avait sur le serveur source
$smoRestore.Database =$smoRestoreDetails.Rows[0]["DatabaseName"]
  
#Commande de restauration de la base
$smoRestore.SqlRestore($server)

"La base " + $smoRestoreDetails.Rows[0]["DatabaseName"] + " a été restaurée avec succès"
}
catch {
           $ex = $_.Exception
           Write-Output $ex.message
           $ex = $ex.InnerException
           while ($ex.InnerException)
           {
               Write-Output $ex.InnerException.message
               $ex = $ex.InnerException
           };
           continue
       }
}

$bckpath = New-Object System.IO.DirectoryInfo $BackupLocalPath

foreach($file in $bckpath.GetFiles())
{
       $exist=$false
       $bck=""
       foreach($value in $DBtoRestore)
       {
             if($file.Name -eq $value)
             {
                    $exist=$true
                    $bck=$file.FullName
                    break
             }
       }
       if($exist)
       {     
             Restore -srv $ServerDestination -bck $bck
       }
}

dimanche 1 janvier 2012

[SQL]Supprimer de gros volume de données grâce au partitionnement

Supprimer de gros volumes de données peut être problématique si l’on se cantonne à utiliser l’instruction DELETE.

Cette dernière a le défaut de poser un verrou exclusif sur la table cible et comme toute transaction sur les données de journaliser les opérations dans le fichier transaction log afin de pouvoir rollback la transaction en cas de problème et d’assurer l’intégrité des données.

Si vous avez besoin de nettoyer une table de données sans supprimer la totalité de la table (sinon l’instruction TRUNCATE TABLE ferait l’affaire), il peut être judicieux d’utiliser les fonctionnalités du partitionnement.

L’idée directrice de cette méthode et d’affecter les données d’une table source partitionnée à une table cible partitionnée sur le même schéma de partitionnement. Dans la mesure où physiquement rien n’est ni modifié ni déplacé et que l’opération porte juste sur les métadonnées, le temps d’exécution de cette manipulation est quasiment instantanée.

Une fois le Switch de partition opéré vous pouvez très facilement lancer l’instruction TRUNCATE TABLE sur la table cible.

Petit rappel : les fonctionnalités de partitionnement sont disponibles à partir de la version 2005 de SQL SERVER.

Ci-dessous je vous ai préparé un petit script d’exemple pour vous illustrer cette manipulation. La manip est faite dans les étapes 7 et 8, avant il ne s’agit que de la préparation de la base.

-----------------------------------------------------------------------------------------
-- ETAPE 01: Création de la base de données
-----------------------------------------------------------------------------------------

Create Database DemoPartitionsDatabase
Go

-----------------------------------------------------------------------------------------
-- ETAPE 02: Création de la fonction de partition et scheme
-----------------------------------------------------------------------------------------

Use DemoPartitionsDatabase
Go

Create Partition Function MaFonctionDePartionnement (int)
As Range LEFT For Values (20110725,20110801)
Go
Create Partition Scheme MaFonctionScheme
As Partition MaFonctionDePartionnement ALL To ([PRIMARY])
Go

-----------------------------------------------------------------------------------------
-- ETAPE 03: Création de la table
-----------------------------------------------------------------------------------------

Create table FactTable (
[date] int not null,
col1 int null,
col2 int null,
col3 int null,
col4 int null,
col5 int null,
col6 int null,
col7 int null,
col8 int null,
col9 int null,
) ON MaFonctionScheme([date])

-----------------------------------------------------------------------------------------
-- ETAPE 04: Mettons un peu de volumétrie dans la table
-----------------------------------------------------------------------------------------

declare @startdate date='2011-07-25',
@i int=0

While @i<9
begin
;with
t0 as (select n = 1 union all select n = 1),
t1 as (select n = 1 from t0 as a, t0 as b),
t2 as (select n = 1 from t1 as a, t1 as b),
t3 as (select n = 1 from t2 as a, t2 as b),
t4 as (select n = 1 from t3 as a, t3 as b),
t5 as (select n = 1 from t4 as a, t4 as b),
result as (select top 1000000 row_number() over (order by n) as n from t5)
insert into FactTable
select (select convert(int,convert(varchar(10),dateadd(dd,@i,@startdate),112))),
n,n,n,n,n,n,n,n,n from result

set @i=@i+1
end
go

-----------------------------------------------------------------------------------------
-- ETAPE 05: Creation d'une vue pour suivre l'état du partitionnement
-----------------------------------------------------------------------------------------

create view [dbo].[partition_info] as
SELECT
OBJECT_NAME(p.object_id) AS ObjectName, i.name AS IndexName,p.index_id AS IndexID,ds.name AS PartitionScheme,p.partition_number AS PartitionNumber,fg.name AS FileGroupName,prv_left.value AS LowerBoundaryValue,prv_right.value AS UpperBoundaryValue,
CASE pf.boundary_value_on_right
WHEN 1 THEN 'RIGHT'
ELSE 'LEFT' END AS Range,
p.rows AS Rows

FROM sys.partitions AS p

JOIN sys.indexes AS i
ON i.object_id = p.object_id
AND i.index_id = p.index_id

JOIN sys.data_spaces AS ds
ON ds.data_space_id = i.data_space_id

JOIN sys.partition_schemes AS ps
ON ps.data_space_id = ds.data_space_id

JOIN sys.partition_functions AS pf
ON pf.function_id = ps.function_id

JOIN sys.destination_data_spaces AS dds2
ON dds2.partition_scheme_id = ps.data_space_id
AND dds2.destination_id = p.partition_number

JOIN sys.filegroups AS fg
ON fg.data_space_id = dds2.data_space_id

LEFT JOIN sys.partition_range_values AS prv_left
ON ps.function_id = prv_left.function_id
AND prv_left.boundary_id = p.partition_number - 1

LEFT JOIN sys.partition_range_values AS prv_right
ON ps.function_id = prv_right.function_id
AND prv_right.boundary_id = p.partition_number

WHERE OBJECTPROPERTY(p.object_id, 'ISMSShipped') = 0

select * from [dbo].[partition_info]
go


-----------------------------------------------------------------------------------------
-- ETAPE 06: Crééons une table de purge identique à la première
-----------------------------------------------------------------------------------------

Create table FactTablePurge (
[date] int not null,
col1 int null,
col2 int null,
col3 int null,
col4 int null,
col5 int null,
col6 int null,
col7 int null,
col8 int null,
col9 int null,
) ON MaFonctionScheme([date])

-----------------------------------------------------------------------------------------
-- ETAPE 07: Déplaçons (que dis-je affectons) les données à la table de purge
-----------------------------------------------------------------------------------------

alter table dbo.FactTable switch partition 2 to dbo.FactTablePurge partition 2
go
select * from dbo.partition_info
go

-----------------------------------------------------------------------------------------
-- ETAPE 08: Il ne reste plus qu'à truncate la table de purge et à regarder le résultat
-----------------------------------------------------------------------------------------

truncate table dbo.FactTablePurge
go
select * from dbo.partition_info
go

/*****************************************************************************************
** The End : Les données ont disparu instantannément sans trace dans le fichier de log
*****************************************************************************************/

/*****
Si vous n'êtes pas convaincu par cette méthode essayez d'éxécuter le delete sur les 7 millions de lignes avec la requête ci-dessous
--delete from FactTable where $partition.MaFonctionDePartionnement(date)=2
*******/

[SSIS] Créer un fichier XML à partir d’une table SQL

A premier abord, cela parait anodin pour un ETL et pourtant nativement il n’existe pas de composant pour envoyer un flux de donnée vers un fichier XML. Confronté à cette problématique chez un client, j’ai dû creuser pour trouver des solutions de contournement à cette carence de SSIS.
Bien heureusement SSIS a l’avantage d’offrir la possibilité de recourir à .NET via des scripts La solution que j’ai trouvée, il y en a d’autre, notamment celle de François Jehl que vous pourrez trouver ici est d’utiliser l’objet DataSet ADO.NET.
L’utilisation de XML dans un DataSet est très bien documentée ici.
L’avantage de cette méthode est de pouvoir générer facilement du XML aussi bien introduire des types complexes, que de définir des colonnes comme attribut d’autres éléments.
Pour cette méthode, il suffit d'implémenter le code suivant dans un script task dans le control flow. Tout le travail (connexion à la base, lecture des tables, transformation XML et écriture dans le fichier) se feront grâce au script ci-dessous.
Voici le script :
//Ne pas oublier la reference.
using System.Data.SqlClient;


public void Main()
{
SqlConnection sqlConn;
// définition des 2 tables liées par un ID
string CurrencyQuery = "SELECT [CurrencyCode],[Name] FROM [Sales].[Currency]";
string CurrencyRateQuery = "SELECT [FromCurrencyCode],[ToCurrencyCode],[AverageRate],[EndOfDayRate] FROM [Sales].[CurrencyRate]";

try
{
    // Connexion à la base AdventureWorks (qui doit être une connexion ADO.NET)
    sqlConn = (SqlConnection)(Dts.Connections["AdventureWorks"]).AcquireConnection(Dts.Transaction);
    sqlConn = new SqlConnection(sqlConn.ConnectionString);
    sqlConn.Open();


    SqlCommand cmdCurrency = new SqlCommand(CurrencyQuery, sqlConn);
    SqlCommand cmdCurrencyRate = new SqlCommand(CurrencyRateQuery, sqlConn);
   
    //permet de remplir les dataset - c'est un lien entre la source et le dataset
    SqlDataAdapter CurrencyAdapter = new SqlDataAdapter(cmdCurrency);
    SqlDataAdapter CurrencyRateAdapter = new SqlDataAdapter(cmdCurrencyRate);

    // création de notre DataSet dont le nom sera la racine de notre fichier XML
    DataSet ds = new DataSet("CurrencyRate");

    //mise en mémoire des 2 tables
    CurrencyAdapter.Fill(ds, "Currency");
    CurrencyRateAdapter.Fill(ds, "CurrencyRate");

    // cette commande attribut est très intéressant il peut prendre plusieurs valeurs dont celle d'attribut.
    ds.Tables["Currency"].Columns["name"].ColumnMapping = MappingType.Attribute;

    // c'est ici que l'on va définir notre type complexe XML en établissant une relation entre les 2 datasets.
    DataRelation CurrencyRateDR = ds.Relations.Add("CurrencyRateRelation", ds.Tables["Currency"].Columns["CurrencyCode"], ds.Tables["CurrencyRate"].Columns["FromCurrencyCode"]);
    // propriété très importante puisqu'elle permet d'imbriquer les éléments enfants dans les éléments parents. Ils sont considéré par défaut comme éléments frère
    CurrencyRateDR.Nested = true;

    ds.WriteXml(new System.IO.StreamWriter("D:\\TEST\\test.xml"));
    sqlConn.Close();
}
catch
{
    Dts.TaskResult = (int)ScriptResults.Failure;
    throw;
}

Dts.TaskResult = (int)ScriptResults.Success;
}
Bien entendu, ici la source est une table XML, mais il est possible d’attaquer d’autres sources directement comme Excel par exemple. J’espère que cela pourra vous aider dans vos projets. A bientôt.