简体   繁体   中英

Dynamic SQL Calculations in Stored Procedure

This is tricky one. I'm writing my first SQL Server stored procedure that will require dynamic SQL and I'm running into a bit of a problem.

Here is the scenario: I am trying to allow the user to build equations in my application and then execute them in SQL Server based on the data stored therein.

Example:

x * (y + 10)

I have a series of items, and each item can have a value entered for it for any subdivision of any week. A simplified version of that table would look like:

Item | WeekEndingDate | Subdivision | Value
-------------------------------------------
1    | 13-Aug-15      | 4           | 100

It is like this because values need to be entered more often than each day every week.

I also have a subdivisions table that breaks out each part of the week.

Simplified it looks like this:

Subdivision | Name
----------------------------
1           | Monday Morning

There is also a third table (which I don't think I need to go into), which holds the different steps for each equation that the user has created.

What I'm trying to do is have the user supply a "WeekEndingDate", then perform a given user-defined equation on each "Value" for each "Subdivision" of that week.

Here is what I tried:

I put the calculations into the form of a T-SQL UDF wherein I use a cursor to loop through the steps of the equation and build a dynamic SQL string, which I could then execute for the result and return the result from the function.

I then tried to use it in a query like the one below:

SELECT dbo.DoCalculations(@Item, @WeekEnding, Subdivision), Subdivision, etc...
FROM Subdivisions

The problem is that, as I have found out, I can't execute dynamic SQL in an UDF. This prevents me from doing the calculation in dbo.DoCalculations and ruins the whole thing.

Is there any other way I can get the desired result?

EDIT:

Here is more data and samples of what I'm doing.

The calculations table looks like this:

ID | Sequence | UseVariable | ItemVariable | Constant | Operator | ParenLevel
------------------------------------------------------------------------------
1  | 1        | True        | 1            | NULL     | 1        | 0

To explain:

  • 'Sequence' shows the order of the steps in the equation.
  • 'UseVariable' tells us whether this step of the calculation uses an items table value as a variable, or whether it uses a constant value.
  • Depending on the value of 'UseVariable' either 'ItemVariable' or 'Constant' will have a value, the other will be null.
  • 'ItemVariable' Is a foreign key reference to the list of items which then links to the values for that item.
  • 'Operator' stores a tinyint where each value from 1 to 4 represent a numeric operator (+, -, *, /).
  • 'ParenLevel' is used to enclose equation steps in '(' and ')' by comparing it to the 'ParenLevel' of the previous step in the equation.

This is my calculation function:

FUNCTION [dbo].[DoCalculations]
(
    -- Add the parameters for the function here
    @ItemID nvarchar(MAX),
    @Subdivision nvarchar(MAX),
    @WeekEnding date
)
RETURNS decimal(18, 5)
AS
BEGIN
    --Return variable
    DECLARE @R decimal(18, 5)

    --Variables for cursor use
    DECLARE @UseVariable bit
    DECLARE @ItemVar nvarchar(MAX)
    DECLARE @Constant decimal(18, 5)
    DECLARE @Operator tinyint
    DECLARE @ParenLevel tinyint

    --Working variables
    DECLARE @CalcSQL varchar(MAX) = 'SELECT @R = (' --Note I'm leaving one open paren and that I am selecting the result into '@R'
    DECLARE @CurrentParenLevel tinyint
    DECLARE @StepTerm nvarchar(MAX)

    --Create the cursor to loop through the calculation steps
    DECLARE CalcCursor CURSOR FAST_FORWARD FOR
        SELECT UseVariable, ItemVariable, Constant, Operator, ParenLevel
        FROM CalculationSteps
        WHERE CalculationSteps.Item = @ItemID
        ORDER BY Sequence

    --Start looping
    OPEN CalcCursor
    FETCH NEXT FROM CalcCursor INTO @UseVariable, @ItemVar, @Constant, @Operator, @ParenLevel
    WHILE @@FETCH_STATUS = 0
    BEGIN
        --Check if wee need to add opening parens to the equation
        IF @ParenLevel > @CurrentParenLevel
        BEGIN
            WHILE (@CurrentParenLevel <> @ParenLevel)
            BEGIN
                SET @CalcSQL = @CalcSQL + '('
                SET @CurrentParenLevel = @CurrentParenLevel + 1
            END
        END

        --Check if this step is using a variable or a constant
        IF @UseVariable = 'True'
        BEGIN
            --If it's using a variable, create the sub-query string to get its value
            SET @StepTerm = '(SELECT ReportValue FROM Reports WHERE Slot = @Slot AND WeekEnding = @WeekEnding AND Stat = ' + @ItemVar + ')'
        END
        ELSE
        BEGIN
            --If its's using a constant, append its value
            SET @StepTerm = '(' + @Constant + ')'
        END

        --Add the step to the equation
        SET @CalcSQL = @CalcSQL + @StepTerm

        --Check if wee need to add closing parens to the equation
        IF @ParenLevel < @CurrentParenLevel
        BEGIN
            WHILE (@CurrentParenLevel <> @ParenLevel)
            BEGIN
                SET @CalcSQL = @CalcSQL + ')'
                SET @CurrentParenLevel = @CurrentParenLevel - 1
            END
        END

        --Add the operator between this step and the next, if any
        SET @CalcSQL = @CalcSQL + (CASE @Operator WHEN 0 THEN '' WHEN 1 THEN '+' WHEN 2 THEN '-' WHEN 3 THEN '*' WHEN 4 THEN '/' END)

        --Go to the next step
        FETCH NEXT FROM CalcCursor INTO @UseVariable, @ItemVar, @Constant, @Operator, @ParenLevel
    END
    CLOSE CalcCursor
    DEALLOCATE CalcCursor

    --Close any open parens in the equation
    WHILE (@CurrentParenLevel > 0)
    BEGIN
        SET @CalcSQL = @CalcSQL + ')'
        SET @CurrentParenLevel = @CurrentParenLevel - 1
    END

    --Close the original open paren to enclose the whole equation
    SET @CalcSQL = @CalcSQL + ')'

    --Execute the equation which should set the result to '@R'
    Exec @CalcSQL

    --Return '@R'
    RETURN @R

You don't really NEED dynamic SQL for building the formula I believe you only need it to execute it. And since you can't really use EXEC or sp_executesql within a function, that portion needs to remain separate. This is kind of a crude example, but you can probably accomplish this with a Table-Valued Parameter. If we knew a little bit more about the structure and perhaps some additional data samples you might be able to avoid Dynamic SQL all together;

CREATE TYPE CalcVariables as TABLE
(
VariableIndex int IDENTITY(1,1),
VariableName varchar(255),
VariableValue varchar(255)
)

CREATE FUNCTION dbo.Dyn_Calc
(
@CalcFormula varchar(255),
@CaclVars CalcVariables ReadOnly
)
RETURNS varchar(255)
BEGIN

DECLARE @Calculation varchar(255) = @CalcFormula
DECLARE @Index int
DECLARE @iName varchar(255)
DECLARE @iValue varchar(255) 
SET @Index = (SELECT MAX(VariableIndex) FROM @CaclVars)

WHILE @Index > 0
BEGIN
SET @iName = (SELECT VariableName FROM @CaclVars WHERE VariableIndex = @Index)
SET @iValue = (SELECT VariableValue FROM @CaclVars WHERE VariableIndex = @Index)

SET @Calculation = REPLACE(@Calculation,@iName,@iValue)

SET @Index = @Index -1
END

RETURN @Calculation
END

DECLARE @CalcFormula varchar(255),
@CaclVars CalcVariables,
@SQL nvarchar(3000)

SET @CalcFormula = '[x] * ([y] + 10)'
INSERT INTO @CaclVars
VALUES ('[x]','10'),
    ('[y]','5')


SET @SQL = 'SELECT ' + (SELECT dbo.Dyn_Calc (@CalcFormula, @CaclVars))
SELECT @SQL

EXEC(@SQL)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM