In my past blogs I showed how to use SSIS package to process dimensions, cubes or build aggregates. I am slowly rebuilding my existing packages thanks to points from Darren Gosbell and Jess Orosz. I found that using CaptureXML method is more convenient for me and it is definitely faster.

Just to explain again about this method. I can change Analysis Services objects (create, drop, process) in the script using AMO commands. Normally each AMO command is executed in a separate batch. But AMO has an option to save XMLA script from the multiple AMO commands and then submit them in one batch. This is done my assigning Analysis Services server property CaptureXML value ‘True’ (to start capturing XMLA) or ‘False’ (to stop capturing XMLA). Then XMLA log can be executed using ExecuteCaptureLog statement where first parameter specifies if statements have to be run in batch.

Past weekend I had some time to rewrite my partition synchronization package to use CaptureXML method. I maintain a list of partitions in SQL Server database table iwSSASPartitions. I use a SSIS script to drop partitions if they do not exists in my partition table and create new partitions if they do not exists in SSAS database. I have a SQL Server table with the list of partitions that should exist in SSAS database. Here is the script to create this table:

CREATE TABLE [dbo].[iwSSASPartitions](
 [Partition] [varchar](100) NOT NULL,
 [CubeName] [varchar](100) NOT NULL,
 [MeasureGroup] [varchar](100) NOT NULL,
 [SQL] [varchar](4000) NOT NULL,
 [NeedsReProcessing] [bit] NOT NULL,
 CONSTRAINT [PK_iwSSASPartitions] PRIMARY KEY CLUSTERED ( [Partition], [CubeName], [MeasureGroup])
)

Normally I would deploy SSAS database (no processing) with just one partition per measure group. Then I would run my SSIS package to synchronize partitions between SQL Server table and SSAS database. For initial deployment I would drop about 35 partitions and then create about 250 new partitions. During normal monthly load I would need to create about 30 partitions.

My old package was runnign XMLA scripts to create/drop partitions. I was creating/dropping partitions one by one. With old method my initial drop of 35 partition was taking about 5 min. Step to create new 250 partitions was taking about 15 min. As this is not done everyday, 20min to me seemed like a reasonable time. But after I re-wrote my package to take advantage of batch executions, step that drops partitions run for 15 seconds and step that create partitions run for 30 seconds. At first I actually thought that my code is not executing! So this is power of executing XMLA in the batch!

Steps to create SSIS package that drops/create partitions based on partition list in the SQL Server table:

  • Create new connection to SQL Server database that contains SQL table with list of partitions. It is important here that connection type is ADO.NET and not OLE DB. I had to use ADO.NET connection as I wanted to pass query result as parameter into Script task.
  • Create new connection to SSAS database. Name that connection DM
  • Create new variable with Scope package, name “PartitionListInDW” and type “Object”. This variable will contain list of partitions from table.
  • Add new “Execute SQL Task” component in Control Flow area. Change following properties for this control:
    • ResultSet: “Full Result Set”
    • ConnectionType: “ADO.NET”
    • Connection: From drop down list choose connection you create in first step
    • SQLStatement: SELECT CubeName,MeasureGroup, Partition, SQL FROM dbo.iwSSASPartitions ORDER BY 1,2,3
    • In ResultSet tab click “Add” and enter 0 for “ResultName” and for “VariableName” choose “user::PartitionListInDW”
    • Change step name and description to your liking and click OK.
  • Add new “Script Task” component and connect it to prevoiusly create task. In “Script” tab change property “ReadOnlyVariables” to “PartitionListInDW” and click on “Design Script” button. In “Project Explorer” add reference to “Analysis Management Objects” and “System.XML.dll”. Rename this component to “Drop Partitions”. Add following code:

Imports System
Imports System.Data
Imports System.Math
Imports Microsoft.SqlServer.Dts.Runtime

Imports Microsoft.AnalysisServices

Public Class ScriptMain

