How Does The CHOOSE Command Affect Performance?

Questions absolutely drive my blog content and I really liked this one: how does the T-SQL CHOOSE command affect performance.

On the face of it, I honestly don’t think it will affect performance at all, depending on where and how you use it. However, the answer is always best supplied by testing.

T-SQL CHOOSE Command

The CHOOSE command was added in SQL Server 2012. It’s fairly straight forward. You supply an array and a numbered index for that array and CHOOSE will pull the matching value for that index. It works like this. We’ll start with a simple proc and execute it:

CREATE OR ALTER PROC dbo.CarrierAndFlag
(
    @SalesOrderID INT,
    @Flag INT
)
AS
BEGIN
    SELECT sod.CarrierTrackingNumber,
           CHOOSE(@Flag, 'A', 'B', 'C') AS Flag
    FROM Sales.SalesOrderDetail AS sod
    WHERE sod.SalesOrderID = @SalesOrderID;
END;
GO

EXEC dbo.CarrierAndFlag @SalesOrderID = 43662, -- int
                        @Flag = 2;             -- int

The query runs. The results come back showing the ‘B’ value for the Flag. The execution plan looks like this:

Simple seek against the clustered index. Nothing to it. The work is done in the Compute Scalar operator of course. We can see that here:

Now, we could modify our proc as follows, to use the CHOOSE against columns instead of a supplied array:

CREATE OR ALTER PROC dbo.CarrierAndFlag
(
    @SalesOrderID INT,
    @Flag INT
)
AS
BEGIN
    SELECT sod.CarrierTrackingNumber,
           CHOOSE(@Flag, sod.OrderQty,sod.LineTotal,sod.UnitPrice) AS Flag
    FROM Sales.SalesOrderDetail AS sod
    WHERE sod.SalesOrderID = @SalesOrderID;
END;
GO

The resulting execution plan is identical to the one above, except of course that the Compute Scalar has changed:

The documentation explains about how it uses data type precedence to determine the conversion, but overall, we’re not looking at performance issues here. In fact, on average, the first iteration of the proc ran in about 449 mc with 3.24 reads. The second about 447 mc with 3.24 reads. In other words, identical in every way.

So we can now safely conclude that CHOOSE doesn’t affect performance, right?

Well, hold on a sec.

Nonclustered Indexes and CHOOSE

I’m going to change the procedure to look for ProductID. There’s a nonclustered index that should be useful in AdventureWorks:

CREATE OR ALTER PROC dbo.CarrierAndFlag
(
    @ProductID INT,
    @Flag INT
)
AS
BEGIN
    SELECT sod.CarrierTrackingNumber,
           CHOOSE(@Flag, sod.OrderQty,sod.LineTotal,sod.UnitPrice) AS Flag
    FROM Sales.SalesOrderDetail AS sod
    WHERE sod.ProductID = @ProductID;
END;
GO

And, depending on the value you pass for ProductID (parameter sniffing, different story), you’ll get the following plan:

So, CHOOSE doesn’t seem to affect index choice. Can we add an INCLUDE operator to eliminate the Key Lookup and still have CHOOSE work appropriately? Here’s my index:

CREATE INDEX ChooseTest
CREATE INDEX ChooseTest
ON Sales.SalesOrderDetail (ProductID)
INCLUDE (
            CarrierTrackingNumber,
			OrderQty,
            LineTotal,
            UnitPriceDiscount,
			UnitPrice
        );

Now when we execute the query we get the following execution plan:

There we go. Question answered, you can use CHOOSE in any situation and performance won’t be affected.

Hang on, one more point.

CHOOSE and the WHERE Clause

What happens when we move it out of the nice safe spot in SELECT? Let’s modify the proc again:

CREATE OR ALTER PROC dbo.CarrierAndFlag
(
    @Flag INT
)
AS
BEGIN
    SELECT sod.CarrierTrackingNumber
    FROM Sales.SalesOrderDetail AS sod
    WHERE sod.ProductID = CHOOSE(@Flag, 897, 998, 432);
