7

I have a query that is both prone to parameter sensitivity and is suffering from its table-valued parameter. I'm lazy and just want to solve this with query hints. When I'm lazy, I can solve parameter sensitivity with OPTION (OPTIMIZE FOR UNKNOWN). This gives me three very tightly related questions:

  1. Does OPTION (OPTIMIZE FOR UNKNOWN) have any impact on table-valued variables?
  2. Does OPTION (OPTIMIZE FOR UNKNOWN) have any impact on table-valued parameters?
  3. What query hints other than OPTION(RECOMPILE) can be used to influence cardinality estimates for table-valued parameters?

Both my own experiments and the documentation suggest that this is a lost cause. Table-valued parameters are subject to parameter sniffing, but all that gets sniffed is their cardinality so OPTION (OPTIMIZE FOR UNKNOWN) ought to do nothing.

As for the question of using other table hints, I know that I can change the estimate for operations on the table-valued parameter (e.g. OPTION (USE HINT ('ASSUME_MIN_SELECTIVITY_FOR_FILTER_ESTIMATES')) but I do not think that hints can influence the cardinality estimate for the parameter itself.

Paul White
  • 94,921
  • 30
  • 437
  • 687
J. Mini
  • 1,161
  • 8
  • 32

2 Answers2

8

oops

It does seem that OPTIMIZE FOR UNKNOWN has no effect at all on either table variables or parameters. In fact, if you try to specify OPTIMIZE FOR (@data UNKNOWN) or OPTIMIZE FOR (@data = NULL) then you get

Msg 137 Level 16 State 1 Line 7
Must declare the scalar variable "@data".

which tells us this hint is only intended for scalar variables, even though this isn't really documented.

Historically, table variables did not get any cardinality estimation and had an estimate of 1 or 100 rows. Table Variable Deferred Compilation was introduced using trace flag 2453, and from 2019 as a database which you can disable using a hint. This will usually cause the estimates to be correct, which in your case seems to be undesired.


get the hint

  • Turning off TF2453, or using the query hint USE HINT (N'DISABLE_DEFERRED_COMPILATION_TV') only works on table variables, not parameters.
  • ROBUST PLAN and FAST 1 do not affect cardinality estimation here.

the bad

You can insert the whole thing into a table variable. This by default gets an estimate of 1 or 100 rows, depending on version.

You also need to turn off TF2453 if it's on. On 2019 and later, you need to instead disable Deferred Table Variable Compilation. You could instead use QUERY_OPTIMIZER_COMPATIBILITY_LEVEL_100, but that's a broad hammer.

CREATE OR ALTER PROCEDURE p
  @data dbo.t READONLY
AS

DECLARE @data2 dbo.t; INSERT @data2 SELECT * FROM @data;

SELECT COUNT(*) AS c FROM @data2 OPTION (USE HINT (N'DISABLE_DEFERRED_COMPILATION_TV'));

This has the obvious downside of needing to copy the entire TVP, which may be very large.


the ugly

You can use sp_prepare and sp_execute with dynamic SQL, to force compilation without cardinality estimation, as noted in @PaulWhite's article. There is no copying in this instance.

CREATE OR ALTER PROCEDURE p
  @data dbo.t READONLY
AS

DECLARE @handle int; EXEC sp_prepare @handle OUT, N'@data dbo.t READONLY', N' SELECT COUNT(*) AS c FROM @data; '; EXEC sp_execute @handle, @data = @data;

This only works with simple statements, otherwise compilation gets deferred and the parameter will be sniffed.

But it makes the procedure a bit of a mess to write and debug.


the good

Just sort your query out. Why does OPTION (RECOMPILE) not solve the issue, or what is throwing off the optimizer when it does have the correct estimate? Solve that instead.

Or just get a good plan and force it using Query Store.


You can see the options in this fiddle.

Charlieface
  • 17,078
  • 22
  • 44
3

Answers to Questions

  1. Does OPTION (OPTIMIZE FOR UNKNOWN) have any impact on table-valued variables?

No. OPTIMIZE FOR UNKNOWN is just a variation of OPTIMIZE FOR (@variable_name = <literal_constant>). It only applies to variables for which a literal constant value could be supplied. Table-valued variables cannot be SET to a literal constant value (even NULL).

  1. Does OPTION (OPTIMIZE FOR UNKNOWN) have any impact on table-valued parameters?

No, for the same reasons.

  1. What query hints other than OPTION(RECOMPILE) can be used to influence cardinality estimates for table-valued parameters?

None. OPTION(RECOMPILE) is the hint to use if the cardinality saved with the plan is not expected to be representative or suitable for the current execution.


Recompilation

A recompilation always sees the current cardinality of a table variable or TVP. Recompilations can be triggered by a wide variety of causes, including statistics being out of date on another table used in the same statement. The point is that whatever the cause, a recompilation always sees current cardinality.

TF 2453 and the Recompilation Threshold (RT)

TF 2453 (available from SQL Server 2012 SP2) added a new possible recompilation cause by enabling the Recompilation Threshold crossing test for table variables and TVPs.

This test causes a recompilation if table cardinality changes by more than RT.

Assuming you do not get a trivial plan for the statement(s) referencing the TVP, recompilation will occur if RT is exceeded, either within the current module or across executions. How changes are recorded and accumulate across module executions is not entirely intuitive—see my linked article just above for details.

Deferred compilation

While an improvement, TF 2453 resulted in an initial compile of the statement in the batch as usual but added a recompilation for the statement(s) referencing the table variable or TVP when RT is exceeded.

Deferred compilation improves on this (for compatibility level 150 and above) by deferring the initial compilation. This new feature therefore made 2453 largely redundant.

Incorrect documentation

The exception to that is the RT crossing test for table cardinality. This test continues to require TF 2453 even on SQL Server 2019 and later, despite what the documentation says.

Starting with SQL Server 2019 (15.x), this became table variable deferred compilation, and Trace Flag 2453 has no effect.

To that extent, the problem of stored cardinality is largely addressed for SQL Server versions with TF 2453 enabled, because a recompilation is triggered by sufficient changes in table cardinality.

TF 2453 is effective for the RT threshold crossing test in all versions from SQL Server 2012 onward, regardless of compatibility level (even 90 on SQL Server 2012). It is not effective with QUERYTRACEON, which is again counter to the documentation.


Demo

SET NOCOUNT ON;
GO
CREATE TYPE dbo.TVP AS table (i integer NULL);
GO
CREATE PROCEDURE dbo.P
    @TVP AS TVP READONLY
AS
SELECT 
    c = COUNT_BIG(*) 
FROM @TVP
WHERE 
    -- Avoid trivial plan
    1 = (SELECT 1);

I'm using documented trace flag 205 to report recompilations:

DBCC TRACEON (3604, 205, 2453);
GO
DECLARE @TVP AS TVP;

INSERT @TVP SELECT SV.number FROM master.dbo.spt_values AS SV WHERE SV.type = N'P' AND SV.number BETWEEN 1 AND 10;

EXECUTE dbo.P @TVP = @TVP; GO DECLARE @TVP AS TVP;

INSERT @TVP SELECT SV.number FROM master.dbo.spt_values AS SV WHERE SV.type = N'P' AND SV.number BETWEEN 1 AND 510;

EXECUTE dbo.P @TVP = @TVP; GO DBCC TRACEOFF (3604, 205, 2453);

Output:

Data related recompile(card change):
  Tbl Dbid: 2, 
  Objid: -1082584932, 
  current rowcount: 510, 
  compile time rowcount: 10, 
  threshold: 500 (small table)

The execution plans show changed cardinality estimates for the TVP due to the recompilation.

Tidy up:

DROP PROCEDURE dbo.P;
DROP TYPE TVP;
Paul White
  • 94,921
  • 30
  • 437
  • 687