{"id":83311,"date":"2019-02-26T15:30:57","date_gmt":"2019-02-26T15:30:57","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=83311"},"modified":"2026-04-14T12:47:58","modified_gmt":"2026-04-14T12:47:58","slug":"what-are-inline-indexes","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/what-are-inline-indexes\/","title":{"rendered":"SQL Server Inline Indexes: Add Indexes to Table Variables and UDTTs"},"content":{"rendered":"<p><strong>Inline indexes in SQL Server allow you to define clustered, nonclustered, and filtered indexes directly within CREATE TABLE syntax &#8211; most usefully for table variables, user-defined table types (UDTTs), and multi-statement table-valued functions. Before inline indexes were introduced, table variables could not be indexed beyond the primary key, which made them perform poorly for large intermediate result sets. Inline index syntax solves this without requiring a separate CREATE INDEX statement, and supports the same index options available for regular tables including filtered indexes and composite keys. Requires SQL Server 2014 or later.<\/strong><\/p>\n<p>Usually, the added features of the CREATE TABLE syntax in new releases of SQL Server are esoteric, and unless you are dealing with memory-optimized tables or other esoteric stuff, they aren\u2019t of great interest. However, the Inline INDEX for both a table and column index has just crept in quietly with SQL Server 2014 (12.x). This was interesting because the SQL Server team back-fitted it to all tables rather than just in-memory OLTP tables for which it was, at the time, found necessary. The new syntax was introduced which allows you to create certain index types inline with the table definition. These could be at column level, concerning just that column, or at the table level, with indexes containing several columns.<\/p>\n<p>Why interesting? This affects multi-statement table functions, user-defined table types, table-valued parameters as well as table variables. It was considered a game-change for table variables because, for a start, it allowed non-unique indexes or explicit clustered indexes to be declared on columns for the first time because you can create indexes on\u00a0table\u00a0variables as part of the table definition. Of more significance were the table-level indexes that allowed you to specify multi-column indexes. Previous releases had allowed multi-column primary or unique constraints, but not explicitly named indexes. You still cannot declare an index after the table is created, which is a shame as there are good reasons for being able to do so after a table is stocked with data. Any sort of large import of data into a table that is over-indexed or prematurely-indexed is doomed to crawl rather than to run. I\u2019ll show this later on in this article.<\/p>\n<p>The SQL Server 2014 improvements introduced named indexes for table variables for the first time. I haven\u2019t yet worked out a practical use for explicitly naming such indexes in such ephemeral objects.<\/p>\n<p>The new indexes apply to table variables, multi-statement table-valued functions, user-defined table types and table-valued parameters<\/p>\n<p>The bad news is that no distribution statistics are kept on table variables, so the indexes you create don\u2019t have them either. In some cases, even when using this new syntax, performance isn\u2019t always as effective as using temporary tables, which provide statistics. The most satisfactory workaround is to is to add OPTION (RECOMPILE) to the query referencing the table variable, forcing the optimizer to take the cardinality of the table variable into account after it has been populated. SQL Server 2019 and SQL Azure introduces \u2018<a href=\"https:\/\/blogs.msdn.microsoft.com\/sqlserverstorageengine\/2018\/07\/16\/public-preview-of-table-variable-deferred-compilation-in-azure-sql-database\/\">Table variable deferred compilation\u2019<\/a> that obviates the need for the use of the OPTION (RECOMPILE) by using cardinality estimates that are based on\u00a0actual\u00a0table variable row counts in order to produce better query plans when compiling queries.<\/p>\n<h2>The Syntax<\/h2>\n<p>Here are the two syntax diagrams for inline indexes. I\u2019ve corrected them slightly from the MSDN original.<\/p>\n<p>For table variables, multi-statement table functions, and UDTTs:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">&lt;column_index&gt; ::=  \n  INDEX index_name  \n{  [ NONCLUSTERED ] | [ CLUSTERED ]  | [ UNIQUE]  }  <\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"458\" height=\"212\" class=\"wp-image-83312\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/02\/word-image-14.png\" \/><\/p>\n<p>For tables:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk \">&lt;column_index&gt; ::=  \n  INDEX index_name  \n{ [ NONCLUSTERED ] | [ CLUSTERED ]  | \n   [ UNIQUE ] [HASH WITH (BUCKET_COUNT = bucket_count)]  }  <\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"911\" height=\"235\" class=\"wp-image-83313\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/02\/word-image-15.png\" \/><\/p>\n<p>Note: Hash indexes are permitted only in memory optimized tables. You can\u2019t specify UNIQUE as well as (NON)CLUSTERED.<\/p>\n<p>For table variables and their variants, we have the new table index. In fact, the UNIQUE attribute is also allowed on the index, but I\u2019ve kept to the published version because although the UNIQUE keyword is accepted, I\u2019m not certain whether it is implemented.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">&lt;table_index&gt; ::=  \n  INDEX index_name  \n{ [ NONCLUSTERED | CLUSTERED ]  (column [ ASC | DESC ] [ ,... n ] )   \n  \n}<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"667\" height=\"179\" class=\"wp-image-83314\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/02\/word-image-16.png\" \/><\/p>\n<p>And finally, the new table index for tables is rather more complex due to its specialized uses!<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk \">&lt;table_index&gt; ::=  \n  INDEX index_name  \n{   [ NONCLUSTERED ] | [ CLUSTERED ]  \n    HASH (column [ ,... n ] ) WITH (BUCKET_COUNT = bucket_count)   \n  | [ NONCLUSTERED ] | [ CLUSTERED ]  \n    (column [ ASC | DESC ] [ ,... n ] )   \n      [ ON filegroup_name | default ]  \n  | CLUSTERED COLUMNSTORE [WITH ( COMPRESSION_DELAY = {0 \n      | delay [Minutes]})]  \n      [ ON filegroup_name | default ]  \n  \n}  <\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1169\" height=\"513\" class=\"wp-image-83315\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/02\/word-image-17.png\" \/><\/p>\n<p>Even if you entirely eschew the useful table variables, multi-statement table functions, UDDTs and TVPs, this is going to save you quite a lot of typing for things like temporary tables.<\/p>\n<p>So, let\u2019s stand back and look at the syntax for table variables (and with a slight difference, UDDTs).<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-83316\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/02\/word-image-18.png\" width=\"435\" height=\"387\" \/><\/p>\n<p>The table type declaration is similar. Instead of <code>Declare @table_name TABLE (<\/code>, it is <code>CREATE TYPE MyType AS TABLE (<\/code>.<\/p>\n<p>From this distance, it all looks simple. Perhaps it is time to look closer.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1096\" height=\"1864\" class=\"wp-image-83317\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/02\/word-image-19.png\" \/><\/p>\n<p>If you would like a better copy of this to print out, here is a <a href=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/02\/TableVariableOverview.pdf\">PDF<\/a>.<\/p>\n<h2>Syntax Examples<\/h2>\n<h3>A table variable<\/h3>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">DECLARE @Test TABLE \/*showing various indexes*\/\n  (\n  Firstly INT NOT NULL DEFAULT (0) INDEX firstIndex CLUSTERED, \n  \/*clustered index on the column*\/\n  Secondly INT NOT NULL INDEX SecondIndex UNIQUE NONCLUSTERED,\n  \/*unique non-clustered index on the column*\/\n  INDEX ThirdIndex UNIQUE NONCLUSTERED (Firstly, Secondly)\n  \/*an example composite index*\/\n  );<\/pre>\n<h3>A multi-statement table function with a clustered index<\/h3>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">CREATE OR ALTER FUNCTION dbo.ConvertListToTable\n\/*\nSELECT * FROM dbo.ConvertListToTable( \n    '1,2,3,1.435,\"firstly\",\"secondly\",\"thirdly\"') *\/\n( @TheList NVARCHAR(MAX) )\nRETURNS @returntable TABLE \n(\n\tTheOrder int,\n\tTheItem nvarchar(100) INDEX ItemIndex CLUSTERED \n)\nAS\nBEGIN\n  DECLARE @JsonString NVARCHAR(MAX) ='['+@TheList+']'\n  INSERT INTO @ReturnTable(TheOrder,TheItem)\n    SELECT [key]+1, value FROM OpenJson(@JsonString)    \nRETURN \nEND<\/pre>\n<h3>Filtered indexes on table variable<\/h3>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">DECLARE @MyTable TABLE\n(\nMyValue INT NULL INDEX MyValueIndex UNIQUE WHERE MyValue IS NOT NULL \n)<\/pre>\n<h3>Clustered composite index on a table type<\/h3>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk \">CREATE TYPE dbo.MyTableType AS TABLE \n\/*user defined table type showing clustered  composite index*\/\n(\n    TheSequence INT, TheItem nvarchar(100), \n    INDEX test CLUSTERED (TheItem, TheSequence)\n)<\/pre>\n<h2>Testing Out the Indexes<\/h2>\n<p>We\u2019ll demonstrate the effect of an index on a table variable. We will create two table variables, one with an index and one without. We\u2019ll then see how long each took to import four million rows of data. For the data to load, run this script to create the table. I suggest using <a href=\"https:\/\/www.red-gate.com\/products\/sql-development\/sql-data-generator\/\">Redgate\u2019s SQL Data Generator<\/a> to populate it with four millions rows, but you can use any method you wish. You can also use <a href=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/02\/SmallFakedDirectory.zip\">this script<\/a> to create a smaller table.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">DROP TABLE IF EXISTS dbo.Directory;\nCREATE TABLE dbo.Directory (\n\tID INT NOT NULL IDENTITY,\n\tName NVARCHAR(100) NOT NULL,\n\tPostCode NVARCHAR(15) NOT NULL); \n--Populate with 4 million rows to \n--follow along using SQL Data Generator<\/pre>\n<p>Once you have the Directory table in place, run this code to see how long it takes to populate the table variable with and without indexes:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">DECLARE @log TABLE\n  (\n  TheOrder INT IDENTITY(1, 1),\n  WhatHappened VARCHAR(200),\n  WHENItDid DATETIME2 DEFAULT GetDate()\n  );\n----start of timing\nINSERT INTO @log (WhatHappened) SELECT 'Starting insert into indexed TV';\n--First we create a new table variable with column indexes in it.\nDECLARE @OrganisationName TABLE\n  (\n  id INT IDENTITY PRIMARY KEY,\n  Business_id INT NOT NULL UNIQUE NONCLUSTERED,\n  OrganisationName NVARCHAR(100) NOT NULL INDEX NameIndex NONCLUSTERED,\n  PostCode NVARCHAR(15) NOT NULL INDEX IndexPostcode NONCLUSTERED\n  );\nINSERT INTO @OrganisationName (Business_id, OrganisationName, PostCode)\n  SELECT id, Name, Postcode FROM Directory;\nINSERT INTO @log (WhatHappened) SELECT 'insert into indexed TV took'; --\n--Now we create a second table variable without any column indexes in it.\nDECLARE @OrganisationNameNoIndex TABLE\n  (\n  id INT IDENTITY PRIMARY KEY,\n  Business_id INT NOT NULL,\n  OrganisationName NVARCHAR(100) NOT NULL,\n  PostCode NVARCHAR(15) NOT NULL\n  );\nINSERT INTO @OrganisationNameNoIndex \n     (Business_id, OrganisationName, PostCode)\n  SELECT id, Name, Postcode FROM Directory;\nINSERT INTO @log (WhatHappened) \nSELECT 'insert into non-indexed TV took';\nSELECT ending.WhatHappened,\n  DateDiff(ms, starting.WHENItDid, ending.WHENItDid) AS ms\n  FROM @log AS starting\n    INNER JOIN @log AS ending\n      ON ending.TheOrder = starting.TheOrder + 1\nUNION ALL\nSELECT 'Total', DateDiff(ms, Min(WHENItDid), Max(WHENItDid)) FROM @log;\n--list out all the timings<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"277\" height=\"94\" class=\"wp-image-83318\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/02\/word-image-20.png\" \/><\/p>\n<p>Is this a problem with the size of the table variable? If we repeat the test with just 10,000 rows, by selecting not the entire table but using the TOP xxx syntax to select just a portion \u2026<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">INSERT INTO @OrganisationName (Business_id, OrganisationName, PostCode)\n  SELECT TOP 10000 id, Name, Postcode FROM BigDirectory;<\/pre>\n<p>\u2026 we get this<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"248\" height=\"96\" class=\"wp-image-83319\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/02\/word-image-21.png\" \/><\/p>\n<p>Yes, the old-fashioned table variable still took less than a tenth of the time to insert those rows! There is definitely a problem in stocking a table with a significant amount of data when you\u2019ve placed all those indexes in it.<\/p>\n<p>Let\u2019s not be so unfair. We\u2019ve not indexed the tables for this test but for the imagined usage of the table variables within the batch. In effect, we\u2019ve over-indexed the table. Also, if you are doing a lot of searching with the table variables, surely, it is going to be so much faster, so maybe the long wait to stock the indexed table might be worthwhile?<\/p>\n<p>We run another test, this time doing a search for organisation names, and just timing that, ignoring the interminable wait to stock the table. I\u2019ve also included an INSERT statement to make sure that the rows we are searching for exist in the Directory table.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">DECLARE @log TABLE\n  (\n  TheOrder INT IDENTITY(1, 1),\n  WhatHappened VARCHAR(200),\n  WHENItDid DATETIME2 DEFAULT GetDate()\n  );\n----start of timing\nDECLARE @OrganisationName TABLE\n  (\n  id INT IDENTITY PRIMARY KEY,\n  OrganisationName NVARCHAR(100) NOT NULL INDEX NameIndex NONCLUSTERED,\n  PostCode NVARCHAR(15) NOT NULL INDEX IndexPostcode NONCLUSTERED\n  );\nINSERT INTO @OrganisationName ( OrganisationName, PostCode)\n  SELECT  Name, Postcode FROM Directory\n  UNION ALL\n(SELECT * FROM   (VALUES('D N Philpott','PL56 1GQ'),\n\t\t('J A Hawkes &amp; Son Ltd','PL56 1GQ'),\n\t\t('Kath''s Kabin','PL56 1GQ'),\n\t('Quakers (Religious Society Of Friends)','PL56 1GQ'),\n\t('Dr M R Dadhania','PL56 1GQ'),\n\t('Sheffield City Council','PL56 1GQ'))\n              f( OrganisationName, PostCode)\n\t)\nDECLARE @OrganisationNameNoIndex TABLE\n  (\n  id INT IDENTITY PRIMARY KEY,\n  OrganisationName NVARCHAR(100) NOT NULL,\n  PostCode NVARCHAR(15) NOT NULL\n  );\nINSERT INTO @OrganisationNameNoIndex (OrganisationName, PostCode)\n  SELECT OrganisationName, Postcode FROM @OrganisationName;\nINSERT INTO @log (WhatHappened) SELECT 'Search for organisation names';\nSELECT OrganisationName, id\n  FROM @OrganisationNameNoIndex\n  WHERE OrganisationName IN\n('D N Philpott', 'J A Hawkes &amp; Son Ltd', 'Kath''s Kabin',\n  'Quakers (Religious Society Of Friends)', 'Dr M R Dadhania',\n  'Sheffield City Council'\n);\nINSERT INTO @log (WhatHappened)\n  SELECT 'Searching for organisation names in unindexed table ' + \n    'variable took ';\nSELECT OrganisationName, id\n  FROM @OrganisationName\n  WHERE OrganisationName IN\n('D N Philpott', 'J A Hawkes &amp; Son Ltd', 'Kath''s Kabin',\n  'Quakers (Religious Society Of Friends)', 'Dr M R Dadhania',\n  'Sheffield City Council'\n);\nINSERT INTO @log (WhatHappened)\n  SELECT 'Searching for organisation names in indexed table ' + \n     'variable took ';\nSELECT ending.WhatHappened,\n  DateDiff(ms, starting.WHENItDid, ending.WHENItDid) AS ms\n  FROM @log AS starting\n    INNER JOIN @log AS ending\n      ON ending.TheOrder = starting.TheOrder + 1\nUNION ALL\nSELECT 'Total', DateDiff(ms, Min(WHENItDid), Max(WHENItDid)) FROM @log;\n--list out all the timings<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"443\" height=\"105\" class=\"wp-image-83320\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/02\/word-image-22.png\" \/><\/p>\n<p>So yes. The unindexed table variable had to do a table scan whereas the indexed table gave the query optimiser the chance to use that \u2018NameIndex\u2018 column index. It was too quick for me to measure it. So, it looks like a tradeoff, but with just a small amount of searching our unindexed table wins hands down.<\/p>\n<p>Were we still being unfair? Hell yes.<\/p>\n<p>That was a simple search. All we actually need is a clustered index for the organisation name. We\u2019ve also added a second index for postcodes that we never used in the test. So, instead of trying to make these two tables equivalent, we optimise them and run the test again. In one case we search using a clustered index, and in the other one we use a non-clustered index.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk \">DECLARE @log TABLE\n  (\n  TheOrder INT IDENTITY(1, 1),\n  WhatHappened VARCHAR(200),\n  WHENItDid DATETIME2 DEFAULT GetDate()\n  );\n----start of timing\nINSERT INTO @log (WhatHappened) SELECT 'Starting insert into indexed TV';\n--First we create a new table variable with a \n--non clustered column index in it.\nDECLARE @OrganisationNameNonClustered TABLE\n  (\n  Business_id INT identity PRIMARY key,\n  OrganisationName NVARCHAR(100) NOT NULL INDEX NameIndex NONCLUSTERED,\n  PostCode NVARCHAR(15) NOT NULL\n  );\nINSERT INTO @OrganisationNameNonClustered ( OrganisationName, PostCode)\n  SELECT   Name, Postcode FROM Directory\n  UNION ALL\n(SELECT * FROM   (VALUES('D N Philpott','PL56 1GQ'),\n\t\t('J A Hawkes &amp; Son Ltd','PL56 1GQ'),\n\t\t('Kath''s Kabin','PL56 1GQ'),\n\t('Quakers (Religious Society Of Friends)','PL56 1GQ'),\n\t('Dr M R Dadhania','PL56 1GQ'),\n\t('Sheffield City Council','PL56 1GQ'))\n             f( OrganisationName, PostCode)\n\t)\nINSERT INTO @log (WhatHappened) \nSELECT 'insert into TV with PK and nonclustered index took'; --\n--Now we create a second table variable with a clustered index\nDECLARE @OrganisationNameClustered TABLE\n  (\n  OrganisationName  NVARCHAR(100) INDEX NameIndex CLUSTERED,\n  Business_id INT identity,\n  PostCode NVARCHAR(15) NOT NULL\n  );\nINSERT INTO @OrganisationNameClustered  (OrganisationName, PostCode)\n--  SELECT OrganisationName, Postcode FROM @OrganisationNameNonClustered;\n SELECT   Name, Postcode FROM Directory\n  UNION ALL\n(SELECT * FROM   (VALUES('D N Philpott','PL56 1GQ'),\n\t\t('J A Hawkes &amp; Son Ltd','PL56 1GQ'),\n\t\t('Kath''s Kabin','PL56 1GQ'),\n\t('Quakers (Religious Society Of Friends)','PL56 1GQ'),\n\t('Dr M R Dadhania','PL56 1GQ'),\n\t('Sheffield City Council','PL56 1GQ'))\n             f( OrganisationName, PostCode)\n\t)\nINSERT INTO @log (WhatHappened) \nSELECT 'insert into TV with clustered index took'; --\nSELECT OrganisationName\n  FROM @OrganisationNameNonClustered\n  WHERE OrganisationName IN\n('D N Philpott', 'J A Hawkes &amp; Son Ltd', 'Kath''s Kabin',\n  'Quakers (Religious Society Of Friends)', 'Dr M R Dadhania',\n  'Sheffield City Council'\n);\nINSERT INTO @log (WhatHappened) \nSELECT 'Searching for organisation names in TV with clustered TV took';\nSELECT OrganisationName\n  FROM @OrganisationNameClustered\n  WHERE OrganisationName IN\n('D N Philpott', 'J A Hawkes &amp; Son Ltd', 'Kath''s Kabin',\n  'Quakers (Religious Society Of Friends)', 'Dr M R Dadhania',\n  'Sheffield City Council'\n);\nINSERT INTO @log (WhatHappened)\n  SELECT 'Searching for organisation names in TV with PK ' + \n      'and nonclustered TV took ';\nSELECT ending.WhatHappened,\n  DateDiff(ms, starting.WHENItDid, ending.WHENItDid) AS ms\n  FROM @log AS starting\n    INNER JOIN @log AS ending\n      ON ending.TheOrder = starting.TheOrder + 1\nUNION ALL\nSELECT 'Total', DateDiff(ms, Min(WHENItDid), Max(WHENItDid)) FROM @log;\n--list out all the timings<\/pre>\n<p>1000 rows<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"451\" height=\"135\" class=\"wp-image-83321\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/02\/word-image-23.png\" \/><\/p>\n<p>10,000 rows<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"458\" height=\"132\" class=\"wp-image-83322\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/02\/word-image-24.png\" \/><\/p>\n<p>100,000 rows<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"489\" height=\"137\" class=\"wp-image-83323\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/02\/word-image-25.png\" \/><\/p>\n<p>1,000,000 rows<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"477\" height=\"134\" class=\"wp-image-83324\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/02\/word-image-26.png\" \/><\/p>\n<p>4,000,000 rows<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"473\" height=\"136\" class=\"wp-image-83325\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/02\/word-image-27.png\" \/><\/p>\n<p>By creating a clustered index on the organisation name we\u2019ve not only kept the <em>@OrganisationNameClustered<\/em> table to just double the load time for the four million rows when no indexes except the primary key are included, but now the search for organisations is blindingly fast. We have the best of both worlds.<\/p>\n<h2>Conclusions<\/h2>\n<p>The new indexes could, with care, make a great difference to Table Variables. However, this depends on coming up with a good strategy for the design of the table variable. It pays to do performance testing on the batch that uses the table variable because the best strategy to choose is so dependent on the details of what the batch is doing. Some problems are obvious: Don\u2019t scatter lots of indexes about just because you can. They come at a performance cost which can be far higher than the benefits. Get it right and suddenly table variables are going to be working blindingly fast.<\/p>\n<p>Will we see an advantage too when using multi-statement table-valued functions, user-defined table types and table-valued parameters? My own experiments with functions aren\u2019t conclusive, but so far in my experiments, the costs of creating the index before filling the table outweigh the subsequent gains from the index.<\/p>\n<p>\u00a0<\/p>\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs: What are Inline Indexes?<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. Can you add an index to a SQL Server table variable?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Yes, from SQL Server 2014 onward, using inline index syntax in the DECLARE @table TABLE(&#8230;) statement. You can define clustered, nonclustered, and filtered indexes inline in the table variable declaration. This significantly improves query performance for table variables used as large intermediate result sets. The index is created and destroyed with the table variable&#8217;s scope &#8211; no separate CREATE INDEX or DROP INDEX is needed.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. What is the syntax for an inline index on a table variable in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Declare the table variable with an INDEX clause after the column definitions: DECLARE @t TABLE (ID INT NOT NULL, Name NVARCHAR(100), INDEX ix_Name NONCLUSTERED (Name)). For a clustered index: add PRIMARY KEY CLUSTERED (ID) in the column definition, or use INDEX ix_cluster CLUSTERED (ID). Filtered indexes use the same WHERE clause syntax as regular filtered indexes.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. What is a UDTT inline index in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p>A user-defined table type (UDTT) is a named table type stored in the database that can be used as a parameter type for stored procedures and functions. From SQL Server 2014, UDTTs support inline index definitions using the same syntax as table variable inline indexes. Indexed UDTTs improve performance when large table-valued parameters are passed to stored procedures, because the receiving procedure gets a parameter that is already indexed.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">4. Do inline indexes on table variables persist between executions?<\/h3>\n            <div class=\"faq-answer\">\n                <p>No. Table variable inline indexes are created when the DECLARE statement executes and dropped when the table variable goes out of scope &#8211; typically at the end of the batch or stored procedure. They do not persist in the database. UDTT indexes are defined as part of the type definition and are recreated each time a variable of that type is declared, but the index definition itself persists with the type in the database.<\/p>\n            <\/div>\n            <\/section>\n","protected":false},"excerpt":{"rendered":"<p>SQL Server inline indexes let you define clustered, nonclustered, and filtered indexes directly in CREATE TABLE syntax for table variables, UDTTs, and multi-statement table functions. Learn the syntax with working T-SQL examples and performance comparisons.&hellip;<\/p>\n","protected":false},"author":154613,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":true,"footnotes":""},"categories":[143529,143531],"tags":[95509],"coauthors":[6813],"class_list":["post-83311","post","type-post","status-publish","format-standard","hentry","category-performance-sql-server","category-t-sql-programming-sql-server","tag-standardize"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/83311","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\/154613"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=83311"}],"version-history":[{"count":17,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/83311\/revisions"}],"predecessor-version":[{"id":109603,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/83311\/revisions\/109603"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=83311"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=83311"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=83311"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=83311"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}