Updating Your Cube Data


The ability to create "hypothetical" what-if scenarios is central to business intelligence because it enables the executive to explore contingencies associated with the financial landscape. In this way, the executive can seek out the profit maximizing potential of the firm while minimizing risk and generally mitigating threats. Because these concepts are so much better explained by way of example, here are two specific examples to help you understand what is meant by what-if scenarios.

In the first example, a company president is considering her options in terms of resource allocation for the coming fiscal year. Her BI team has astutely developed UDM that calculate key business drivers for the company. Because the user can writeback new values into those seed measures, new configurations of company resource allocation can be tried and the results assessed. The key business metrics will be recalculated due to new values entered into the system. The sorts of questions that can be "asked" of the system through the use of these simulations depend on how many measures are designed to act as seed values. Typical questions include, "What if we increase our advertising budget for the next fiscal year?" "What if we charge more (or less) for our product?" and "What if we take on more debt to expand production facilities and therefore enhance manufacturing capacity?" Keep in mind that not just any question could be asked of the system. Only those questions that have the required measures available for use as seed values and underlying calculations or KPIs in support of cascading correct changes will provide meaningful results.

The second example considers the needs of a bank. The vice-president is considering strategies for the coming year regarding appropriate risk distribution for commercial loan application acceptance. The bank has good metrics from which to base calculations on successful loan repayments versus defaults and those metrics have been entered into the relevant cubes. If banks are anything like most bureaucratic organizations, they likely have regulations that mandate certain minimum distributions of loans to different risk categories. For example, in the interest of economic development and creating new active bank customers in the future, a certain percentage of high-risk loans should be accepted. The vice-presi-dent is most likely looking to give loans so as to maximize income from loans for the bank; with a fund of $500 million from which to make loans, the president can consider the manipulation of multiple seed values like interest rates and risk acceptance percentages. In the end, some optimal state will emerge, such as allocating $100 million to low risk, $250 million for moderate risk, and $150 million to high risk businesses.

In financial applications (corporate plan, budget, and forecast systems), most of the time the UDM is applied as a data gathering and analysis business model to gather data input from all departments, and perform many what-if analyses for future business decisions. Once the analysts make a final decision, the data values need to be updated in the cube for appropriate actions to be taken. For example, the budget for a department might get allocated based on the current year's revenue and that department would have to plan the next fiscal year's financial plans based on the allocated budget. If the executives have made a forecast of achieving specific revenue for the next year, other business decisions need to be propagated to the people in the corporate food chain appropriately. For example, if the sales target for the organization was to have 500 million dollars (10% growth over the current year), the business goals or commitments for the individual sales employees need to be appropriately set to reach the organization goal. In this section you learn how to effectively use Analysis Services to provide what-if scenarios to top executives and to update the cube data so that it can be appropriately propagated to the entire organization. Updating the data in an Analysis Services cube is referred to as cell writeback because you are updating the cell values in the cube space.

