{"id":97983,"date":"2023-09-18T20:53:28","date_gmt":"2023-09-18T20:53:28","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=97983"},"modified":"2023-10-09T13:28:19","modified_gmt":"2023-10-09T13:28:19","slug":"alternatives-to-sql-2022-built-in-function-greatest","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/alternatives-to-sql-2022-built-in-function-greatest\/","title":{"rendered":"Alternatives To SQL Server 2022 Built-in function GREATEST"},"content":{"rendered":"<p>If you haven&#8217;t already heard, SQL Server 2022\u00a0 introduced a new built-in system function called <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/functions\/logical-functions-greatest-transact-sql?view=azuresqldb-current&amp;viewFallbackFrom=sql-server-ver16\">GREATEST<\/a>. Simply put, it is to a set of columns, variables, expressions etc. what the <code>MAX<\/code> function is to a set of values (i.e., rows) of a single column or expression. The opposite of <code>GREATEST<\/code> function is <code>LEAST<\/code> function which returns the smallest value from the supplied list of columns, variables, constants etc. <code>GREATEST<\/code> &amp; <code>LEAST<\/code> can be considered a pair, just as <code>MIN<\/code>\/<code>MAX<\/code> functions are.<\/p>\n<p>So, on a SQL Server 2022 instance, you can simply call <code>GREATEST<\/code> function with list of columns, variables, or constant values to get the highest value out of all of them. Here is an example with list of constants:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SELECT GREATEST(500, 10, 3, 50, 200, 8) [Greatest],\r\n       GREATEST(10, 500, 3, 50, 200, 8) [Greatest2],\r\n       GREATEST(200, 10, 3, 50, 500, 8) [Greatest3]<\/pre>\n<p>This will return:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"264\" height=\"67\" class=\"wp-image-97984\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/08\/a-screenshot-of-a-computer-description-automatica-35.png\" alt=\"A screenshot of a computer\n\nDescription automatically generated\" \/><\/p>\n<p>In the same manner,<\/p>\n<p>I can also pass a list of columns to it:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">with cte as\r\n(\r\nselect \r\n      DB_NAME(database_id) db_name,\r\n      max(last_user_seek) last_user_seek,\r\n      max(last_user_scan) last_user_scan,\r\n      max(last_user_lookup) last_user_lookup,\r\n      max(last_user_update) last_user_update\r\nfrom sys.dm_db_index_usage_stats\r\ngroup by DB_NAME(database_id)\r\n)\r\nselect \r\n      *,\r\n      GREATEST(last_user_seek, last_user_scan, \r\n      last_user_lookup,last_user_update) last_access_time_cte\r\nfrom cte;<\/pre>\n<p>This returns the following:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"911\" height=\"88\" class=\"wp-image-97985\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/08\/word-image-97983-2.png\" \/><\/p>\n<p>And while it is not as easy to read, note that you can pass in expressions as well, as you can get the same result by pushing the <code>GREATEST<\/code> function up into the primary query too:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">select DB_NAME(database_id) db_name, \r\n       max(last_user_seek) last_user_seek, \r\n       max(last_user_scan) last_user_scan, \r\n       max(last_user_lookup) last_user_lookup, \r\n       max(last_user_update) last_user_update, \r\n       GREATEST(max(last_user_seek), \r\n                max(last_user_scan), \r\n                max(last_user_lookup), \r\n                max(last_user_update)) AS last_access_time_cte \r\nfrom sys.dm_db_index_usage_stats \r\ngroup by DB_NAME(database_id);<\/pre>\n<p>Isn&#8217;t that a nifty feature?<\/p>\n<p>This rest of this article, focuses on how to achieve the same functionality in pre-SQL Server 2022 versions of SQL Server. Same approaches can be used to mimic the functionality of <code>LEAST<\/code> function as well.<\/p>\n<p>I recently had to remind a client of mine that we are past the halfway mark of year 2023 that they still didn&#8217;t have a single SQL Server 2022 server, even though their SQL Server licenses covered it. I would not be surprised if there are many organizations that may still have a sizable number of SQL Servers pre-SQL 2022.<\/p>\n<p>I personally though, am looking forward to when the next release of SQL Server will come out, whether it would be in 2024, 2025, or 2026? Hopefully Microsoft doesn&#8217;t make us wait beyond the year 2026.<\/p>\n<p>In this article I am going to demonstrate 2 viable alternative solutions to find the largest value of a group of values in your queries (not including writing a very complicated <code>CASE<\/code> expression!)<\/p>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/queries\/table-value-constructor-transact-sql?view=sql-server-ver16\" target=\"_blank\" rel=\"noopener\">Table Value Constructor<\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/queries\/from-using-pivot-and-unpivot?view=sql-server-ver15\" target=\"_blank\" rel=\"noopener\">PIVOT and UNPIVOT<\/a><\/li>\n<\/ul>\n<h2>Table Value Constructor<\/h2>\n<p>To start with, I will discuss using a Table Value Constructor (TVC) as I think it is easier to write and understand. TVC is very similar to the <code>VALUES<\/code> clause that you have used with the <code>INSERT INTO<\/code> statements to add new data into a table. You are maybe already aware that you can insert multiple rows in a single <code>INSERT INTO<\/code> statement:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">declare @customer table  (id int identity, name varchar(100));\r\ninsert into @customer(name)\r\nvalues\r\n       ('tom'),\r\n       ('jerry'),\r\n       ('brian');\r\n select * from @customer;<\/pre>\n<p>This returns:<\/p>\n<p><code>id\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 name<br \/>\n----------- ----------<\/code><code><\/code><code><br \/>\n1\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0tom<br \/>\n2\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0jerry<\/code><code><br \/>\n3\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0brian<\/code><\/p>\n<p>Similarly, you can use a <code>VALUES<\/code> clause to construct a derived table. Here is an example of using it with constants:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SELECT [Greatest] = \r\n(\r\n    SELECT MAX([Greatest])  FROM\r\n          (VALUES (500),(1000),(3),\r\n                   (50), (200),(8))\r\n             AS derived_table([Greatest])\r\n);<\/pre>\n<p>This will return the greatest of values in that derived table:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"646\" height=\"193\" class=\"wp-image-97986\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/08\/a-screenshot-of-a-computer-description-automatica-36.png\" alt=\"A screenshot of a computer\n\nDescription automatically generated\" \/><\/p>\n<p>Similarly, you can use list of columns, variables, expressions etc.\u00a0<\/p>\n<p>So, using the <code>VALUES<\/code> construct here, I will construct a derived table to convert values from multiple columns into values\/rows for a single derived column, <code>last_access_time<\/code>, sort its results in descending order (from highest to lowest) then select the first row\u2019s value to get the highest (i.e. <code>GREATEST<\/code>) value.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">with cte as\r\n(\r\nselect\r\n       DB_NAME(database_id) db_name,\r\n       max(last_user_seek) last_user_seek,\r\n       max(last_user_scan) last_user_scan,\r\n       max(last_user_lookup) last_user_lookup,\r\n       max(last_user_update) last_user_update\r\nfrom sys.dm_db_index_usage_stats\r\ngroup by DB_NAME(database_id)\r\n)\r\nselect\r\n             *,\r\n             last_access_time_derived = \r\n                    (select top 1 last_access_time from\r\n                    (values\r\n                           (last_user_seek),\r\n                           (last_user_scan),\r\n                           (last_user_lookup),\r\n                           (last_user_update))\r\n                           derived_table(last_access_time) \r\n                     order by last_access_time desc\r\n                    )\r\nfrom cte;<\/pre>\n<p>This will return something like the following (depending on your actual server&#8217;s utilization):<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"905\" height=\"71\" class=\"wp-image-97987\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/08\/word-image-97983-4.png\" \/><\/p>\n<p>Note: depending on the kind of server you are using this on, it may have <code>NULL<\/code> values for some of the values. For example, on the editor&#8217;s computer, it returned:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"697\" height=\"39\" class=\"wp-image-97988\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/08\/word-image-97983-5.png\" \/><\/p>\n<p>So, just like <code>MAX,<\/code> it does not return <code>NULL<\/code> unless all scalar expressions are <code>NULL<\/code>. This method is by no means as easy to work with as the <code>GREATEST<\/code> function, but it is generally straightforward to implement if needed.<\/p>\n<p>It looks complicated without the built-in <code>GREATEST<\/code> function, doesn&#8217;t it? Well, I think the next method, <code>UNPIVOT<\/code>, I will show you is even more complicated. That&#8217;s just my opinion, the opinions of others in general may vary.<\/p>\n<h2>Unpivot<\/h2>\n<p>The idea with using an unpivot is to take values from multiple columns and turns them into rows for a single column. In other words, it can be used to convert a set of columns into a row. If you are familiar with the <code>TRANSPOSE<\/code> function in excel, unpivot is very similar. It\u2019s the opposite of the <code>PIVOT<\/code> statement, which takes values for a single column from multiple rows and turns each value into multiple columns.<\/p>\n<p>Here is an example of how you can use <code>UNPIVOT<\/code> to convert columns into rows (note that if you have not executed any queries in the other databases since a reboot, no data may be returned by this query):<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">\/*\r\nThe DMV sys.dm_db_index_usage_stats returns the operational stats on \r\neach index in every database that have had any read and\/or write \r\nactivity since the last restart of the sql instance. It returns \r\none row for each index. However, by use of the UNPIVOT clause, I\r\nam converting it's four columns with \"last_*\" names, into rows so\r\ninstead of a single row for each index, it returns multiple rows, \r\none row for each column that has a not-null value. \r\nThe where clause filters out:\r\n1. System databases i.e. where database_id &gt; 4\r\n2. Non-clustered indexes i.e where index_id &lt;= 1\r\n   Essentially, when index_id is 0 the table is a heap i.e. \r\n                                   it has no clustered index\r\n                when index_id is 1 the table has a clustered \r\n                                       index and is not a heap\r\n*\/\r\nSELECT TOP 10\r\n\tDB_NAME(database_id) [db_name],\r\n\tOBJECT_NAME(object_id, database_id) [object_name],\r\n\t[column_name],\r\n\t[column_value]\r\nFROM sys.dm_db_index_usage_stats\r\n\tUNPIVOT ([column_value]\r\n                     FOR [column_name] IN \r\n\t\t\t\t(\r\n                                   last_user_seek, \r\n\t\t\t\t   last_user_scan, \r\n\t\t\t\t   last_user_lookup, \r\n\t\t\t\t   last_user_update\r\n\t\t\t\t\t\t)\r\n\t\t) unpv\r\nWHERE index_id &lt;= 1\r\nAND database_id &gt; 4\r\nORDER BY [db_name], [object_name], [column_name];<\/pre>\n<p>The values for the four columns <code>last_user_lookup<\/code>, <code>last_user_scan<\/code>, <code>last_user_seek<\/code> and <code>last_user_update<\/code> in the <code>sys.dm_db_index_usage_stats<\/code> are now showing as rows.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"542\" height=\"117\" class=\"wp-image-97989\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/08\/word-image-97983-6.png\" \/><\/p>\n<p>And I just want to know the highest value from the 4 columns, I can just use the <code>MAX<\/code> function and slightly modify the query to add the <code>GROUP BY<\/code> clause.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SELECT TOP 10\r\n\tDB_NAME(database_id) [db_name],\r\n\tOBJECT_NAME(object_id, database_id) [object_name],\r\n\t[last_accessed] = MAX([column_value])\r\nFROM sys.dm_db_index_usage_stats\r\n\tUNPIVOT ([column_value]\r\n                     FOR [column_name] IN \r\n\t\t\t\t\t\t(\r\n\t\t\t\t\t\t\tlast_user_seek, \r\n\t\t\t\t\t\t\tlast_user_scan, \r\n\t\t\t\t\t\t\tlast_user_lookup, \r\n\t\t\t\t\t\t\tlast_user_update\r\n\t\t\t\t\t\t)\r\n\t\t\t\t\t) unpv\r\nWHERE database_id &gt; 4\r\nGROUP BY database_id, object_id\r\nORDER BY [db_name], [object_name];<\/pre>\n<p>This returns the following on my server.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"419\" height=\"101\" class=\"wp-image-97990\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/08\/word-image-97983-7.png\" \/><\/p>\n<p>To extend the example further, I want to know if\/when the last time a table in the the database was accessed. This query is longer than it may need to be because I also want to display the individual values for the 4 columns as well as the MAX\/highest value among them.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">USE  MSDB;\r\n\r\nwith cte as\r\n(\r\nSELECT\r\n        [last_user_seek]   = MAX(last_user_seek),\r\n        [last_user_scan]   = MAX(last_user_scan),\r\n        [last_user_lookup] = MAX(last_user_lookup),\r\n        [last_user_update] = MAX(last_user_update)\r\nFROM sys.dm_db_index_usage_stats AS i\r\nWHERE i.database_id = DB_ID()\r\n)\r\n SELECT\r\n             [Database] = DB_NAME(),  \r\n             *\r\nFROM cte,\r\n(\r\n       SELECT MAX([last_access_time]) [last_access_time]\r\n       FROM\r\n             (\r\n                 SELECT\r\n                     [last_user_seek]   = MAX(last_user_seek),\r\n                     [last_user_scan]   = MAX(last_user_scan),\r\n                     [last_user_lookup] = MAX(last_user_lookup),\r\n                     [last_user_update] = MAX(last_user_update)\r\n                 FROM cte\r\n             ) p\r\n       UNPIVOT \r\n             ([last_access_time] FOR [Column] IN\r\n                    (\r\n                      last_user_seek,\r\n                      last_user_scan,\r\n                      last_user_lookup,\r\n                      last_user_update)\r\n                    )  AS unpvt\r\n) unpvt;<\/pre>\n<p>This returns:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"610\" height=\"72\" class=\"wp-image-97991\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/08\/word-image-97983-8.png\" \/><\/p>\n<p>As you can see, the using <code>UNPIVOT<\/code> can take more lines of code and can be quite a bit complicated. As you will see later in this article, per the performance test I performed, it is also slower than the Table Value Constructor method.<\/p>\n<p>I have tested these syntaxes in SQL Server versions going back to 2012. <code>PIVOT<\/code> and <code>UNPIVOT<\/code> were first introduced in SQL 2005 so the syntax\u2019s I presented in this article should still work there.<\/p>\n<h2>Performance Test<\/h2>\n<p>How is the query performance between the TVF and Unpivot methods, or with the new greatest function?<\/p>\n<p>To find that out, I am going to generate some random data. The following SQL script creates a table with name <code>random_data_for_testing<\/code> and inserts 10 million rows into it.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SET NOCOUNT ON;\r\nif object_id('random_data_for_testing', 'U') is not null \r\n\tdrop table random_data_for_testing;\r\nGO\r\ncreate table random_data_for_testing\r\n(\r\n\tid int identity primary key,\r\n\tvalue_01 float default substring(cast(rand() \r\n                              as varchar(20)), 3, 15),\r\n\tvalue_02 float default substring(cast(rand() \r\n                              as varchar(20)), 3, 15),\r\n\tvalue_03 float default substring(cast(rand() \r\n                              as varchar(20)), 3, 15),\r\n\tvalue_04 float default substring(cast(rand() \r\n                              as varchar(20)), 3, 15),\r\n\tvalue_05 float default substring(cast(rand() \r\n                              as varchar(20)), 3, 15),\r\n\tvalue_06 float default substring(cast(rand() \r\n                              as varchar(20)), 3, 15),\r\n\tvalue_07 float default substring(cast(rand() \r\n                              as varchar(20)), 3, 15),\r\n\tvalue_08 float default substring(cast(rand() \r\n                              as varchar(20)), 3, 15),\r\n\tvalue_09 float default substring(cast(rand() \r\n                              as varchar(20)), 3, 15),\r\n\tvalue_10 float default substring(cast(rand() \r\n                              as varchar(20)), 3, 15)\r\n);\r\nGO\r\ndeclare @commit_size int = 100000; \r\ndeclare @max_rows int = 10000000; \r\ndeclare @counter int = 1;\r\ndeclare @commit_counter int = 0;\r\ndeclare @max_commit_count int = @max_rows \/ @commit_size; \r\nwhile @counter &lt;= @max_rows\r\nbegin\r\n\tif @@TRANCOUNT = 0 BEGIN TRAN\r\n\t\r\n\tinsert into random_data_for_testing default values\r\n\tset @counter = @counter + 1\r\n\tIF @@TRANCOUNT &gt; 0 and @counter % @commit_size = 0\r\n\tBEGIN\r\n\t\tset @commit_counter = @commit_counter + 1\r\n\t\traiserror('Committing %i of %i transactions....', 10, \r\n             1, @commit_counter, @max_commit_count) with nowait\r\n\t\tcommit;\r\n\tEND\r\nend\r\nif @@TRANCOUNT &gt; 0 COMMIT;<\/pre>\n<p>My first test query is to do a test to get the highest value among the 10 columns (named value_01 to value_10 in the test table), using all 3 methods. I ran the script multiple times to make sure there are no physical reads or read-ahead reads with any of the queries so we can compare just the duration, logical IO and CPU as accurately as possible.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SET NOCOUNT ON;\r\nSET ANSI_WARNINGS OFF;\r\nGO\r\nPRINT '---------------------------------------------'\r\nPRINT '********* Test using the function Greatest...'\r\nPRINT '---------------------------------------------'\r\nSET STATISTICS IO ON;\r\nSET STATISTICS TIME ON;\r\nGO\r\nSELECT MAX(GREATEST((value_01),(value_02),(value_03),\r\n                     (value_04),(value_05),(value_06), \r\n                     (value_07),(value_09),(value_10))) \r\n                                AS using_greatest_function\r\nFROM random_data_for_testing;\r\n\r\nSET STATISTICS IO OFF;\r\nSET STATISTICS TIME OFF;\r\nGO\r\nPRINT '---------------------------------------------'\r\nPRINT '********* Test using the TVC method..........'\r\nPRINT '---------------------------------------------'\r\nSET STATISTICS IO ON;\r\nSET STATISTICS TIME ON;\r\nGO\r\nSELECT MAX(greatest_value) using_tvf FROM\r\n(\r\n\tSELECT greatest_value = \r\n\t\t(SELECT MAX(greatest_value) \r\n                 FROM (VALUES(value_01),(value_02),(value_03),\r\n\t\t\t     (value_04),(value_05),(value_06),\r\n\t\t\t     (value_07),(value_09),(value_10)\r\n\t\t\t\t\t) derived_table(greatest_value)\r\n\t\t)\r\n\t\t\tFROM random_data_for_testing\r\n) a;\r\n\r\nSET STATISTICS IO OFF;\r\nSET STATISTICS TIME OFF;\r\nGO\r\nPRINT '---------------------------------------------'\r\nPRINT '********* Test using the Unpivot...'\r\nPRINT '---------------------------------------------'\r\nSET STATISTICS IO ON;\r\nSET STATISTICS TIME ON;\r\nGO\r\nSELECT MAX(greatest_value) using_unpivot FROM random_data_for_testing\r\nUNPIVOT  ([greatest_value] FOR [Column] IN (value_01, value_02, \r\n                                            value_03 , value_04, \r\n                                            value_05,value_06, \r\n                                            value_07,value_09,\r\n                                                 value_10)) AS unpvt\r\n\r\nSET STATISTICS IO OFF;\r\nSET STATISTICS TIME OFF;\r\nGO<\/pre>\n<p>Results:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"211\" height=\"218\" class=\"wp-image-97992\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/08\/word-image-97983-9.png\" \/><\/p>\n<p>Here are the key stats from the <code>STATISTICS IO<\/code> and <code>STATISTICS TIME<\/code>:<\/p>\n<p><code><strong>********* Test using the function Greatest...<br \/>\n<\/strong><span style=\"background-color: #f2f4f5; color: #222222;\">Table 'random_data_for_testing'. Scan count 5, logical reads 118295 <br \/>\n<\/span><\/code><code><span style=\"background-color: #f2f4f5; color: #222222;\">SQL Server Execution Times: <br \/>\n<\/span><\/code><code><span style=\"background-color: #f2f4f5; color: #222222;\">CPU time = 4405 ms<\/span><span style=\"background-color: #f2f4f5; color: #222222;\">, <\/span><span style=\"background-color: #f2f4f5; color: #222222;\">elapsed time = 1381<\/span><span style=\"background-color: #f2f4f5; color: #222222;\"> ms. <\/span><\/code><\/p>\n<p><code><span style=\"background-color: #f2f4f5; color: #222222;\">********* Test using the TVC method..........<\/span><span style=\"background-color: #f2f4f5; color: #222222;\"> <br \/>\n<\/span><\/code><code><span style=\"background-color: #f2f4f5; color: #222222;\">Table 'random_data_for_testing'. Scan count 5, logical reads 117343 <br \/>\n<\/span><\/code><code><span style=\"background-color: #f2f4f5; color: #222222;\">SQL Server Execution Times: <br \/>\n<\/span><\/code><code><span style=\"background-color: #f2f4f5; color: #222222;\">CPU time = 12000 ms<\/span><span style=\"background-color: #f2f4f5; color: #222222;\">, <\/span><span style=\"background-color: #f2f4f5; color: #222222;\">elapsed time = 3332<\/span><span style=\"background-color: #f2f4f5; color: #222222;\"> ms.<\/span><\/code><\/p>\n<p><code> <\/code><code><span style=\"background-color: #f2f4f5; color: #222222;\">********* Test using the Unpivot...<\/span><span style=\"background-color: #f2f4f5; color: #222222;\"> <br \/>\n<\/span><\/code><code><span style=\"background-color: #f2f4f5; color: #222222;\">Table 'random_data_for_testing'<\/span><\/code><code><span style=\"background-color: #f2f4f5; color: #222222;\">Scan count 5, logical reads 117179 <br \/>\n<\/span><\/code><code><span style=\"background-color: #f2f4f5; color: #222222;\">SQL Server Execution Times: <br \/>\n<\/span><\/code><code><span style=\"background-color: #f2f4f5; color: #222222;\">CPU time = 17219 ms<\/span><span style=\"background-color: #f2f4f5; color: #222222;\">, <\/span><span style=\"background-color: #f2f4f5; color: #222222;\">elapsed time = 4813<\/span><span style=\"background-color: #f2f4f5; color: #222222;\"> ms.<\/span><\/code><\/p>\n<p>The amount of IO (logical reads) is almost identical among the three methods. But notice the difference in the CPU time and Elapsed time values for each. The elapsed time for the function <code>GREATEST<\/code> is 1381 ms, for the TVC it is 3332 ms and for <code>UNPIVOT<\/code> it is 4813 ms.<\/p>\n<p>Below is the screenshot of the actual execution plan and as you can see the query cost as relative to the batch, cost of function <code>GREATEST<\/code> is 12%, TVC is 34% and the <code>UNPIVOT<\/code> is 54%.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"957\" height=\"619\" class=\"wp-image-97993\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/08\/word-image-97983-10.png\" \/><\/p>\n<p>As you can see from the thickness of the pipe going from right to left, when a query needs to process a relatively great deal of rows, using the build in function gives the best performance. That shouldn\u2019t be a surprise. If that wasn\u2019t the case, somebody at Microsoft made a big booboo! However, between the TVF and Unpivot, TVF is faster.<\/p>\n<p><em>Note: This by no means a scientific test. Generally, though I have seen TVF to be faster than Unpivot.<\/em><\/p>\n<p>What if the query is using a very selective filter condition on a clustered primary key on a single, integer column? This kind of queries are usually ideal queries in \u201calmost\u201d any conditions, even if it includes columns with LOB data types like <code>XML<\/code>, <code>VARBINARY(MAX)<\/code> etc. <br \/>\nJust to be sure, let\u2019s look at the following example:<\/p>\n<pre class=\"\">SET NOCOUNT ON; \r\nSET ANSI_WARNINGS OFF; \r\nGO \r\n\r\ndeclare @id int = 100100; \r\nPRINT '---------------------------------------------'; \r\nPRINT '********* Test using the function Greatest...'; \r\nPRINT '---------------------------------------------';\r\nSET STATISTICS IO ON; \r\nSET STATISTICS TIME ON;\r\n\r\nSELECT *,\r\n \u00a0 \u00a0 \u00a0 \u00a0using_greatest_function = \r\n  \u00a0\u00a0 \u00a0\u00a0 \u00a0\u00a0 \u00a0\u00a0 \u00a0\u00a0 \u00a0\u00a0 \u00a0\u00a0 \u00a0\u00a0 GREATEST(value_01,value_02,value_03,value_04, \r\n                                   value_05,value_06,value_07,value_08,\r\n                                   value_09, value_10 \t\t\t\t\t\t\t) \r\nFROM random_data_for_testing \r\nWHERE id = @id;\r\n\r\nSET STATISTICS IO OFF; \r\nSET STATISTICS TIME OFF; \r\nPRINT '---------------------------------------------' \r\nPRINT '********* Test using the TVC method..........' \r\nPRINT '---------------------------------------------'\r\n\r\nSET STATISTICS IO ON; \r\nSET STATISTICS TIME ON;\u00a0 \r\n\r\nSELECT\t*, using_tvf = ( \r\n \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0SELECT MAX(greatest_value) \r\n                FROM  (VALUES (value_01),(value_02),(value_03), \r\n      \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 (value_04),(value_05),(value_06),\u00a0 \r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0       (value_07),(value_08),(value_09),                           \r\n                              (value_10)) AS derived_table(greatest_value) \t\t\t ) \r\nFROM random_data_for_testing \r\nWHERE id = @id;\r\n\r\nSET STATISTICS IO OFF; \r\nSET STATISTICS TIME OFF; \r\n\r\nPRINT '---------------------------------------------'; \r\nPRINT '********* Test using the Unpivot...'; \r\nPRINT '---------------------------------------------';\r\nSET STATISTICS IO ON; \r\nSET STATISTICS TIME ON; \r\n\r\nSELECT t.*, p.using_unpivot \r\nFROM random_data_for_testing t\r\n \u00a0 \u00a0 \u00a0 \u00a0INNER JOIN  ( SELECT id, MAX(greatest_value) using_unpivot \r\n  \u00a0\u00a0 \u00a0\u00a0 \u00a0\u00a0 \u00a0\u00a0 \u00a0\u00a0 \u00a0\u00a0 \u00a0 FROM random_data_for_testing \r\n\u00a0 \u00a0\u00a0 \u00a0\u00a0 \u00a0\u00a0 \u00a0\u00a0 \u00a0\u00a0 \u00a0\u00a0 \u00a0 UNPIVOT  ([greatest_value] FOR [Column] IN  \r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0(value_01,value_02,value_03, value_04, \r\n \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0         value_05, value_06, value_07, value_08,\r\n                                   value_09, value_10)) AS unpvt \r\n                       WHERE id = @id group by id ) p \r\n \u00a0\u00a0 \u00a0\u00a0 \u00a0\u00a0 \u00a0\u00a0 \u00a0\u00a0on p.id = t.id;\r\n\r\nSET STATISTICS IO OFF; \r\nSET STATISTICS TIME OFF;<\/pre>\n<p>This returns three identical result sets:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"919\" height=\"293\" class=\"wp-image-97994\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/08\/word-image-97983-11.png\" \/><\/p>\n<p>And quite similar execution times as well:<\/p>\n<p><code><strong>********* Test using the function Greatest...<\/strong> <br \/>\n<\/code><code>Table 'random_data_for_testing'. Scan count 0, logical reads 3 <br \/>\nSQL Server Execution Times: <br \/>\nCPU time = 0 ms, elapsed time = 0 ms. <\/code><\/p>\n<p><code><strong>********* Test using the TVC method..........<\/strong> <br \/>\nTable 'random_data_for_testing'. Scan count 0, logical reads 3 <br \/>\nSQL Server Execution Times: <br \/>\nCPU time = 0 ms, elapsed time = 0 ms. <\/code><\/p>\n<p><code><strong>********* Test using the Unpivot...<\/strong> <br \/>\nTable 'random_data_for_testing'. Scan count 0, logical reads 6 <br \/>\nSQL Server Execution Times: <br \/>\nCPU time = 0 ms, elapsed time = 5 ms.<\/code><\/p>\n<p>With the following actual execution plans:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"836\" height=\"723\" class=\"wp-image-97995\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/08\/word-image-97983-12.png\" \/><\/p>\n<p>The entire script finishes in sub-sub-second. Still, at a microsecond (a millionth of a second) level, you can see that the Unpivot performs the worst.<\/p>\n<h2>Summary<\/h2>\n<p>In this article, I have demonstrated the new <code>GREATEST<\/code> and <code>LEAST<\/code> value functions in SQL Server 2022 and have show that in general testing, these functions are very fast. However, as not everyone is using SQL Server 2022, I also demonstrated a method using Table Value Constructors that give you similar functionality (with much less straightforward code), as well as a method using the often disregarded <code>UNPIVOT<\/code> function.<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>If you haven&#8217;t already heard, SQL Server 2022\u00a0 introduced a new built-in system function called GREATEST. Simply put, it is to a set of columns, variables, expressions etc. what the MAX function is to a set of values (i.e., rows) of a single column or expression. The opposite of GREATEST function is LEAST function which&#8230;&hellip;<\/p>\n","protected":false},"author":342465,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[53,143531],"tags":[],"coauthors":[159014],"class_list":["post-97983","post","type-post","status-publish","format-standard","hentry","category-featured","category-t-sql-programming-sql-server"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/97983","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/342465"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=97983"}],"version-history":[{"count":18,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/97983\/revisions"}],"predecessor-version":[{"id":97997,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/97983\/revisions\/97997"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=97983"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=97983"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=97983"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=97983"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}