Public Sub Main()

  Dim ExecutionSuccessfull As Boolean = True ‘ If true, package executed without errors
  Dim dataBytes(0) As Byte

  Dim PartListInDM(999, 2) As String ‘ expecting no more than a 1000 partitions.
  Dim DropPartCount As Integer = 0

  ‘ Following objects are used to capture execution results
  Dim oResults As Microsoft.AnalysisServices.XmlaResultCollection
  Dim oResult As Microsoft.AnalysisServices.XmlaResult
  Dim oMessage As Microsoft.AnalysisServices.XmlaMessage

  Dim oPartitionList As Variable = Dts.Variables(”PartitionListInDW”) ‘ From variable
  Dim oDS As DataSet = CType(oPartitionList.Value, DataSet)

  Dim oTbl As DataTable = oDS.Tables(0)
  Dim oRow As DataRow
  Dim oConnection As ConnectionManager
  oConnection = Dts.Connections(”DM”)
  Dim sServer As String = CStr(oConnection.Properties(”ServerName”).GetValue(oConnection))
  Dim sDatabase As String = CStr(oConnection.Properties(”InitialCatalog”).GetValue(oConnection))

  Dim oServer As New Microsoft.AnalysisServices.Server
  oServer.Connect(sServer) ‘ connect to the server and start scanning down the object hierarchy
  Dim oDB As Microsoft.AnalysisServices.Database = oServer.Databases.FindByName(sDatabase)

  If oDB Is Nothing Then
    Dts.Log(”Did not find expected database: ” & sDatabase, 0, dataBytes) ‘ You need to setup proper package loging to see this!
    ExecutionSuccessfull = False
    GoTo Done
  End If

  Dim oCube As Microsoft.AnalysisServices.Cube
  Dim oMeasureGroup As Microsoft.AnalysisServices.MeasureGroup
  Dim oPartition As Microsoft.AnalysisServices.Partition
  Dim bNeedToDropPartition As Boolean
  For Each oCube In oDB.Cubes
    For Each oMeasureGroup In oCube.MeasureGroups
      For Each oPartition In oMeasureGroup.Partitions
        ‘ Check if this partition exists in our partition table
        bNeedToDropPartition = True
        ’ oTbl.Reset()
        For Each oRow In oTbl.Rows
          If oRow(”CubeName”).ToString = oCube.Name.ToString And oRow(”MeasureGroup”).ToString =     oMeasureGroup.Name.ToString _
  And oRow(”Partition”).ToString = oPartition.Name.ToString Then
            bNeedToDropPartition = False ‘ Found partition in DW list, no need to drop it
            Exit For
          End If
        Next
        If bNeedToDropPartition Then
          PartListInDM(DropPartCount, 0) = oCube.Name
          PartListInDM(DropPartCount, 1) = oMeasureGroup.Name
          PartListInDM(DropPartCount, 2) = oPartition.Name
          DropPartCount = DropPartCount + 1
          Dts.Log(”Found partition that needs to be dropped: ” & oPartition.Name, 0, dataBytes) ‘ You need to setup proper package loging to see this!
  ‘       oPartition.Drop()
       End If
     Next
   Next
  Next
  ‘ MsgBox(DropPartCount)

  oServer.CaptureXml() = True
  Dim i As Integer = 0
  For i = 0 To DropPartCount - 1
    oCube = oDB.Cubes.FindByName(PartListInDM(i, 0))
    oMeasureGroup = oCube.MeasureGroups.FindByName(PartListInDM(i, 1))
    oPartition = oMeasureGroup.Partitions.FindByName(PartListInDM(i, 2))
    oPartition.Drop()
  Next i
  oServer.CaptureXml() = False
  ‘ Executing log that contains all XMLA commands
  oResults = oServer.ExecuteCaptureLog(True, False)
  ‘Log the errors and warnings
  For Each oResult In oResults
    For Each oMessage In oResult.Messages
       If oMessage.GetType.Name = “XmlaError” Then
         ‘The processing failed
         Dts.Log(oMessage.Description, 0, dataBytes) ‘ You need to setup proper package loging to see this!
         ExecutionSuccessfull = False
       Else
         ‘It is just a warning.
         ExecutionSuccessfull = True ‘ if you want to fail on warning, change this to False
      End If
    Next oMessage
  Next oResult
  ‘ Finished code that drops partitions
  ‘ ————————————————————————————————————————-

Done:
  oServer.Disconnect()

  If ExecutionSuccessfull Then
    Dts.TaskResult = Dts.Results.Success
  Else
    Dts.TaskResult = Dts.Results.Failure
  End If

End Sub