You will use the cube created in the previous section to learn about updating data within the cube. Consider the scenario where you need to allocate the budget amount for the group lead by Amy Alberts. Amy has three employees reporting to her and the budget needs to be distributed to her reports based on certain business factors. You will consider examples of various ways in which allocation of data can be accomplished with the cube's data. Analysis Services 2005 does not provide a front-end interface to update cube data unlike updating dimension data through the dimension browser. However, you can build your own application once you know what MDX statements to send to Analysis Services. Hence the examples you see in this section are primarily MDX statements, which you need to execute through SQL Server Management Studio to help you understand how to update the cube data. The following steps show how to make modifications to the cube so that you can use the database for understanding data allocation and update of cube data:

  1. Execute the SQL queries (CreateWriteBackExampleTables.sql in the Chapter 11 folder that can be downloaded from the accompanying web site) initially executed to ensure you have all the dimension members. Open the dimension WB Period in the Analysis Services cube used in the previous section.

  2. Rename the key attribute as Quarter, and have the name column for the key attribute set to QuarterName. Remove the attribute hierarchy QuarterName and create a user hierarchy Period that has two levels, Calendar, Year and Quarter, as shown in Figure 11-9. Open the Adventure Works DW cube in the WriteBackExample database and delete the measure WB_Fact Count that was created by intellicube. You will learn later in this section why you need to delete the measure with aggregation function Count.

    image from book
    Figure 11-9

  3. Deploy the changes to your Analysis Services instance.

  4. Send the following MDX query to the cube:

         select {[WB Employee].[Manager].&[290],     [WB Employee].[Manager].&[290].children} on 1 ,     [WB Period].[Period].&[2004].children on 0     from [Adventure Works DW] 
  5. The results of this MDX query are shown in the following table. The cell values show the Budget Expense amount for various quarters for employees reporting to Amy Alberts. Notice that the value for 2004 Q3 is not available.

    2004 Q1

    2004 Q2

    2004 Q3

    Alberts,Amy

    2072000

    2865000

    (null)

    Alberts,Amy

    116000

    1000

    (null)

    Pak,Jae

    883000

    1329000

    (null)

    Varkey Chudukatil,Ranjit

    707000

    908000

    (null)

    Valdez,Rachel

    366000

    627000

    (null)

  6. You are aware that a cube contains one or more measure groups. When a cube is enabled to update data, that means that one or more measure groups are enabled for updating data in the cube. Typically, a fact table would correspond to a measure group in the cube. Each measure group contains one or more partitions. The data from the relational data source is read and stored within partitions by Analysis Services. In order to update the data within the cube you need to have a new partition called the writeback partition for each measure group you need to enable for updating data. Any data (measure values) that gets updated in the cube space will be entered into this writeback partition. The writeback partition is a partition that points to a relational data source. This data can reside within the same relational data source used by the cube or in a different relational database. Similar to restrictions in writing back to dimensions, Analysis Services has restrictions in writing back to a measure group. You can write enable a cube for updating data only when all the measures in the measure group have an aggregation function as Sum. Even if one of the measures has an aggregation function as count, distinct count, or any of the semi-additive aggregation functions, the measure group cannot be write-enable for writeback. Hence whenever you want to enable a cube for cell writeback you need to have measures that have aggregation function other than Sum to be in a separate measure group.

  7. In order to enable updating data within the cube, connect to the database you have deployed; you can either use BIDS or SQL Server Management Studio. Within the BIDS, click the partitions tab of the Adventure Works DW cube. Select the partition WB Fact, right-click, and then select Writeback Settings as shown in Figure 11-10.

    image from book
    Figure 11-10

  8. You will now be presented with the Enable Writeback dialog shown in Figure 11-11. When data is being written back to the cells of a cube, Analysis Services stores appropriate information in a relational database table. You learn about this information later in this chapter. For now assume it is stored in a table. Because Analysis Services needs to store some information in a relational table, you need to specify a data source that points to a database where Analysis Services has write permissions. In the enable writeback dialog you need to specify the data source and the name of the table to store the cell writeback data. By default, Analysis Services chooses the existing data source that is used by the partition with the writeback table name as WritebackTable_ <PartitionName>. If you do not want the writeback table within the same database as that of your relational backend, you can specify a new data source in the enable writeback dialog by clicking New. Once you have specified the writeback table, click the OK button. You can write enable a measure group even through SQL Server Management Studio. To do so, connect to the Analysis Services database using SQL Server Management Studio, navigate to the measure group, right-click the measure group, and select Writeback Optionsimage from book
    Figure 11-11

  9. Once you click OK in the enable writeback dialog, the Partition editor automatically creates a ROLAP partition for Writeback_WB Fact. You can see a new partition called Writeback_WB Fact added to the measure group WB Fact in the partition list. When you deploy the entire project to the Analysis Services instance, the writeback partition metadata gets updated for the WriteBackExample database. The BIDS then sends a process command to process the database. At the time of processing the writeback partition, Analysis Services checks if the writeback table exists in the specified data source. If not, Analysis Services instance creates the writeback table by sending the following CREATE SQL statement. You do have the option of forcibly creating the writeback table at the time of each process of the measure group in the DDL, but by default Analysis Services creates the table only if it does not exist in the database.

         CREATE TABLE [dbo].[WriteTable_WB Fact](       [BudgetExpenseAmount_0] [float] NULL,       EmployeeKey_1] [int] NULL,       [QuarterKey_2] [int] NULL,       [MS_AUDIT_TIME_3] [datetime] NULL,       [MS_AUDIT_USER_4] [nvarchar](255) COLLATE SQL_Latin1_General_CP1_CI_AS NULL      ) ON [PRIMARY] 

  10. Once the writeback table has been created, the cube has been enabled for writeback. You can now update the budget values for 2004 Q3. You have the option to writeback to a single cell or multiple cells depending on where you write the data. You will learn the various options in the next few sections.

