Table Value Function VS ORs

  • Hi,

    I'm executing the following queries:

    UPDATE AcumuladosContas SET MES12DB = MES12DB + 5000.00 WHERE TipoLancamento = '000' AND Ano=2012 AND Moeda = 'EUR' AND (Conta = '111' OR Conta = '11')

    UPDATE AcumuladosContas SET MES12DB = MES12DB + 5000.00 FROM (SELECT SubConta FROM dbo.GetSubContasTVF('111')) t WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR' AND Conta = t.SubConta

    UPDATE AcumuladosContas SET MES12DB = MES12DB + 5000.00 WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR' AND Conta IN (SELECT SubConta FROM dbo.GetSubContasTVF('111'))

    The TVF is:

    ALTER FUNCTION [dbo].[GetSubContasTVF](@Conta NVARCHAR(20))

    RETURNS @SubContas TABLE (SubConta NVARCHAR(20) NOT NULL PRIMARY KEY)

    AS

    BEGIN

    INSERT INTO @SubContas SELECT SUBSTRING(@Conta, ID, LEN(@Conta)) SubConta FROM PriTally WHERE ID BETWEEN 1 AND LEN(@Conta) - 1

    RETURN;

    END

    In SSMS the execution plan for the queries states that the OR takes 30% and the other two 35% each.

    In SQL Sentry Plan Explorer the OR takes 22%, the 2nd 52% and the 3rd 26%...

    When using SQLStress the OR is faster with only one thread but when using multiple threads the other ones are faster...

    What's the best query?

    Is there any other query to perform the operation?

    Thanks,

    Pedro



    If you need to work better, try working less...

  • Forgot to mention that I'm using the TVF since the query is build dynamically and I'm using EXEC sp_executeSQL with the query and parameters and if I use the OR the query statement won't have as many parameters as using the function, hence the procedure cache won't be as good as it could be:

    SET @stm = 'UPDATE AcumuladosContas SET ' + @MonthField + ' = ' + @MonthField + ' + @Value WHERE TipoLancamento = @TipoLancamento AND Ano= @Ano AND Moeda = @Moeda AND ' + @ContasOr

    -- vs --

    SET @stm = 'UPDATE AcumuladosContas SET ' + @MonthField + ' = ' + @MonthField + ' + @Value WHERE TipoLancamento = @TipoLancamento AND Ano= @Ano AND Moeda = @Moeda AND Conta IN (SELECT SubConta FROM dbo.GetSubContasTVF(@Conta))'



    If you need to work better, try working less...

  • Try using a proper iTVF, the performance will be far better:

    ALTER FUNCTION [dbo].[GetSubContasTVF](@Conta NVARCHAR(20))

    RETURNS TABLE

    AS

    RETURN SELECT SUBSTRING(@Conta, ID, LEN(@Conta)) SubConta FROM PriTally WHERE ID BETWEEN 1 AND LEN(@Conta) - 1

    “Write the query the simplest way. If through testing it becomes clear that the performance is inadequate, consider alternative query forms.” - Gail Shaw

    For fast, accurate and documented assistance in answering your questions, please read this article.
    Understanding and using APPLY, (I) and (II) Paul White
    Hidden RBAR: Triangular Joins / The "Numbers" or "Tally" Table: What it is and how it replaces a loop Jeff Moden

  • I created a new function:

    CREATE FUNCTION [dbo].[GetSubContasiTVF](@Conta NVARCHAR(20))

    RETURNS TABLE

    AS

    RETURN SELECT SUBSTRING(@Conta, ID, LEN(@Conta)) SubConta FROM PriTally WHERE ID BETWEEN 1 AND LEN(@Conta) - 1

    According to Execution Plan the queries using the new function are slower..

    UPDATE AcumuladosContas SET MES12DB = MES12DB + 5000.00 WHERE TipoLancamento = '000' AND Ano=2012 AND Moeda = 'EUR' AND (Conta = '111' OR Conta = '11')

    -- 3%

    UPDATE AcumuladosContas SET MES12DB = MES12DB + 5000.00 FROM (SELECT SubConta FROM dbo.GetSubContasTVF('111')) t WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR' AND Conta = t.SubConta

    -- 4%

    UPDATE AcumuladosContas SET MES12DB = MES12DB + 5000.00 WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR' AND Conta IN (SELECT SubConta FROM dbo.GetSubContasTVF('111'))

    -- 4%

    UPDATE AcumuladosContas SET MES12DB = MES12DB + 5000.00 FROM (SELECT SubConta FROM dbo.GetSubContasiTVF('111')) t WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR' AND Conta = t.SubConta

    -- 24%

    UPDATE AcumuladosContas SET MES12DB = MES12DB + 5000.00 WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR' AND Conta IN (SELECT SubConta FROM dbo.GetSubContasiTVF('111'))

    -- 66%

    Am I using the new function the wrong way?!

    I use iTVF when the parameter to the function is a field from a table so it's used in a JOIN.. much faster than msTVF ou UDF.. but this isn't the case...

    Thanks,

    Pedro



    If you need to work better, try working less...

  • Please post the DDL for the table(s), sample data for the tables, expected results after the update(s) are completed, and the code used for the updates (including code for the table valued functions).

  • You can't compare queries like this - you have to time them using a realistically-sized data set. "% of the batch" is 100% unreliable.

    If a function contains a BEGIN/END block then it's not an iTVF, it's an msTVF. Lynn Pettis ran some tests on different function types recently. His properly-written iTVF cost about the same as a calculation in the SELECT and was about 200 times faster than the msTVF (same type as your function above).

    “Write the query the simplest way. If through testing it becomes clear that the performance is inadequate, consider alternative query forms.” - Gail Shaw

    For fast, accurate and documented assistance in answering your questions, please read this article.
    Understanding and using APPLY, (I) and (II) Paul White
    Hidden RBAR: Triangular Joins / The "Numbers" or "Tally" Table: What it is and how it replaces a loop Jeff Moden

  • Lynn Pettis (3/5/2013)


    Please post the DDL for the table(s), sample data for the tables, expected results after the update(s) are completed, and the code used for the updates (including code for the table valued functions).

    The structure is this:

    SET NOCOUNT ON

    GO

    -- create tally table

    CREATE TABLE PriTally (ID INT IDENTITY(1,1) NOT NULL, CONSTRAINT PK_PriTally PRIMARY KEY CLUSTERED (ID))

    GO

    INSERT INTO PriTally DEFAULT VALUES

    GO 100000

    GO

    --create iTVF

    CREATE FUNCTION [dbo].[GetSubContasiTVF](@Conta NVARCHAR(20))

    RETURNS TABLE

    AS

    RETURN SELECT SUBSTRING(@Conta, ID, LEN(@Conta)) SubConta FROM PriTally WHERE ID BETWEEN 1 AND LEN(@Conta) - 1

    GO

    --create msTVF

    CREATE FUNCTION [dbo].[GetSubContasTVF](@Conta NVARCHAR(20))

    RETURNS @SubContas TABLE (SubConta NVARCHAR(20) NOT NULL PRIMARY KEY)

    AS

    BEGIN

    INSERT INTO @SubContas SELECT SUBSTRING(@Conta, ID, LEN(@Conta)) SubConta FROM PriTally WHERE ID BETWEEN 1 AND LEN(@Conta) - 1

    RETURN;

    END

    GO

    --create table

    CREATE TABLE [dbo].[AcumuladosContas](

    [Ano] [smallint] NOT NULL,

    [Conta] [nvarchar](20) NOT NULL,

    [Moeda] [nvarchar](3) NOT NULL,

    [Mes00CR] [money] NULL DEFAULT 0,

    [Mes01CR] [money] NULL DEFAULT 0,

    [Mes02CR] [money] NULL DEFAULT 0,

    [Mes03CR] [money] NULL DEFAULT 0,

    [Mes04CR] [money] NULL DEFAULT 0,

    [Mes05CR] [money] NULL DEFAULT 0,

    [Mes06CR] [money] NULL DEFAULT 0,

    [Mes07CR] [money] NULL DEFAULT 0,

    [Mes08CR] [money] NULL DEFAULT 0,

    [Mes09CR] [money] NULL DEFAULT 0,

    [Mes10CR] [money] NULL DEFAULT 0,

    [Mes11CR] [money] NULL DEFAULT 0,

    [Mes12CR] [money] NULL DEFAULT 0,

    [Mes13CR] [money] NULL DEFAULT 0,

    [Mes14CR] [money] NULL DEFAULT 0,

    [Mes15CR] [money] NULL DEFAULT 0,

    [Mes00DB] [money] NULL DEFAULT 0,

    [Mes01DB] [money] NULL DEFAULT 0,

    [Mes02DB] [money] NULL DEFAULT 0,

    [Mes03DB] [money] NULL DEFAULT 0,

    [Mes04DB] [money] NULL DEFAULT 0,

    [Mes05DB] [money] NULL DEFAULT 0,

    [Mes06DB] [money] NULL DEFAULT 0,

    [Mes07DB] [money] NULL DEFAULT 0,

    [Mes08DB] [money] NULL DEFAULT 0,

    [Mes09DB] [money] NULL DEFAULT 0,

    [Mes10DB] [money] NULL DEFAULT 0,

    [Mes11DB] [money] NULL DEFAULT 0,

    [Mes12DB] [money] NULL DEFAULT 0,

    [Mes13DB] [money] NULL DEFAULT 0,

    [Mes14DB] [money] NULL DEFAULT 0,

    [Mes15DB] [money] NULL DEFAULT 0,

    [Mes01OR] [money] NULL DEFAULT 0,

    [Mes02OR] [money] NULL DEFAULT 0,

    [Mes03OR] [money] NULL DEFAULT 0,

    [Mes04OR] [money] NULL DEFAULT 0,

    [Mes05OR] [money] NULL DEFAULT 0,

    [Mes06OR] [money] NULL DEFAULT 0,

    [Mes07OR] [money] NULL DEFAULT 0,

    [Mes08OR] [money] NULL DEFAULT 0,

    [Mes09OR] [money] NULL DEFAULT 0,

    [Mes10OR] [money] NULL DEFAULT 0,

    [Mes11OR] [money] NULL DEFAULT 0,

    [Mes12OR] [money] NULL DEFAULT 0,

    [TipoLancamento] [varchar](3) NOT NULL,

    [NaturezaOR] [varchar](1) NOT NULL,

    CONSTRAINT [AcumuladosContas01] PRIMARY KEY CLUSTERED

    (

    [TipoLancamento] ASC,

    [Ano] ASC,

    [Conta] ASC,

    [Moeda] ASC

    )

    )

    GO

    --create index

    CREATE NONCLUSTERED INDEX [IDX_AcumuladosContas_Teste] ON [dbo].[AcumuladosContas]

    (

    [Ano] ASC,

    [Moeda] ASC,

    [Conta] ASC,

    [TipoLancamento] ASC

    )

    INCLUDE ([Mes12DB])

    GO

    -- insert data

    DELETE FROM AcumuladosContas

    GO

    -- the "target" rows

    INSERT INTO AcumuladosContas (Ano, Moeda, TipoLancamento, NaturezaOR, Conta, Mes12DB) VALUES (2012, 'EUR', '000', 'C', '111', 100)

    INSERT INTO AcumuladosContas (Ano, Moeda, TipoLancamento, NaturezaOR, Conta, Mes12DB) VALUES (2012, 'EUR', '000', 'C', '11', 100)

    GO

    -- insert 10.000 misc rows (can occour duplicate erros but batch continues and still inserts some considerable data)

    INSERT INTO AcumuladosContas (Ano, Moeda, TipoLancamento, NaturezaOR, Conta, Mes12DB) VALUES (2012, 'EUR', '000', 'C', CAST(RAND()*100000000 AS NVARCHAR(20)), 100)

    GO 10000

    GO

    SELECT COUNT(1) FROM AcumuladosContas

    GO

    The indexes will be quite fragmented so should run:

    ALTER INDEX [IDX_AcumuladosContas_Teste] ON AcumuladosContas REBUILD

    ALTER INDEX [AcumuladosContas01] ON AcumuladosContas REBUILD

    The update statements being compared are the following:

    UPDATE AcumuladosContas SET MES12DB = MES12DB + 5000.00 WHERE TipoLancamento = '000' AND Ano=2012 AND Moeda = 'EUR' AND (Conta = '111' OR Conta = '11')

    UPDATE AcumuladosContas SET MES12DB = MES12DB + 5000.00 FROM dbo.GetSubContasTVF('111') t WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR' AND Conta = t.SubConta

    UPDATE AcumuladosContas SET MES12DB = MES12DB + 5000.00 WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR' AND Conta IN (SELECT SubConta FROM dbo.GetSubContasTVF('111'))

    UPDATE AcumuladosContas SET MES12DB = MES12DB + 5000.00 FROM dbo.GetSubContasiTVF('111') t WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR' AND Conta = t.SubConta

    UPDATE AcumuladosContas SET MES12DB = MES12DB + 5000.00 WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR' AND Conta IN (SELECT SubConta FROM dbo.GetSubContasiTVF('111'))

    By each execution of an UPDATE the row should increase 5000. So after a run with the 5 statements the rows for Conta 111 and 11 should have Mes12DB with 25100.

    SELECT Ano,Conta,Moeda,Mes12DB FROM AcumuladosContas WHERE Conta IN ('111', '11')

    Thanks,

    Pedro



    If you need to work better, try working less...

  • I have to head to work. I may look at while eating lunch else it will be tonight.

  • You should avoid this syntax:

    UPDATE AcumuladosContas SET MES12DB = MES12DB + 5000.00

    FROM dbo.GetSubContasTVF('111') t

    WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR'

    AND Conta = t.SubConta

    UPDATE ...FROM... should list the update target (AcumuladosContas ) first. SQL Server won't raise an error, however, some folks claim to have observed very poor performance when this rule has been broken.

    “Write the query the simplest way. If through testing it becomes clear that the performance is inadequate, consider alternative query forms.” - Gail Shaw

    For fast, accurate and documented assistance in answering your questions, please read this article.
    Understanding and using APPLY, (I) and (II) Paul White
    Hidden RBAR: Triangular Joins / The "Numbers" or "Tally" Table: What it is and how it replaces a loop Jeff Moden

  • Lynn Pettis (3/5/2013)


    I have to head to work. I may look at while eating lunch else it will be tonight.

    Thanks.

    I believe that in this case, since the parameter to the iTVF isn't a table column but a variable, the msTVF is faster. At least that's what the plan says..

    Also the OR can be faster but for short values.. For a value of 10 or more chars the functions are much faster, also there's also the WHILE cycle that "generates" the OR.

    In the example I just put the OR already built but in the SP is a code for building the OR statement..

    ...

    DECLARE @params NVARCHAR(MAX) = '@ValorIn MONEY, @TipoLancamentoIn NVARCHAR(3), @AnoIn SMALLINT, @MoedaIn NVARCHAR(3), @ContaIn NVARCHAR(20)'

    SET @StrSQL = 'UPDATE AcumuladosContas SET ' + @CampoMes + ' = ' + @CampoMes + ' + @ValorIn FROM dbo.GetSubContasiTVF(@ContaIn) t WHERE TipoLancamento = @TipoLancamentoIn AND Ano = @AnoIn AND Moeda = @MoedaIn AND Conta = t.SubConta'

    EXEC sp_executeSQL @StrSQL, @params, @ValorIn = @Valor, @TipoLancamentoIn = @TipoLancamento, @AnoIn = @Ano, @MoedaIn = @MoedaBase, @ContaIn = @Conta

    --- vs ---

    SELECT @SqlWHEREConta = '(Conta = ''' + @Conta + ''''

    WHILE Len(@Conta) > 2

    BEGIN

    SELECT @Conta = Substring(@Conta,1,Len(@Conta) - 1)

    SELECT @SqlWHEREConta = @SqlWHEREConta + ' OR Conta = ''' + @Conta + ''''

    END

    SELECT @SqlWHEREConta = @SqlWHEREConta + ')'

    SELECT @StrSQL = 'UPDATE AcumuladosContas SET ' + @CampoMes + ' = ' + @CampoMes + ' + ' + Convert(NVARCHAR,@Valor) + ' WHERE TipoLancamento = ''' + @TipoLancamento + ''' AND Ano=' + Convert(NVARCHAR,@Ano) + ' AND Moeda = ''' + @MoedaBase + ''' AND ' + @SqlWHEREConta

    EXECUTE( @StrSQL)

    I'm using the function approach so the sp_executeSQL has only one cached plan...

    Thanks,

    Pedro



    If you need to work better, try working less...

  • ChrisM@Work (3/5/2013)


    You should avoid this syntax:

    UPDATE AcumuladosContas SET MES12DB = MES12DB + 5000.00

    FROM dbo.GetSubContasTVF('111') t

    WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR'

    AND Conta = t.SubConta

    UPDATE ...FROM... should list the update target (AcumuladosContas ) first. SQL Server won't raise an error, however, some folks claim to have observed very poor performance when this rule has been broken.

    And this approach (I use this when I want a SELECT for UPDATE):

    UPDATE sq SET Mes12DB = Mes12DB + 5000 FROM (SELECT Mes12DB FROM AcumuladosContas a JOIN dbo.GetSubContasTVF('111') t ON a.Conta = t.SubConta WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR') sq

    Thanks,

    Pedro



    If you need to work better, try working less...

  • PiMané (3/5/2013)


    Lynn Pettis (3/5/2013)


    I have to head to work. I may look at while eating lunch else it will be tonight.

    Thanks.

    I believe that in this case, since the parameter to the iTVF isn't a table column but a variable, the msTVF is faster. At least that's what the plan says..

    Also the OR can be faster but for short values.. For a value of 10 or more chars the functions are much faster, also there's also the WHILE cycle that "generates" the OR.

    In the example I just put the OR already built but in the SP is a code for building the OR statement..

    ...

    DECLARE @params NVARCHAR(MAX) = '@ValorIn MONEY, @TipoLancamentoIn NVARCHAR(3), @AnoIn SMALLINT, @MoedaIn NVARCHAR(3), @ContaIn NVARCHAR(20)'

    SET @StrSQL = 'UPDATE AcumuladosContas SET ' + @CampoMes + ' = ' + @CampoMes + ' + @ValorIn FROM dbo.GetSubContasiTVF(@ContaIn) t WHERE TipoLancamento = @TipoLancamentoIn AND Ano = @AnoIn AND Moeda = @MoedaIn AND Conta = t.SubConta'

    EXEC sp_executeSQL @StrSQL, @params, @ValorIn = @Valor, @TipoLancamentoIn = @TipoLancamento, @AnoIn = @Ano, @MoedaIn = @MoedaBase, @ContaIn = @Conta

    --- vs ---

    SELECT @SqlWHEREConta = '(Conta = ''' + @Conta + ''''

    WHILE Len(@Conta) > 2

    BEGIN

    SELECT @Conta = Substring(@Conta,1,Len(@Conta) - 1)

    SELECT @SqlWHEREConta = @SqlWHEREConta + ' OR Conta = ''' + @Conta + ''''

    END

    SELECT @SqlWHEREConta = @SqlWHEREConta + ')'

    SELECT @StrSQL = 'UPDATE AcumuladosContas SET ' + @CampoMes + ' = ' + @CampoMes + ' + ' + Convert(NVARCHAR,@Valor) + ' WHERE TipoLancamento = ''' + @TipoLancamento + ''' AND Ano=' + Convert(NVARCHAR,@Ano) + ' AND Moeda = ''' + @MoedaBase + ''' AND ' + @SqlWHEREConta

    EXECUTE( @StrSQL)

    I'm using the function approach so the sp_executeSQL has only one cached plan...

    Thanks,

    Pedro

    I truly disbelieve this statement:

    I believe that in this case, since the parameter to the iTVF isn't a table column but a variable, the msTVF is faster.

    A mTVF function is a black box and must completely populate the table variable that is returned from the function before processing may continue. A true iTVF is expanded as part of the query similar to a view, CTE, or derived table.

  • PiMané (3/5/2013)


    ChrisM@Work (3/5/2013)


    You should avoid this syntax:

    UPDATE AcumuladosContas SET MES12DB = MES12DB + 5000.00

    FROM dbo.GetSubContasTVF('111') t

    WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR'

    AND Conta = t.SubConta

    UPDATE ...FROM... should list the update target (AcumuladosContas ) first. SQL Server won't raise an error, however, some folks claim to have observed very poor performance when this rule has been broken.

    And this approach (I use this when I want a SELECT for UPDATE):

    UPDATE sq SET Mes12DB = Mes12DB + 5000 FROM (SELECT Mes12DB FROM AcumuladosContas a JOIN dbo.GetSubContasTVF('111') t ON a.Conta = t.SubConta WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR') sq

    Thanks,

    Pedro

    I'd expect the execution plan to be identical to this:

    UPDATE a SET Mes12DB = Mes12DB + 5000

    --FROM (

    --SELECT Mes12DB

    FROM AcumuladosContas a

    JOIN dbo.GetSubContasTVF('111') t

    ON a.Conta = t.SubConta

    WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR'

    --) sq

    “Write the query the simplest way. If through testing it becomes clear that the performance is inadequate, consider alternative query forms.” - Gail Shaw

    For fast, accurate and documented assistance in answering your questions, please read this article.
    Understanding and using APPLY, (I) and (II) Paul White
    Hidden RBAR: Triangular Joins / The "Numbers" or "Tally" Table: What it is and how it replaces a loop Jeff Moden

  • Is SQL Sentry Plan more accurate than the Execution Plan from SSMS?!

    I ask this cause in SSMS the TVF is better but in Sentry Plan iTVF is much better...

    Thanks,

    Pedro

    [Edited to add the attachment with SQL Sentry Plan results]



    If you need to work better, try working less...

  • ChrisM@Work (3/5/2013)


    PiMané (3/5/2013)


    ChrisM@Work (3/5/2013)


    You should avoid this syntax:

    UPDATE AcumuladosContas SET MES12DB = MES12DB + 5000.00

    FROM dbo.GetSubContasTVF('111') t

    WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR'

    AND Conta = t.SubConta

    UPDATE ...FROM... should list the update target (AcumuladosContas ) first. SQL Server won't raise an error, however, some folks claim to have observed very poor performance when this rule has been broken.

    And this approach (I use this when I want a SELECT for UPDATE):

    UPDATE sq SET Mes12DB = Mes12DB + 5000 FROM (SELECT Mes12DB FROM AcumuladosContas a JOIN dbo.GetSubContasTVF('111') t ON a.Conta = t.SubConta WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR') sq

    Thanks,

    Pedro

    I'd expect the execution plan to be identical to this:

    UPDATE a SET Mes12DB = Mes12DB + 5000

    --FROM (

    --SELECT Mes12DB

    FROM AcumuladosContas a

    JOIN dbo.GetSubContasTVF('111') t

    ON a.Conta = t.SubConta

    WHERE TipoLancamento = '000' AND Ano = 2012 AND Moeda = 'EUR'

    --) sq

    Yep. Same plan... 🙂



    If you need to work better, try working less...

Viewing 15 posts - 1 through 15 (of 17 total)

You must be logged in to reply to this topic. Login to reply