blogs.conchango.com

welcome to the conchango blogging site
Welcome to blogs.conchango.com Sign in | Join | Help
in Search

Christian Wade's Blog

  • Databind ChartFX to OLAP Datasource

    I’ve just gone through an informal and limited evaluation process for a charting tool for a BI dashboard.  It included some of the RS extension products, ProClarity and ChartFX.  Of course I would have been keen to use ProClarity, but in this case the cost was a little high and, to be honest, it would have been a sledgehammer to crack a nut.


    ChartFX is the charting engine used by ProClarity.  It looks and feels exactly the same as ProClarity charts.  In theory, everything you can do with ProClarity charts, you can do with ChartFX – you just have to code it yourself (which is a lot easier than it sounds).


    Anyway, when evaluating ChartFX, I initially looked at the OLAP extension to it.  Seemed to work great at first and I was very impressed with it.  However, when I got to doing anything a little out of the ordinary like multiple Y axes or specifying X axes labeling, the code simply didn’t work!  For example, the code at the bottom of the following snippet has no effect.


    AdoMultiDimensionalData ADOMD1 = new AdoMultiDimensionalData();

    string connstr = "Provider=msolap; Data Source=localhost;Initial Catalog=Adventure Works;";

    string ds =

    @"SELECT

        [Date].[Calendar].[Calendar Year].Members ON 0,

        {

            [Measures].[Internet Sales Amount],

            [Measures].[Internet Freight Cost]

        } ON 1

    FROM

        [Adventure Works];";

    ADOMD1.Connect(connstr, ds);

    Olap1.DataSource = ADOMD1;


    // This code hides the Analysis Bar

    Olap1.AnalysisBar = AnalysisBar.Menu;

    ToolBar tb = (ToolBar)Olap1.AnalysisObject;

    tb.Visible = false;

    Chart1.Gallery = Gallery.Bar;


    // Code that doesn't work!

    AxisY addlAxisY = new AxisY();

    addlAxisY.Visible = true;

    addlAxisY.Position = AxisPosition.Far;

    Chart1.AxesY.Add(addlAxisY);

    Chart1.Series[0].AxisY = addlAxisY;


    The reason for the code not working is that the OLAP extension uses databinding, which overrides code such as this.  Here’s what the chart looks like.


    Chart with no extra Y axis

    The way I got round this problem was to not use the OLAP extension at all.  Instead, I am using the ChartFX API to iterate an ADOMD.NET CellSet and set the chart data.  Here is the code; it works fine for MDX with 2 axes.  You could of course take it a little further to display crossjoined sets, etc.  Here is the code.


    using (AdomdConnection conn = new AdomdConnection("Data Source=localhost;Initial Catalog=Adventure Works;"))

    {

        conn.Open();

        //Create a command, using this connection

        AdomdCommand cmd = conn.CreateCommand();

        cmd.CommandText =

        @"SELECT

            [Date].[Calendar].[Calendar Year].Members ON 0,

            {

                [Measures].[Internet Sales Amount],

                [Measures].[Internet Freight Cost]

            } ON 1

        FROM

            [Adventure Works];";

        //Execute the query, returning a cellset

        CellSet cs = cmd.ExecuteCellSet();

        //Get back rows representing each series

        TupleCollection tuplesOnRows = cs.Axes[1].Set.Tuples;

        //Set the number of series

        Chart1.Data.Series = tuplesOnRows.Count;

        //Get back columns representing each x axis category

        TupleCollection tuplesOnColumns = cs.Axes[0].Set.Tuples;

        Chart1.Data.Points = tuplesOnColumns.Count;

        //Set the data values and series names

        for (int row = 0; row < tuplesOnRows.Count; row++)

        {

            Chart1.Series[row].Text = tuplesOnRows[row].Members[0].Caption;

            for (int col = 0; col < tuplesOnColumns.Count; col++)

            {

                Chart1.Data[row, col] = Convert.ToDouble(cs.Cells[col, row].Value);

            }

        }

        //Set the x axis category names

        for (int i = 0; i < tuplesOnColumns.Count; i++)

        {

            Chart1.AxisX.Labels[ i ] = tuplesOnColumns[ i ].Members[0].Caption;

        }

        conn.Close();

    } // using connection


    //Set chart type

    Chart1.Gallery = Gallery.Bar;

    //Create secondary y axes series and its appearance properties

    AxisY addlAxisY = new AxisY();

    addlAxisY.Visible = true;

    addlAxisY.Position = AxisPosition.Far;

    Chart1.AxesY.Add(addlAxisY);

    Chart1.Series[1].AxisY = addlAxisY;



    Here’s what the chart looks like.


    Chart with extra Y axis

    We now have an AJAX enabled UI with blistering performance.  Changing the slicing of the chart has it refresh instantly.  No more RS spinnies!





  • SQL Server Standard - Recursive Hierarchies to XML

    I wrote an article for the September 2006 issue of SQL Server Standard magazine:

     SQL Server Standard

    The version that was finally published was a very cut down version with minimal code snippets.  This was for obvious reasons; they had to fit the content onto small columns in the magazine.  For the readers who would prefer the more verbose version, here it is!  I guess this is further reference material for the published article; I’m sure my friends at SQL Server Standard won’t mind.

    Suppose we have a sizeable recursive hierarchy in our SQL Server 2005 relational database.  We want to export it to XML.  This could be for a variety of reasons.  We may wish to use it as a pre-cached input to a UI control (e.g. a tree control); we may wish to export it to another system using a predefined format.  The possibilities are endless.


    Here is an example input dataset using the Northwind Employees table.

    Northwind Employees

    And this is how we want it represented in XML based on the ReportsTo self-referencing relationship.


    <Employee EmployeeID="2" LastName="Fuller" FirstName="Andrew">

                <Employee EmployeeID="1" LastName="Davolio" FirstName="Nancy" />

                <Employee EmployeeID="3" LastName="Leverling" FirstName="Janet" />

                <Employee EmployeeID="4" LastName="Peacock" FirstName="Margaret" />

                <Employee EmployeeID="5" LastName="Buchanan" FirstName="Steven">

                            <Employee EmployeeID="6" LastName="Suyama" FirstName="Michael" />

                            <Employee EmployeeID="7" LastName="King" FirstName="Robert" />

                            <Employee EmployeeID="9" LastName="Dodsworth" FirstName="Anne" />

                </Employee>

                <Employee EmployeeID="8" LastName="Callahan" FirstName="Laura" />

    </Employee>


    On the face of it, this seems like a simple nut to crack.  However, there are various design options available to us.  This article explores some of the options.  Performance is deemed the overriding factor when evaluating the optimal approach.


    It is assumed that it is beneficial to convert the data to XML in the database; i.e. in-process with SQL Server.  However, the overall evaluation of the different approaches would be the same even if the transformation to the desired format took place in the middle tier.


    All the code in this post can be downloaded from here.  This is a link to a zip file containing a solution and a SQL Server project.  To use it, it is necessary to change the database connection string in the project properties.  The TSQL scripts that generate the XML output can be found in the Option1.sql and Option2.sql solution files.


    Note: A Northwind database restored to SQL Server 2005 is also required to run the code.  It is necessary to build and deploy the above project.  A backup of Northwind from SQL Server 2005 can be downloaded from here.



    What were the options we had in SQL Server 2000 for exporting relational data to XML?  We had the FOR XML clause of the SELECT statement.  Incidentally, SQL Server 2005 has a new mode called FOR XML PATH.  This mode is powerful because it allows us to use XPath-type syntax to define our XML structure.  FOR XML PATH will meet about 95% of the use cases for which we traditionally had to resort to FOR XML EXPLICIT.  FOR XML PATH is a lot less cumbersome and easy to use.  However, both these options require us to know the depth of the lowest level node when writing the query.  For recursive hierarchies, we do not actually know the lowest depth beforehand because it is variable.  This means it is not feasible to use FOR XML and directly arrive at our desired XML output.  The best we can do with FOR XML is to run the output through an XSLT transform.



    Option 1: FOR XML AUTO and XSLT

    The SQL Server 2005 functionality for XML provided through XQuery and XML indexing can be considerably enhanced by using SQL CLR to apply XSLT transformations.  Here we are using a SQL CLR function to convert the output of a FOR XML AUTO query against our Northwind dataset in order to arrive at the desired output.


    DECLARE @input xml, @stylesheet xml

    SELECT            @input =

                            (SELECT EmployeeID, ReportsTo, LastName, FirstName

                            FROM Employees AS Employee

                            FOR XML AUTO, ROOT('Employees')),

                            @stylesheet =

                            CAST(

    '<?xml version="1.0" encoding="UTF-8"?>

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

                <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

                <xsl:template match="/Employees" >

                            <xsl:apply-templates select="Employee[not(@ReportsTo)]" />

                </xsl:template>

                <xsl:template match="Employee[not(@ReportsTo)]">

                            <xsl:element name="Employee">

                                        <xsl:attribute name="EmployeeID">

                                                    <xsl:value-of select="@EmployeeID" />

                                        </xsl:attribute>

                                        <xsl:attribute name="LastName">

                                                    <xsl:value-of select="@LastName" />

                                        </xsl:attribute>

                                        <xsl:attribute name="FirstName">

                                                    <xsl:value-of select="@FirstName" />

                                        </xsl:attribute>

                                        <xsl:call-template  name="FindChildNodes">

                                                    <xsl:with-param name="ReportsTo" select="@EmployeeID" />

                                        </xsl:call-template>

                            </xsl:element>

                </xsl:template>

                <xsl:template name="FindChildNodes">

                            <xsl:param name="ReportsTo" />

                            <xsl:for-each select="//Employees/Employee[@ReportsTo=$ReportsTo]">

                                        <xsl:variable name="EmployeeID" select="@EmployeeID" />

                                        <xsl:element name="Employee">

                                                    <xsl:attribute name="EmployeeID">

                                                                <xsl:value-of select="@EmployeeID" />

                                                    </xsl:attribute>

                                                    <xsl:attribute name="LastName">

                                                                <xsl:value-of select="@LastName" />

                                                    </xsl:attribute>

                                                    <xsl:attribute name="FirstName">

                                                                <xsl:value-of select="@FirstName" />

                                                    </xsl:attribute>

                                                    <!-- only call template recursively if EmployeeID is a ReportsTo of another node -->

                                                    <xsl:if test="count(//Employees/Employee[@ReportsTo = $EmployeeID]) > 0">

                                                                <xsl:call-template  name="FindChildNodes">

                                                                            <xsl:with-param name="ReportsTo" select="@EmployeeID" />

                                                                </xsl:call-template>

                                                    </xsl:if>

                                        </xsl:element>

                            </xsl:for-each>

                </xsl:template>

    </xsl:stylesheet>

    ' AS xml)

    SELECT dbo.ApplyTransform(@input, @stylesheet)


    Further discussion regarding the implementation of the XSLT stylesheet is outside the scope of this article.


    Here is an implementation of the ApplyTransform function that receives the stylesheet.  It basically just exposes the System.Xml.Xsl.CompiledTransform class’ Transform method to TSQL.


    using System;

    using System.Data;

    using System.Data.SqlClient;

    using System.Data.SqlTypes;

    using System.Xml;

    using System.Text;

    using System.IO;

    using Microsoft.SqlServer.Server;

     [SqlFunction(DataAccess = DataAccessKind.None, SystemDataAccess = SystemDataAccessKind.None, IsDeterministic = false, IsPrecise = false)]

    [return: SqlFacet(IsFixedLength = false, MaxSize = -1)]

    public static SqlXml ApplyTransform(SqlXml data, SqlXml styleSheet)

    {

        // Create a stream for the xml output

        MemoryStream ms = new MemoryStream();

        XmlWriter xw = XmlWriter.Create(ms);

        // Load the XML and transform it

        XslCompiledTransform ctx = new XslCompiledTransform(false);

        ctx.Load(styleSheet.CreateReader());

        ctx.Transform(data.CreateReader(), xw);

        // return the output

        return new SqlXml(ms);

    }


    Assuming some prerequisite knowledge of XSL stylesheets, this appears to be a good option to achieve our goal.  However, let us evaluate others before deciding the optimum choice.



    We could of course simply access the relational data in a SQL CLR stored procedure and write it to XML.  However, if we don’t access the data in the same order it will appear in the XML, we will have to load the whole structure into the DOM.  Loading the whole structure into the DOM means loading it into memory; if we are talking about a million rows, we may well not have enough memory available!  The ‘right order’ is as follows.  This allows us to write it to XML sequentially, which is vastly more performant and efficient.

    • Node1
      • Node2
        • Node3
        • Node4
      • Node5

    How can we return the data in this order from a single TSQL query?  Probably the best option is to use a recursive Common Table Expression (CTE).  CTEs are a new feature in SQL Server 2005.  They can be conceptualized similarly to views and derived tables in that they encapsulate an inner query.  However, they differentiate themselves from views and derived tables in the way they deal with recursion.  The syntax for a recursive CTE is as follows.


    WITH RecursiveCTE(<column_list>)

    AS

    (

                -- Anchor Member:

                SELECT ...

                FROM <some_table(s)>

                ...

               

                UNION ALL

               

                -- Recursive Member

                SELECT ...

                FROM <some_table(s)>

                JOIN RecursiveCTE

                ...

    )

    -- Outer Query

    SELECT ...

    FROM RecursiveCTE

    ...


    The WITH clause is the definition of the CTE and it precedes the outer query, which refers back to the CTE.  Within the WITH clause, the anchor member is a SELECT statement that acts as the seed for recursion.  It is merged using the UNION ALL operator to the recursive member, which is a SELECT statement that refers back to the CTE; hence it is recursive.


    Note: it was possible to return the data in the ‘right order’ from a single TSQL query in SQL Server 2000 using a recursive table-valued user-defined function.  I tried this approach and the performance is far worse than the recursive CTE.  For large datasets, the execution time was multiplied by a factor of at least 5!  For example code of how this would be done using a recursive table-valued user-defined function, see this old blog post of mine.

    Note: this whitepaper describes a way of avoiding using a table-valued UDF.  It does however use a scalar UDF, so it is still limited to 32 nested recursions.  If you know your hierarchies will never exceed 32 nested recursions, the scalar UDF method is probably a good way to go.  However, given the nature of recursive hierarchies, this may be an assumption you cannot make.

    Note: recursive CTEs are by default limited to 100 nested recursions.  However, this only acts as a safeguard against infinite recursion.  This limit can be changed using the MAXRECURSION query hint to anything up to 32,767 nested recursions.  Recursive table-valued user-defined functions, on the other hand, are limited to 32 nested recursions.


    Note: FOR XML cannot be used by the anchor or recursive member SELECT statements in a recursive CTE.  This is why we need a CLR routine to present the data as XML.



    Option 2: Recursive CTE and sequential writing of XML


    The usp_OrderedEmployeeHierarchy stored procedure below returns the data in the order we require.


    CREATE PROCEDURE usp_OrderedEmployeeHierarchy (@Seed int)

    AS

                ;WITH EmployeeCTE(EmployeeID, ReportsTo, LastName, FirstName, Depth, SortCol)

                AS

                (

                  SELECT EmployeeID, ReportsTo, LastName, FirstName, 0, CAST(EmployeeID AS varbinary(max))

                  FROM Employees

                  WHERE EmployeeID = @Seed

                  UNION ALL

                  SELECT E.EmployeeID, E.ReportsTo, E.LastName, E.FirstName, M.Depth+1,

                            CAST(SortCol + CAST(E.EmployeeID AS binary(4)) AS varbinary(max))

                  FROM Employees AS E

                            JOIN EmployeeCTE AS M

                              ON E.ReportsTo = M.EmployeeID

                )

                SELECT

                  EmployeeID, ReportsTo, LastName, FirstName, Depth

                --,SortCol

                FROM EmployeeCTE

                ORDER BY SortCol

    GO


    When passing a @Seed parameter of 2, the following dataset is returned.


    Ordered Northwind Employees

    The way the CTE handles ordering the data is through the SortCol column.  For the seed of recursion, this column is the EmployeeID cast to varbinary(max).  For each subsequent recursive member, it is the existing SortCol value concatenated with the binary representation of the EmployeeID.  If we include it in the output, the following dataset is returned.


    Ordered Northwind Employees with SortCol

    As shown, the SortCol column provides a useful structure on which we can sort the dataset.  The data is thereby retrieved in the required order.


    The Depth column is initialized at zero for the seed of the recursion and then incremented by one for each recursion.


    This is the SQL CLR stored procedure that writes the XML sequentially.


    using System;

    using System.Data;

    using System.Data.SqlClient;

    using System.Data.SqlTypes;

    using System.Xml;

    using System.Text;

    using System.IO;

    using Microsoft.SqlServer.Server;

    [Microsoft.SqlServer.Server.SqlProcedure(Name = "usp_GetEmployeeHierarchyXml")]

    public static void GetEmployeeHierarchyXml(SqlInt32 seed)

    {

        // Create a stream for the xml output

        MemoryStream ms = new MemoryStream();

        XmlWriter xw = XmlWriter.Create(ms);

        // Depth counters to identify when to close elements

        int previousDepth = -1;

        int newDepth = 0;

        using (SqlConnection conn = new SqlConnection("context connection=true"))

        {

            conn.Open();

            using (SqlCommand cmd = conn.CreateCommand())

            {

                cmd.CommandType = CommandType.StoredProcedure;

                cmd.CommandText = "usp_OrderedEmployeeHierarchy";

                cmd.Parameters.Add(new SqlParameter("@Seed", seed));

                SqlDataReader reader = cmd.ExecuteReader();

                while (reader.Read())

                {

                    // Get the depth of the current node

                    newDepth = Convert.ToInt32(reader["Depth"]);

                    // Do we need close an open element(s)?

                    CloseOpenElements(xw, previousDepth, newDepth);

                    xw.WriteStartElement("Employee");

                    xw.WriteAttributeString("EmployeeID", Convert.ToString(reader["EmployeeID"]));

                    xw.WriteAttributeString("LastName", Convert.ToString(reader["LastName"]));

                    xw.WriteAttributeString("FirstName", Convert.ToString(reader["FirstName"]));

                    previousDepth = newDepth;

                }

                // Now we are back at depth 0; do we need close an open element(s)?

                newDepth = 0;

                CloseOpenElements(xw, previousDepth, newDepth);

                xw.Close();

                // Return the results to the client

                SqlDataRecord record;

                record = new SqlDataRecord(new SqlMetaData[] { new SqlMetaData("EmployeeHierarchy", SqlDbType.Xml) });

                // Set the record field.

                record.SetSqlXml(0, new SqlXml(ms));

                // Return the record to the client.

                SqlContext.Pipe.Send(record);

            }

        }

    }

    private static void CloseOpenElements(XmlWriter xw, int previousDepth, int newDepth)

    {

        if (newDepth <= previousDepth)

            for (int i = 0; i <= previousDepth - newDepth; i++)

                xw.WriteEndElement();

    }


    The GetEmployeeHierarchyXml stored procedure executes the usp_OrderedEmployeeHierarchy stored procedure for data access.  A System.Xml.XmlWriter object is used to sequentially write the XML.  The Depth column is used to keep track of when to close open XML elements.  A Microsoft.SqlServer.Server.SqlDataRecord object is used to return a one-row, one-column, XML-datatype result set to the client.  This is done using the static Send method, which is a member of the Microsoft.SqlServer.Server.SqlContext class’ Pipe property.  The SqlContext class is a class dedicated solely to in-process data access.  It contains other interesting properties and methods, which are outside the scope of this article.


    Executing usp_GetEmployeeHierarchyXml returns the XML in the desired format.



    Some readers may be asking the question as to why we do not simply write a SQL CLR stored procedure that retrieves a set of child nodes at a time for every single node in the hierarchy.  This does mean a lot more TSQL queries being executed, but is conceptually the simplest approach.  However, this is not feasible because multiple active resultsets (MARS) is not supported in SQL CLR.  We would therefore have to instantiate many connection objects.  This also presents a problem because we can only have one open ‘context’ connection at a time.  I did attempt this approach processing the resultset in a client-side application and the performance was far worse than Option 1 or 2.  This approach has therefore been ruled out.


    So, here is the question we’ve all been waiting for: which is the most efficient choice?  I tried both approaches on a product hierarchy, which happened to originate from Analysis Services, containing 8,700 members.  Here are the findings.  Note that Option 2 assumes a cached execution plan for the usp_OrderedEmployeeHierarchy stored procedure.


    Option 1: 38 seconds

    Option 2: 15 seconds


    This is a performance gain of over 60% !!


    We can therefore conclude that CTEs provide a well architected method for accessing recursive relational data.  A prime example of this is when exporting such data to XML.


  • Currency Conversion in Analysis Services 2005

    I recently investigated currency conversion in SSAS for a customer.  I found that there is not much on the web in the way of examples.  Here is an attempt to correct that.

    First thing to do: figure out what type of currency conversion you need.  There are 3 types.

    1. MANY-TO-MANY.  This is where the facts are stored in multiple currencies – i.e. amounts for the same measure are in various currencies in the same fact table.  Also, the users might want to report the total amount in different currencies.
    2. MANY-TO-ONE.  The facts are stored in multiple currencies, but this time it is only necessary to ever report the total amount in a single currency – e.g. a corporate currency.
    3. ONE-TO-MANY.   The facts are always stored in one currency only.  However, the users might want to report the total amount in different currencies.

    This post focuses on the different options available for One-to-Many.  With One-to-Many, there are 2 options: measure expressions or the currency conversion wizard.

    If you create a DSV on top of AdventureWorksDW and include DimTime, FactInternetSales, FactCurrencyRate and DimCurrency, it should look something like this.

    DSV1

    The relationship between FactInternetSales and DimCurrency is for the case where your facts are stored in multiple currencies (Many-to-Many and Many-to-One).  To look at One-to-Many, we will delete this relationship.  Also, we will delete the ShipDate and DueDate relationships between FactInternetSales and DimTime, but leave the OrderDate relationship (this post is about currency conversion, not role-playing dimensions!).

    We now have the following DSV diagram.

    DSV2

    ONE-TO-MANY USING MEASURE EXPRESSIONS

    I created a cube on top of this DSV.  Here is a backup of it http://blogs.conchango.com/christianwade/CurrencyConversionBackup.zip  You can download, restore and open it using the VisualStudio using the File > Open > Analysis Services Database menu option.

    The only measures in it are Sales Amount (from FactInternetSales) and End Of Day Rate (from FactCurrencyRate).  Sales Amount has a FormatString of “Currency”.  End Of Day Rate has an AggregateFunction property value of “LastNonEmpty”.  LastNonEmpty is a semi-additive measure.  We want it to sum for all dimensions except Date.  For the Date dimension, it will take the last non-empty child.  For example, the exchange rate we would want to see for a week member in a report would not be the sum of the exchange rate on Monday + exchange rate on Tuesday + exchange rate on Wednesday, …  Instead, we just want the most recent exchange rate (maybe exchange rate on Saturday).

    The Date dimension is very simple.  The Date attribute, which is the key attribute, has a KeyColumns property of TimeKey (which is the surrogate key of the DimTime table) and a NameColumn of FullDateAlternateKey (what the users see at Date level).

    I used the Dimension Wizard to create the Date dimension so that it would be flagged with Type = Time, etc.  This is one of the few cases where having these properties set correctly actually affects cube behaviour.  For example, semi-additive measures and some MDX functions like YTD won't work without Type = Time.

    Date Dimension

    Here are the mappings in the Dimension Usage tab.  They are pretty straightforward.  There is a many-to-many relationship between Currency and Fact Internet Sales.

    Dimension Usage

    Here is a screenshot of the Currency dimension.  The main points about the Currency dimension are as follows.

    • The KeyColumns property of the Currency attribute is set to CurrencyKey (which is the surrogate key in for the DimCurrency table in AdventureWorksDW).
    • The NameColumn property of the Currency attribute is set to CurrencyName (which is what we want the users to see).
    • The Type property of the dimension is set to Currency.  The only purpose of this (unlike Type=Time that does affect cube behaviour) is to inform client tools in case they want to display currency dimensions differently to regular dimensions.
    • The Type property of the Currency attribute is set to CurrencyName.  Again, this is just to inform client tools.
    • The IsAggregatable property of the Currency attribute is set to False.  This removes the All level for the attribute.  We would after all not want to sum the values of different currencies.  500 pounds sterling plus 100,000 cruzeiros equals 100,500 of what currency?  Monopoly money?
    • Set the DefaultMember property of the Currency attribute to whatever the Sales Amount values are stored in.  In the case of AdventureWorksDW, it is US Dollars.
    • The attribute relationship between Currency and Currency Alternate Key has its Cardinality property set to One.  This is because, for a particular Currency, there can be only one Currency Alternate Key – i.e. they have a one-to-one relationship.  This improves efficiency in aggregation because the numbers will always be the same (e.g. £500 for “Great Britain Pounds” will always result in £500 for “GBP”).  Analysis Services will therefore not bother f