0

I have a table called Budgetline. It keeps track of budget lines for projects (Grants).

Schema:

BudgetId       int
Amount         decimal
LoginUser      varchar
InsertDate     datetime
GrantPhaseID   int
BudgetChartID  int
Rootdir        hierarchyid
OverHead       decimal

The Amount column is the budgeted amount for a budget line. A budget line can have a sub budgetline. A sub budgetline can have another sub budgetline. Sometimes there can be up to 5 levels.

There is another table TransactionsDetail; it keeps track of budgetline spending

Schema:

TransactionDetailID  int
TransactionID        int
Amount               numeric
ExRateAmount         numeric
TransactionDate      date
BudgetId             int
InsertDate           datetime
OverHead             decimal
Paid                 bit
PaidDate             datetime
LoginUser            varchar

Projects (Grants) have phases. There is another table GrantPhase to keep track of that.

There is another table called BudgetChart. It holds list of Budgetlines. Different projects (Grants) can have same budgetlines.

Below is the complete table-valued function to get the Sub budgetlines (descendants) of a budgetline (Parent).

ALTER FUNCTION [dbo].[getSUBS]
    (@BudgetID INT
     --,@GrantPhaseID INT
    )
RETURNS @Mytable TABLE (CID INT,
                        [COUNT] INT,
                        DESCRIPTION VARCHAR(256),
                        AMOUNT NUMERIC(18,2),
                        SPENT NUMERIC(18,2),
                        BALANCE NUMERIC(18,2),
                        OVERHEAD NUMERIC(18,2)
                        --  BUDGETLIMIT numeric(18,2)
                       )
AS
BEGIN

-- get budgetline root level
declare @BudgetIDrootRevel int
SELECT @BudgetIDrootRevel = RootDir.GetLevel() FROM budgetlines WHERE budgetid = @BudgetID

-- GET GRANTPHASEID
DECLARE @GrantPhaseID int=(
select GrantPhaseID from BudgetLines where BudgetId=@BudgetID
)

DECLARE @RootDir HIERARCHYID
SELECT @RootDir = RootDir FROM budgetlines WHERE budgetid = @BudgetID
insert into @Mytable(
    CID 
,   [COUNT]
,   DESCRIPTION 
,   AMOUNT 
,   SPENT
,   BALANCE
,   OVERHEAD 
--, BUDGETLIMIT
)

SELECT
    BudgetId 
,   ROW_NUMBER() OVER (ORDER BY BudgetID DESC)
,   [Description]
,   dbo.[getBudgetAmount](BudgetLines.BudgetId)  AMOUNT --Sums all transactions made in the TransactionDetails table
,   [dbo].[getBudgetSpent](BudgetId) as SPENT
,   ISNULL((dbo.[getBudgetAmount](BudgetLines.BudgetId)-[dbo].[getBudgetSpent](BudgetId)),0) as BALANCE
,   BudgetLines.OVERHEAD
--, BUDGETLIMIT

FROM BudgetLines INNER JOIN BudgetChart 
ON BudgetChart.BudgetChartID = BudgetLines.BudgetChartID

WHERE RootDir.IsDescendantOf(@RootDir)=1
and GrantPhaseID = @GrantPhaseID
and Rootdir.GetLevel()=(@BudgetIDrootRevel+1)
--AND isBudgetline=1

return ;

end

It works.

  • Budgetline table has only 252 records
  • TransactionDetails table has only 172 records

My Challenge:

It takes like 10 seconds to return sub budgetlines if that particular budgetline has 3 or more sub levels (descendants).

My Question:

Is there a better way to optimize(rewrite) this function so it can run faster.

Below is how the design looks like. User can see the sub budget lines either by double-clikcing a row or clicking the [Move Down] Button.

How the UI looks

Needless to say: This is my first post on almighty stackoverflow. Sorry If I'v violate any of the community rules. I'm still learning them.

