Affichage des articles dont le libellé est SSAS. Afficher tous les articles
Affichage des articles dont le libellé est SSAS. Afficher tous les articles

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

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.

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