END;
GO

If we then execute the code for the @Flag value of 2, we get the following plan:

And this is where things get fun. If we recompile the proc or remote it from cache and pass in the @Flag value of 1, the results change from 685 to 2. However the row estimate stays the same. It’s using an average against the statistics in the index. However, it’s using the index.

Let’s break it.

Breaking Index Use of CHOOSE

I’m honestly shocked that moving that to the WHERE clause didn’t result in an immediate scan. Sure, maybe of the nonclustered index, but absolutely a scan, not seek.

So, doing some experiments, changing the value to a string just resulted in a benign implicit conversion, but the index was still used. Trying it with decimals became the same. That makes sense based on the documentation. Even mixing the data types didn’t affect the plans.

Then, I added a column to the CHOOSE statement. And we got this for the plan:

The key is found in the predicate:

Adding in the column, and I tried with different data types, safe ones, nothing crazy, and no matter what, I was back on the scan.

Conclusion

There are a whole bunch more tests I could write up. If you had asked me how this would behave in the WHERE clause, I would have predicted that it was always going to be scan. I was a little surprised that it could use the indexes a little better. However, the behavior of the addition of a column made a lot of sense to me.

Overall, I’d say using CHOOSE in the SELECT clause is fine from a performance standpoint. In a filter clause, if you’re using values, you’re probably OK. If you’re trying to toss columns in there, don’t.

NOTE: The post originally stated that CHOOSE was introduced in 2016. It was actually introduced in 2012 and the text now reflects that. Thanks for catching this Robert.

7 thoughts on “How Does The CHOOSE Command Affect Performance?

  • Robert

    With any new feature, such as choose, someone will find a creative way to use that will bring out the worse in query performance. The queries below seems to be a very slick use of the choose function with bad cardinality estimates that propagates from the leaf level to the final operator.

    DECLARE @Column INT = 1,
    @Value INT = 43659;

    SELECT CarrierTrackingNumber
    , OrderQty
    , UnitPrice
    , LineTotal
    FROM Sales.SalesOrderDetail
    WHERE CHOOSE(@Column, SalesOrderID, ProductID) = @Value
    GO

    DECLARE @Column INT = 2,
    @Value INT = 870;

    SELECT CarrierTrackingNumber
    , OrderQty
    , UnitPrice
    , LineTotal
    FROM Sales.SalesOrderDetail
    WHERE CHOOSE(@Column, SalesOrderID, ProductID) = @Value
    GO

    The choose feature has been within SQL Server since 2012. Unfortunately, BOL does not reflect this. Just to double check, I went to my last 2012 instance and executed a query with the choose function and it succeeded.

    I first saw this function used this way in a training course on Plural Sight by Joe Sack. The courses where SQL Server: Common Query Tuning Problems and Solutions – Part 1 and Part 2. It is worth watching if you have not seen it.

    • ScaryDBA

      I’ll do a quick edit and add a note. I tried to research when it was introduced and yeah, BOL, let me down there. Ah well.

  • Joe Celko

    The ANSI/ISO standards have given us the CASE, expression. Why would anyone who wants portable readable code used a highly proprietary thing stolen from a non-SQL language? . I’m assuming that most people know or should know, there are two forms of CASE expression, and the choose is hillbilly dialect for the computed version.

    CASE
    WHEN THEN
    WHEN THEN
    ..
    ELSE END;

    • Jeff Moden

      BWAAA-HAAAA!!! You know the answer to that, Joe. Unless you’re willing to settle on some seriously reduced abilities in code, it’s nearly impossible to write truly portable code… so you might as well use the “power tools” of whatever environment you’re in.

  • Jeff Moden

    Nice article, Grant. I didn’t know this about CHOOSE and certainly I wouldn’t have believed that it would be SARGable in any way, shape, or form. Looks like I’ve got some testing to do!

Please let me know what you think about this article or any questions:

This site uses Akismet to reduce spam. Learn how your comment data is processed.