Update a Single Cell Value

One of the scenarios is to update a single cell value in the cube. For example, you need to allocate a budget of $1000 for Jae Pak. Follow these instructions to update the cell value corresponding to Jae Pak:

  1. Open SSMS and create two new MDX query windows.

  2. Select the database WriteBackExample and the Adventure Works DW cube.

  3. In step 5 of the previous section you saw the budget value for 2004 Q3 for employee Jae Pak was null. Send the following update statement to update the budget value for employee Jae Pak:

         UPDATE CUBE [Adventure Works DW]     SET ( [WB Employee].[Manager].&[291]     , [WB Period].[Period].&[20043]) = 1000     //Updating the budget for employee Jae Pak for Quarter 3 of year 2004 

  4. The update statement is used to update the cell values in the cube. The syntax of the update statement is shown below.

         UPDATE CUBE <CubeName>     SET <Tuple Expression> = Numeric or String value     [ALLOCATION TYPE clause] 
  5. The UPDATE CUBE syntax is pretty straightforward where you specify the coordinates in the cube to update the new value. The allocation type clause is an optional clause by which you can specify the nature of the allocation. You see examples of various allocation types later in this chapter through examples.

  6. In the update statement the coordinate pointed by the tuple ([WB Employee].[Manger].&[291], [WB Period].[Period].&[20043]) refers to the budget value for Jae Pak. The statement updates the cell value to 1000.

  7. If you send the following query to the cube you will see that the budget value for Jae Pak is now 1000:

         SELECT {     [WB Employee].[Manager].&[291] } ON 1 ,     [WB Period].[Period].&[20043] ON 0     FROM     [Adventure Works DW] 
  8. Send the same query in the second MDX query editor window. You will see the original value null rather than the new value 1000. The update statement and the value only take effect for the first MDX window, which holds one connection (or session). Other connections (or sessions) are not affected by the updated value. This is because Analysis Services updates the new cell value only if you request it to make these changes permanently in the cube. In order to do that you need to send the commit statement to the server as shown below.

         COMMIT 
  9. COMMIT is a short form for COMMIT TRANSACTION statement. By default, when you start executing new queries within SQL Server Management Studio, an implicit statement called BEGIN TRANSACTION is executed. Due to the changes, cell values will get updated only after you call the COMMIT TRANSACTION or just the COMMIT statement. When the COMMIT statement is executed, Analysis Services gets the new value to be written, subtracts the original cell value for that tuple, and then writes back the difference in the ROLAP partition that has been set up for writeback. Because the original cell value was null and the new value is 1000, you should see 1000 in the ROLAP partition.

  10. Send the following query to the relational table in the Adventure Works DW database. You will see that there is a new entry in the relational table that has a value of 1000.

         select * from [WriteTable_WB Fact] 
  11. If you go to the second MDX query window and query the cell value for Jae Pak, you will see the new value 1000.

  12. Analysis Services allows you to send multiple updates within a transaction so that you have the ability to roll back or commit the entire transaction that does an update to cube. You can include one or more update queries within the statements Begin Transaction and Commit Transaction query, to start a transaction for cell update.

  13. After the update statement, send the first MDX query in the same query window:

         SELECT {[WB Employee].[Manager].&[290],     [WB Employee].[Manager].&[290].children     } on 1 ,     [WB Period].[Period].&[2004].children on 0     FROM [Adventure Works DW] 
  14. You will see the results shown in the following table. Notice that Alberts, Amy, parent member of Pak, Jae, got the 1000 aggregated. Analysis Services takes care of calculations and aggregations of the data written back to the cells automatically.

    Open table as spreadsheet

    2004 Q1

    2004 Q2

    2004 Q3

    Alberts,Amy

    2072000

    2865000

    1000

    Alberts,Amy

    116000

    1000

    (null)

    Pak,Jae

    883000

    1329000

    1000

    Valdez,Rachel

    366000

    627000

    (null)

    Varkey Chudukatil,Ranjit

    707000

    908000

    (null)

  15. Switch to the first MDX window, and send the following query to update the same cell to 800. If you are executing the following statements in SQL Server Management Studio, execute them as separate statements by selecting the statement and then pressing execute or Ctrl+E to execute; that is, Begin Transaction should first be highlighted and executed, followed by the update statement and then the Commit Transaction statement.

         BEGIN TRANSACTION     UPDATE CUBE [Adventure Works DW]     SET ( [WB Employee].[Manager].&[291]     , [WB Period].[Period].&[20043]) = 800     COMMIT TRANSACTION 
  16. If you send a query to retrieve the data for Jae Pak for Quarter 3 of 2004 in both MDX query windows, you will see the exact same value of 800.