Kaunda
  • 67
  • 1
  • 7
  • 1
    The common way to build and query hierarchical data in sqlserver is with a recursive cte - take a look at: https://stackoverflow.com/questions/3187850/how-does-a-recursive-cte-run-line-by-line - also googling "recursive cte" and hitting SO's top answers for more. https://stackoverflow.com/questions/14518090/recursive-query-in-sql-server https://stackoverflow.com/questions/45888858/understanding-the-recursive-cte-termination-check – Caius Jard Nov 11 '18 at 09:42
  • I actually read around CTE when I had the need to save hierarchical data. 1) I found it difficult understanding. 2) I read from Microsoft site that the HierarchyID data type is optimized for representing tree data structure. It says: "The built-in hierarchyid data type makes it easier to store and query hierarchical data. hierarchyid is optimized for representing trees, which are the most common type of hierarchical data." Source: (https://learn.microsoft.com/en-us/sql/relational-databases/hierarchical-data-sql-server?view=sql-server-2017) – Kaunda Nov 11 '18 at 11:41
  • Having read more closely, I'm kinda puzzled why you seem to be managing the tree traversal yourself - you seem to only go one level deeper than where you are currently in getsubs, but surely sqlserver's IsAncestorOf and isDescendantOf will walk the tree for you and get you all the data you require? – Caius Jard Nov 12 '18 at 05:20
  • That is what I used. The "IsDescendantOf" feature. U can see that in the "WHERE" section of the code: "WHERE RootDir.IsDescendantOf(@RootDir)=1". I'm not managing the tree traversal myself – Kaunda Nov 12 '18 at 15:40
  • Yeah, but you restrict the level too : `Rootdir.GetLevel()=(@BudgetIDrootRevel+1)` ? and then repeatedly call this function to get you each next level? If you want all the levels, why not just let SQLS get them all in one hit? – Caius Jard Nov 12 '18 at 16:05
  • It's true. I can't even remember why I foolishly put added "..and Rootdir.GetLevel()<=100" to the where condition. I'v taking it off. Thanks. But the slowness is same. – Kaunda Nov 12 '18 at 17:26
  • And what speed do you encounter in simply running `SELECT * FROM BudgetLines INNER JOIN BudgetChart ON BudgetChart.BudgetChartID = BudgetLines.BudgetChartID WHERE RootDir.IsDescendantOf(@RootDir)=1 and GrantPhaseID = @GrantPhaseID` ? I think you need to tune this first – Caius Jard Nov 12 '18 at 17:38
  • That returns records with lightening speed. That gave me a hint to check the scalar functions ("getBudgetAmount" and "getBudgetSpent") . That is where the headache it. It displays very fine and quick if I comment Budget, Spent and Variance columns. Thanks for the tip off – Kaunda Nov 13 '18 at 16:07
  • It's often a bad idea to use scalar udf in a query because it might mean that queries are run thousands or millions more queries than are necessary. Sometimes the query optimiser can extrapolate the query inside a udf and rewrite the main query so it includes the query inside the udf, but it's a slim chance. I'd be tempted to make those udf into stored procedures (so they can't be accidentally used in a query) and rewrite your query that used the udf, so that it just gets the required results as a single query - better optimization and set based operations provide a speed boost – Caius Jard Nov 13 '18 at 18:37
  • Thanks. I'm working towards that – Kaunda Nov 14 '18 at 09:55

1 Answers1

0

It's probably worth pointing out that you're asking for/possibly assuming this "get the sub budget lines" is best done in the database as soon as the main budget line is asked for

Consider that you're building a UI that understands/displays a master-detail relationship and a lot of this can be done in the UI with greater efficiency* if the sub levels won't be desired all the time.. There's no point chasing and returning 5 levels of hierarchical data if the user really only wants to view the root level in the UI. It may thus be better to let the UI drive the demand for data - just return relevant levels as they're requested by the user

*efficient in terms of: not wasting DB's time collecting data that is unwanted/not transferring data over a network if it won't be used

Caius Jard
  • 72,509
  • 5
  • 49
  • 80
  • 1) It's true: I'v never think around implementing it at the UI level. I will search into that. 2) My query returns only the *Wanted* data. Or it does something else behind the scene? – Kaunda Nov 11 '18 at 11:44