End Class

  • Few notes about last script:
    • Trying to run dropping of partitions in a single batch in parallel (ExecuteCaptureLog(True, True)) generated error: “The Delete element at line 9, column 94 (namespace http://schemas.microsoft.com/analysisservices/2003/engine) cannot appear under Envelope/Body/Execute/Command/Batch/Parallel”
    • You cannot drop partition in For Each partition loop. This is because dropping would change list of members in For each statement. That is way I save all partitions that have to be dropped in array and drop it latter.
    • You need to enable proper package and (!) step logging to see errors/warnings in the log
  • Add new “Script Task” component and connect it to prevoiusly create task. In “Script” tab change property “ReadOnlyVariables” to “PartitionListInDW” and click on “Design Script” button. In “Project Explorer” add reference to “Analysis Management Objects” and “System.XML.dll”. Rename this component to “Create New Partitions”. Add following code:

Imports System
Imports System.Data
Imports System.Math
Imports Microsoft.SqlServer.Dts.Runtime

Imports Microsoft.AnalysisServices

Public Class ScriptMain

Public Sub Main()

  Dim ExecutionSuccessfull As Boolean = True ‘ If true, package executed without errors
  Dim dataBytes(0) As Byte

  ‘ Following objects are used to capture execution results
  Dim oResults As Microsoft.AnalysisServices.XmlaResultCollection
  Dim oResult As Microsoft.AnalysisServices.XmlaResult
  Dim oMessage As Microsoft.AnalysisServices.XmlaMessage

  Dim oPartitionList As Variable = Dts.Variables(”PartitionListInDW”) ‘ From variable
  Dim oDS As DataSet = CType(oPartitionList.Value, DataSet)

  Dim oTbl As DataTable = oDS.Tables(0)
  Dim oRow As DataRow
  Dim oConnection As ConnectionManager
  oConnection = Dts.Connections(”DM”)
  Dim sServer As String = CStr(oConnection.Properties(”ServerName”).GetValue(oConnection))
  Dim sDatabase As String = CStr(oConnection.Properties(”InitialCatalog”).GetValue(oConnection))

  Dim oServer As New Microsoft.AnalysisServices.Server
  oServer.Connect(sServer) ‘ connect to the server and start scanning down the object hierarchy
  Dim oDB As Microsoft.AnalysisServices.Database = oServer.Databases.FindByName(sDatabase)

  If oDB Is Nothing Then
    Dts.Log(”Did not find expected database: ” & sDatabase, 0, dataBytes) ‘ You need to setup proper package loging to see this!
    ExecutionSuccessfull = False
    GoTo Done
  End If

  Dim oCube As Microsoft.AnalysisServices.Cube
  Dim oMeasureGroup As Microsoft.AnalysisServices.MeasureGroup
  Dim oPartition As Microsoft.AnalysisServices.Partition

  oServer.CaptureXml() = True
  For Each oRow In oTbl.Rows
    oCube = oDB.Cubes.FindByName(oRow(”CubeName”).ToString)
    If oCube Is Nothing Then
      Dts.Log(”Did not find cube: ” & oRow(”CubeName”).ToString, 0, dataBytes) ‘ You need to setup proper package loging to see this!
      Continue For
    End If
    oMeasureGroup = oCube.MeasureGroups.FindByName(oRow(”MeasureGroup”).ToString)
    If oMeasureGroup Is Nothing Then
      Dts.Log(”Did not find measure group: ” & oRow(”MeasureGroup”).ToString, 0, dataBytes) ‘ You need to setup proper package loging to see this!
      Continue For
    End If
    oPartition = oMeasureGroup.Partitions.FindByName(oRow(”Partition”).ToString)
    If (oPartition Is Nothing) Then
      Dts.Log(”Need to create partition: ” & oRow(”Partition”).ToString, 0, dataBytes) ‘ You need to setup proper package loging to see this!
      oPartition = oMeasureGroup.Partitions.Add(oRow(”Partition”).ToString)
      oPartition.StorageMode = StorageMode.Molap
      oPartition.Source = New QueryBinding(oDB.DataSources(0).ID, oRow(”SQL”).ToString)
      If oMeasureGroup.AggregationDesigns.Count > 0 Then ‘
        oPartition.AggregationDesignID = oMeasureGroup.AggregationDesigns(0).ID ‘ Taking first available aggregation design!
      End If
      oPartition.Update(UpdateOptions.ExpandFull)
    End If
  Next
  oServer.CaptureXml() = False

  ‘ Executing log that contains all XMLA commands
  oResults = oServer.ExecuteCaptureLog(True, False)

  ‘Log the errors and warnings
  For Each oResult In oResults
    For Each oMessage In oResult.Messages
      If oMessage.GetType.Name = “XmlaError” Then
        ‘The processing failed
        Dts.Log(oMessage.Description, 0, dataBytes) ‘ You need to setup proper package loging to see this!
        ExecutionSuccessfull = False
      Else
        ‘Its just a warning.
        ExecutionSuccessfull = True ‘ if you want to fail on warning, change this to False
      End If
    Next oMessage
  Next oResult
  ‘ Finished code that creates new partitions
  ‘ ————————————————————————————————————————-

  Done:
    oServer.Disconnect()

  If ExecutionSuccessfull Then
    Dts.TaskResult = Dts.Results.Success
  Else
    Dts.TaskResult = Dts.Results.Failure
  End If

End Sub
End Class

  • Few notes about last script:
    • Trying to run creation of partitions in a single batch in parallel (ExecuteCaptureLog(True, True)) generated error: “The Alter element at line 9, column 141 (namespace http://schemas.microsoft.com/analysisservices/2003/engine) cannot appear under Envelope/Body/Execute/Command/Batch/Parallel.”
    • You need to enable proper SSIS package and (!) step logging to see errors/warnings in the log
  • Populate table dbo.iwSSASPartitions with the list of partitions you desire
  • Test run package. Be careful, if you run it with empty table, all partitions will be dropped.

Vidas Matelis

Note 2008-July-03, it appears to me that this code might be used by many people. I had multiple requests for actual package and finally I posted ziped version of package here.

And thank you very much to everybody who left me (or will leave) comments here about this post. I am very happy that you found my code useful!