Analysis Services uses the ROLAP partition to implement the cell writeback; when you send the update cell query the cell data value is held in memory for that specific session and transaction. When a user sends the commit statement, the change in cell value is written to the writeback table by Analysis Services. Because the writeback partition is ROLAP, other connections or users will pick up the data change immediately without reprocessing the cube.

Now open a SQL query window and connect to Adventure Works DW relational database and send the SQL query to retrieve all the rows in the Writeback table. You will see the results shown in the following table.

Budget Expense Amount_0

EmployeeKey_1

QuarterKey_2

MS_AUDIT_TIME_3

MS_AUDIT_USER_4

1000

291

20043

42:31.0

Sivah04\sivah

200

291

20043

45:40.0

Sivah04\sivah

In the table you will see two rows being entered for the measure value. Recall that the first value of 1000 was the first update statement you executed. When you executed the second update statement, you updated the cell value to 800. The difference of -200 is therefore entered for the same tuple within the writeback table. When a new query comes in, the aggregated data of 1000-200 + the cell value within the cube is seen by the user. Note Analysis Services logs the time and the user who did the update to the cell. Consider this as a tracking mechanism for you to trace the writeback operations. If you notice serious discrepancies in your data, due to the logging of user and time when the update was done, users cannot deny what they did and this might come in handy if you are audited.

Update NON-Leaf Cell Value using Allocation

The previous example demonstrates how to update for Jae Pak who is a leaf member in the dimension who does not have any reports under him. A leaf-level cell in Analysis Services means all dimension members of that cell are on the granularity level; for example, member "Pak, Jae" doesn't have children and you choose to write to Q4. If you have another dimension, you would have to include a member from that dimension for the granularity attribute. However, in many cases, a user might want to input a number at a higher level granularity and allocate down to the leaf-level members via different rules. For instance, a user can input an entire year's budget and allocate to each quarter by last year's sales. In this section, you would allocate the budget for each employee reporting to Amy Alberts using the value allocated to Amy Alberts. Analysis Services provides several ways to allocate/update values for non-leaf level cells. Because the actual data being allocated cannot be held directly in a non-leaf cell within Analysis Services, the data needs to be propagated to the non-leaf-level cells. The most obvious and easiest way to allocate in this way is to allocate the value equally to all the leaf-level cells.

Equal Allocation

Consider the scenario where Amy Alberts is allocated $1,000 and this needs to be propagated to her and all her direct reports, because she also needs to budget for the work she does. As seen with the UPDATE statement syntax you have an optional allocation clause. In order to allocate this value equally to her direct reports, you need to specify the keyword USE_EQUAL_ALLOCATION. Before you execute the update statement below send an update statement to set the budget value for Jae Pak to 0. Following MDX query will update the budget value for Amy Alberts by equally allocating the value to her direct reports:

     UPDATE CUBE [Adventure Works DW]     SET (     [WB Employee].[Manager].&[290]     , [WB Period].[Period].&[20043]) = 1000     USE_EQUAL_ALLOCATION 

After this update statement, if you send the following query you will see that each of the employees reporting to Amy Alberts will get a value of 250 as shown in the following table.

     SELECT {[WB Employee].[Manager].&[290],     [WB Employee].[Manager].&[290].children     } on 1 ,     [WB Period].[Period].&[2004].children on 0     FROM [Adventure Works DW] 

Open table as spreadsheet

2004 Q1

2004 Q2

2004 Q3

Alberts,Amy

2072000

2865000

1000

Alberts,Amy

116000

1000

250

Pak,Jae

883000

1329000

250

Varkey Chudukatil,Ranjit

707000

908000

250

Valdez,Rachel

366000

627000

250

