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
       }
}