If you did not specify the allocation clause to be USE_EQUAL_ALLOCATION, Analysis Services assumes that the data allocated needs to be equally distributed to all the children. Therefore, the following UPDATE statement will also result in the same results as shown in the previous table:

     UPDATE CUBE [Adventure Works DW]     SET (     [WB Employee].[Manager].&[290]     , [WB Period].[Period].&[20043]) = 1000 

Now that you have learned how to update data to a non-leaf member, you will learn the remaining allocation options provided by Analysis Servivces.

Weighted Allocation

In a more complex scenario, allocation of budgets depends on the size of the organization or the revenue generated by the person (or group) in the previous year. A common form of allocation in the real-world is to allocate values based on rates calculated by using last period's budget rate plus some percentage increase to determine this period's value, or just use last year's sales to allocate this year's budget. Analysis Services provides a way to write back data to leaf levels using various proportions, and hence such an allocation is called weighted allocation.

Consider a case such that Amy Alberts gets $1,000 as a budget and she wants to allocate it to her direct reports and herself based on the ratio calculated from the previous quarter. Now, how do you go about forming an update statement that will accomplish this scenario? Let's first break it down and build the MDX.

First, you know the update statement to allocate to Amy Alberts is:

     UPDATE CUBE [Adventure Works DW]     SET (     [WB Employee].[Manager].&[290]     , [WB Period].[Period].&[20043]) = 1000 

Next, you need to add the allocation clause. For weighted or ratio allocation you need to use the keyword USE_WEIGHTED_ALLOCATION BY, which gets added at the end of the preceding statement. Following the USE_WEIGHTED_ALLOCATION BY you need to specify a ratio or weight that will derive the rate based on the previous quarter. The following MDX expression calculates the ratio of budget for the previous quarter for the employees reporting to Amy Alberts.

     ([WB Period].[Period].[20042], [WB Employee].[Manager].currentmember)/     ([WB Employee].[Manager].&[290],[WB Period].[Period].[20042]) 

The first part of the MDX expression takes the current member in the context of the query, which will be one of the direct reports of Amy Alberts and their budget value in the second quarter of 2004. The second part of the MDX query provides the budget value for Amy Alberts for the second quarter of 2004. Because the value for Amy Alberts is an aggregated data of all her reports, you get a ratio of each employee's budget as compared to the overall budget allocated to Amy Alberts in the second quarter of 2004.

Combining all the sections of the MDX you have seen, you will have the following MDX query to allocate the budget to Amy Albert's team based on a ratio of the previous quarter:

     update cube [Adventure Works DW]     set (     [WB Employee].[Manager].&[290]     , [WB Period].[Period].&[20043]) = 1000 use_weighted_allocation by     ([WB Period].[Period].&[20042], [WB Employee].[Manager].currentmember)/     ([WB Employee].[Manager].&[290],[WB Period].[Period].&[20042]) 

If you execute the MDX query to retrieve the budget amount for all the children of Amy Albets for various quarters of 2004 after executing the update statement, you should see the results shown in the following table. In the RTM version of the product the allocation does not get distributed. We believe this is a bug that might be resolved in service packs. We will provide an update through the download site on this example.

2004 Q1

2004 Q2

2004 Q3

Alberts,Amy

2072000

2865000

1000

Alberts,Amy

116000

1000

0.34904014

Pak,Jae

883000

1329000

463.8743455

Valdez,Rachel

366000

627000

218.8481675

Varkey Chudukatil,Ranjit

707000

908000

316.9284468

Incremental Allocation

The third scenario is where a cell already has a value and you need to allocate values to leaf-level cells so that they are incremented by the new value allocated — based on equal allocation or weighted allocation. Consider an organization that receives funding from multiple sources and these funds need to be allocated to subdivisions one by one. Further, you do not want to overwrite the previous data. Before you learn about incremental allocation, please delete the Writeback table in the relational data source and reprocess the WriteBackExample database.

Lets say Amy Alberts obtained funding in the amount of $1,000 for Jae Pak in support of the project he is working on. Amy allocates this budget directly to Jae for quarter 3 of 2004. Send the following update statement to allocate the budget to Jae.

     UPDATE CUBE [Adventure Works DW]     SET (     [WB Employee].[Manager].&[290]     , [WB Period].[Period].&[20043]) = 1000 

Now assume Amy gets funding in the amount of $1,000 for the entire group and she wants to allocate this equally to all her direct reports. She obviously does not want to overwrite the existing budget value allocated for Jae already. Analysis Services provides a way to allocate this new budget amount to the leaf-level cells either through equal or through weighted allocation. The allocation clause keyword that needs to be used is USE_EQUAL_INCREMENT or USE_WEIGHTED_INCREMENT along with the weight as seen in the Weighted allocation example.

Consider the scenario where Amy wants to allocate the amount equally. The MDX query to do this allocation is:

     update cube [Adventure Works DW]     set (     [WB Employee].[Manager].&[290]     , [WB Period].[Period].&[20043]) = 2000 use_equal_increment 

Send the following MDX query to see if the allocation with increment worked correctly:

     select {[WB Employee].[Manager].&[290],     [WB Employee].[Manager].&[290].children     } on 1 ,     [WB Period].[Period].&[2004].children on 0     from     [Adventure Works DW] 

You will see the results shown in the following table whereby Jae Pak's budget for 2004 Q3 is now $1,250 and the budget for remaining employees is $250 each. Note the total budget allocated for Amy and her direct reports is $2,000, which is sum of the budget of all employees reporting to her.

Open table as spreadsheet

2004 Q1

2004 Q2

2004 Q3

Alberts,Amy

2072000

2865000

2000

Alberts,Amy

116000

1000

250

Pak,Jae

883000

1329000

1250

Valdez,Rachel

366000

627000

250

Varkey Chudukatil,Ranjit

707000

908000

250

Similarly you can use the USE_WEIGHTED_INCREMENT option to writeback to the cell corresponding to Amy Alberts and see that the budget gets distributed based on weights and also gets added to existing budget amounts.

You learned how to update a cube's cell data using the cell writeback feature of Analysis Services 2005. You also learned that the changes are propagated back to the cube only when you issue a commit statement. Therefore, you can perform many what-if scenarios by doing allocations followed by queries; this will surface the influence of the allocations on the financial status of the company. Typically we expect calculations to be defined in MDX scripts that make use of the measure values such as budget, which will reflect the overall profit or key performance indicators of the company. If the updates you have done do not yield the expected results, you can roll back the complete transaction thereby preventing the entire update operation to be propagated to the writeback table.

You learned how to writeback values to your cube and do what-if analysis or scenarios by sending MDX queries to see the effects of the writeback. Similar to dimension writeback where you had certain limitations, there are some things you should be aware of while updating cell values using the UPDATE statement. Assume you have a large cube with several dimensions (greater than twenty dimensions). Due to multidimensionality, a specific cell is now referred to by multiple dimensions. If you do an allocation using the UPDATE statement that includes only a few dimension's granularity attributes, Analysis Services will try to equally distribute the value allocated to a cell in the cube to all the leaf-level cells (across all the hierarchies in each dimension). Hence if you do an update on a cell that is referred to by the topmost-level on certain dimensions, the update to leaf levels can be quite expensive because Analysis Services needs to equally distribute the value across all members of all dimensions. Such an update will result in a lot of rows being entered into the writeback table. Hence whenever possible please make sure you do the writeback to the appropriate level intended. We are just warning you about data expansion.

To understand the data expansion problem better, consider the following example of updating Amy Alberts's budget for the year 2003 which is referred to by the tuple ([WB Employee].[Manager].&[290], [WB Period].[Period].&[2003]). This will update all the leaf-level members, which is the product of all the members reporting to Amy Alberts and all the quarters in 2003. This update results in changes to 16 cells at the leaf level. Imagine dimensions that have hundreds or even thousands of members. As with a Product dimension, a simple mistake of updating at the topmost level will cascade out with millions of leaf-level cells being updated — and you will see millions of rows in the writeback table. To mitigate this problem you need to identify meaningful leaf-level cells and then writeback to just those specific cells.

Keep in mind that you can still do what-if analysis on a cube, even if the cube is not enabled for write-back. The changed measure cannot be committed back to the server if the cube doesn't have a writeback partition. However, you can still do a "begin transaction," update cell values, and send MDX queries to view the results of the measure (cell value) change, and then simply "rollback transaction" when finished.



Professional SQL Server Analysis Services 2005 with MDX
Professional SQL Server Analysis Services 2005 with MDX (Programmer to Programmer)
ISBN: 0764579185
EAN: 2147483647
Year: 2004
Pages: 176

Similar book on Amazon

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net