<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	xmlns:media="http://search.yahoo.com/mrss/" >

<channel>
	<title>SQL Server Guides</title>
	<atom:link href="https://sqlserverguides.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://sqlserverguides.com</link>
	<description>Tutorials on SQL Server</description>
	<lastBuildDate>Wed, 04 Nov 2026 07:26:57 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.4</generator>

<image>
	<url>https://sqlserverguides.com/wp-content/uploads/2023/10/sqlserverguides-150x150.png</url>
	<title>SQL Server Guides</title>
	<link>https://sqlserverguides.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>How to Convert Rows Into Columns in SQL</title>
		<link>https://sqlserverguides.com/how-to-convert-rows-into-columns-in-sql/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Wed, 04 Nov 2026 07:25:24 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[How to Convert Rows Into Columns in SQL]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=24083</guid>

					<description><![CDATA[A finance team once asked me for a report that looked simple on paper: one line per product, with four columns for Q1 through Q4. Their database stored sales the way databases should, with one row per product per quarter. An analyst had been exporting the data to Excel and rebuilding the layout by hand ... <a title="How to Convert Rows Into Columns in SQL" class="read-more" href="https://sqlserverguides.com/how-to-convert-rows-into-columns-in-sql/" aria-label="Read more about How to Convert Rows Into Columns in SQL">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A finance team once asked me for a report that looked simple on paper: one line per product, with four columns for Q1 through Q4. Their database stored sales the way databases should, with one row per product per quarter. An analyst had been exporting the data to Excel and rebuilding the layout by hand every month. When a quarter was missing for one product, the copy-and-paste shifted a column and the numbers landed under the wrong heading for two weeks.</p>



<p class="wp-block-paragraph">This is the classic reason to <strong>convert rows into columns in SQL</strong>. The database holds narrow, tall data, and reports want wide, flat data. SQL Server gives you three solid ways to do it: the <code>PIVOT</code> operator, conditional aggregation with <code>CASE</code>, and dynamic SQL when the columns are not known in advance. This guide builds each one, explains the trade-offs, and covers NULLs, performance, and secure reporting in SQL Server and Azure SQL Database.</p>



<h2 class="wp-block-heading">What &#8220;Rows Into Columns&#8221; Means</h2>



<p class="wp-block-paragraph">Turning rows into columns is called <strong>pivoting</strong>. You take the distinct values from one column, such as Q1, Q2, Q3, and Q4, and turn each into its own column heading. At the same time, you <strong>aggregate</strong>, which means combine many rows into one value with a function like <code>SUM</code>, so each cell of the new grid holds a single number.</p>



<p class="wp-block-paragraph">Every pivot has three parts:</p>



<ul class="wp-block-list">
<li><strong>Row identifier:</strong> the column that stays as rows, such as <code>Product</code>.</li>



<li><strong>Pivot column:</strong> the column whose values become headings, such as <code>Quarter</code>.</li>



<li><strong>Value column:</strong> the data that fills the cells, such as <code>Revenue</code>, with an aggregate.</li>
</ul>



<p class="wp-block-paragraph">The opposite operation, turning columns back into rows, is called unpivoting; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-unpivot/">SQL UNPIVOT</a>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, I write down those three parts on paper before touching the keyboard. Nearly every broken pivot I have debugged was missing a clear answer to &#8220;which column becomes the headings?&#8221;</p>
</blockquote>



<h3 class="wp-block-heading">Create a Practice Table</h3>



<p class="wp-block-paragraph">A <strong>schema</strong> is the structure of your tables. Use a small sales table with realistic gaps and a duplicate.</p>



<pre class="wp-block-preformatted"><code>CREATE TABLE dbo.QuarterlySales (<br>    SaleID   INT IDENTITY(1,1) PRIMARY KEY,<br>    Product  VARCHAR(30)   NOT NULL,<br>    Quarter  CHAR(2)       NOT NULL,<br>    Revenue  DECIMAL(10,2) NOT NULL<br>);<br><br>INSERT INTO dbo.QuarterlySales (Product, Quarter, Revenue)<br>VALUES<br> ('Laptop',  'Q1', 100.00), ('Laptop',  'Q1',  20.00),<br> ('Laptop',  'Q2', 120.00), ('Laptop',  'Q3',  90.00), ('Laptop', 'Q4', 150.00),<br> ('Monitor', 'Q1',  60.00), ('Monitor', 'Q3',  70.00), ('Monitor', 'Q4',  80.00),<br> ('Dock',    'Q2',  30.00), ('Dock',    'Q3',  40.00);</code></pre>



<p class="wp-block-paragraph">After executing the above query, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img fetchpriority="high" decoding="async" width="870" height="401" src="https://sqlserverguides.com/wp-content/uploads/2026/11/convert-rows-into-columns-in-sql.jpg" alt="convert rows into columns in sql" class="wp-image-24084" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/convert-rows-into-columns-in-sql.jpg 870w, https://sqlserverguides.com/wp-content/uploads/2026/11/convert-rows-into-columns-in-sql-766x353.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/11/convert-rows-into-columns-in-sql-300x138.jpg 300w" sizes="(max-width: 870px) 100vw, 870px" /></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><img decoding="async" width="1024" height="290" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-convert-rows-into-columns-in-sql-server-1024x290.jpg" alt="how to convert rows into columns in sql server" class="wp-image-24085" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-convert-rows-into-columns-in-sql-server-1024x290.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-convert-rows-into-columns-in-sql-server-300x85.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-convert-rows-into-columns-in-sql-server-766x217.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-convert-rows-into-columns-in-sql-server.jpg 1508w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">Run this only in a test database, because it inserts rows. Notice three deliberate details. Laptop has two Q1 rows that must be added together. Monitor has no Q2 row. Dock has no Q1 or Q4 rows. The expected wide result is:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Product</th><th class="has-text-align-left" data-align="left">Q1</th><th class="has-text-align-left" data-align="left">Q2</th><th class="has-text-align-left" data-align="left">Q3</th><th class="has-text-align-left" data-align="left">Q4</th></tr></thead><tbody><tr><td>Dock</td><td>NULL</td><td>30</td><td>40</td><td>NULL</td></tr><tr><td>Laptop</td><td>120</td><td>120</td><td>90</td><td>150</td></tr><tr><td>Monitor</td><td>60</td><td>NULL</td><td>70</td><td>80</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">How to Convert Rows Into Columns in SQL With PIVOT</h3>



<p class="wp-block-paragraph">The <code>PIVOT</code> operator is built into T-SQL for this job. It works in SQL Server and Azure SQL Database.</p>



<pre class="wp-block-preformatted"><code>SELECT Product, [Q1], [Q2], [Q3], [Q4]<br>FROM (<br>    SELECT Product, Quarter, Revenue<br>    FROM dbo.QuarterlySales<br>) AS src<br>PIVOT (<br>    SUM(Revenue)<br>    FOR Quarter IN ([Q1], [Q2], [Q3], [Q4])<br>) AS p<br>ORDER BY Product;</code></pre>



<p class="wp-block-paragraph">Here is what each piece does:</p>



<ul class="wp-block-list">
<li><strong>The inner query</strong> (<code>src</code>) selects only the three columns the pivot needs. This matters a lot, and we come back to it below.</li>



<li><strong><code>SUM(Revenue)</code></strong> is the aggregate that fills each cell. Laptop&#8217;s two Q1 rows add up to 120.</li>



<li><strong><code>FOR Quarter IN (...)</code></strong> names the pivot column and lists the values that become headings. You must list them by hand. Square brackets are required for names that contain spaces or start with a digit.</li>



<li><strong><code>AS p</code></strong> gives the pivot result an alias, which SQL Server requires.</li>
</ul>



<p class="wp-block-paragraph">After executing the above query, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img decoding="async" width="891" height="512" src="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Convert-Rows-Into-Columns-in-SQL.jpg" alt="How to Convert Rows Into Columns in SQL" class="wp-image-24086" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Convert-Rows-Into-Columns-in-SQL.jpg 891w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Convert-Rows-Into-Columns-in-SQL-300x172.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Convert-Rows-Into-Columns-in-SQL-767x441.jpg 767w" sizes="(max-width: 891px) 100vw, 891px" /></figure>
</div>


<p class="wp-block-paragraph">The output matches the table above. Missing combinations, like Monitor Q2, appear as NULL because no row existed to aggregate. A <strong>NULL</strong> is a missing or unknown value, not zero. To show zeros, wrap each column with <code>COALESCE</code>, which returns the first non-NULL value; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-coalesce-function/">the COALESCE function</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-null/">how SQL handles NULL</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT Product,<br>       COALESCE([Q1], 0) AS Q1,<br>       COALESCE([Q2], 0) AS Q2,<br>       COALESCE([Q3], 0) AS Q3,<br>       COALESCE([Q4], 0) AS Q4<br>FROM (SELECT Product, Quarter, Revenue FROM dbo.QuarterlySales) AS src<br>PIVOT (SUM(Revenue) FOR Quarter IN ([Q1], [Q2], [Q3], [Q4])) AS p;</code></pre>



<p class="wp-block-paragraph">After executing the above query, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="316" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-convert-rows-into-columns-in-sql-query-1024x316.jpg" alt="" class="wp-image-24087" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-convert-rows-into-columns-in-sql-query-1024x316.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-convert-rows-into-columns-in-sql-query-768x237.jpg 768w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-convert-rows-into-columns-in-sql-query-300x92.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-convert-rows-into-columns-in-sql-query.jpg 1338w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<h4 class="wp-block-heading">The Hidden-Column Trap</h4>



<p class="wp-block-paragraph"><code>PIVOT</code> groups by every column in the source that is not the pivot column or the value column. If you write <code>SELECT * FROM dbo.QuarterlySales PIVOT (...)</code>, the unique <code>SaleID</code> becomes part of the grouping, and every row ends up on its own line with mostly NULLs. Always use a subquery that selects only the columns you need, as shown above.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I once debugged a report that returned ten times too many rows because someone added a <code>LastModified</code> column to the source. Selecting exactly three columns in the inner query protects you from that kind of drift.</p>
</blockquote>



<h3 class="wp-block-heading">Method 2: Conditional Aggregation With CASE</h3>



<p class="wp-block-paragraph">You can get the same result with ordinary <code>GROUP BY</code> and <code>CASE</code>. This is called <strong>conditional aggregation</strong>: each column sums only the rows that match its heading. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/case-statement-in-sql-server/">the CASE statement</a>, <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-sum-function-in-sql-server/">the SUM function</a>, and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-group-by-clause-in-sql-server/">GROUP BY</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    Product,<br>    SUM(CASE WHEN Quarter = 'Q1' THEN Revenue ELSE 0 END) AS Q1,<br>    SUM(CASE WHEN Quarter = 'Q2' THEN Revenue ELSE 0 END) AS Q2,<br>    SUM(CASE WHEN Quarter = 'Q3' THEN Revenue ELSE 0 END) AS Q3,<br>    SUM(CASE WHEN Quarter = 'Q4' THEN Revenue ELSE 0 END) AS Q4<br>FROM dbo.QuarterlySales<br>GROUP BY Product<br>ORDER BY Product;</code></pre>



<p class="wp-block-paragraph">After executing the above query, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="429" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-convert-rows-into-columns-sql-server-1024x429.jpg" alt="how to convert rows into columns sql server" class="wp-image-24088" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-convert-rows-into-columns-sql-server-1024x429.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-convert-rows-into-columns-sql-server-300x126.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-convert-rows-into-columns-sql-server-766x321.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-convert-rows-into-columns-sql-server.jpg 1229w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">Each <code>CASE</code> returns the revenue when the row belongs to that quarter and 0 otherwise. <code>SUM</code> adds them up per product. The <code>ELSE 0</code> gives zeros instead of NULLs. If you leave out <code>ELSE</code>, missing combinations come back as NULL.</p>



<h3 class="wp-block-heading">PIVOT vs. CASE</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Question</th><th class="has-text-align-left" data-align="left">PIVOT</th><th class="has-text-align-left" data-align="left">CASE (conditional aggregation)</th></tr></thead><tbody><tr><td>Code length for many columns</td><td>Shorter</td><td>Longer</td></tr><tr><td>Multiple aggregates (sum and count)</td><td>Awkward</td><td>Easy</td></tr><tr><td>Different logic per column</td><td>Not possible</td><td>Easy</td></tr><tr><td>Works on all SQL dialects</td><td>No (T-SQL and a few others)</td><td>Yes</td></tr><tr><td>Familiar to new teammates</td><td>Often unfamiliar</td><td>Usually clear</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Both read the table once, so performance is usually similar. I use <code>PIVOT</code> for quick one-aggregate layouts and <code>CASE</code> when a report needs counts, sums, and custom rules side by side. Pick the one your team can maintain.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, teams that learn conditional aggregation first rarely need <code>PIVOT</code>. It handles more cases, and it ports to other databases if you ever migrate.</p>
</blockquote>



<h3 class="wp-block-heading">Method 3: Dynamic Pivot When Columns Change</h3>



<p class="wp-block-paragraph">Both methods above hard-code the headings. That breaks the day a new value appears, such as Q5 or a new region. When the list of values changes, build the column list from the data with <strong>dynamic SQL</strong>, which means assembling a SQL statement as text and running it at run time. Read more in <a rel="noopener" target="_blank" href="https://sqlserverguides.com/dynamic-sql/">dynamic SQL</a>.</p>



<pre class="wp-block-preformatted"><code>DECLARE @cols NVARCHAR(MAX), @sql NVARCHAR(MAX);<br><br>SELECT @cols = STRING_AGG(CAST(QUOTENAME(Quarter) AS NVARCHAR(MAX)), N', ')<br>               WITHIN GROUP (ORDER BY Quarter)<br>FROM (SELECT DISTINCT Quarter FROM dbo.QuarterlySales) AS q;<br><br>SET @sql = N'<br>SELECT Product, ' + @cols + N'<br>FROM (SELECT Product, Quarter, Revenue FROM dbo.QuarterlySales) AS src<br>PIVOT (SUM(Revenue) FOR Quarter IN (' + @cols + N')) AS p<br>ORDER BY Product;';<br><br>EXEC sys.sp_executesql @sql;</code></pre>



<p class="wp-block-paragraph">Step by step:</p>



<ul class="wp-block-list">
<li><strong><code>SELECT DISTINCT Quarter</code></strong> finds the headings that exist right now.</li>



<li><strong><code>QUOTENAME</code></strong> wraps each value in brackets and escapes special characters. This is essential for safety and for names with spaces. See <a href="https://sqlserverguides.com/quotename-function-in-sql-server/" target="_blank" rel="noopener">QUOTENAME</a>.</li>



<li><strong><code>STRING_AGG</code></strong> joins the values into a comma-separated list, ordered with <code>WITHIN GROUP</code>. It requires SQL Server 2017 or later and is available in Azure SQL Database. See <a href="https://sqlserverguides.com/sql-server-string_agg-function/" target="_blank" rel="noopener">STRING_AGG</a>. On older versions, the <code>FOR XML PATH</code> trick is the traditional replacement.</li>



<li><strong><code>sp_executesql</code></strong> runs the generated statement.</li>
</ul>



<p class="wp-block-paragraph">After executing the above query, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="488" src="https://sqlserverguides.com/wp-content/uploads/2026/11/convert-columns-into-rows-in-sql-1024x488.jpg" alt="sql how to convert rows into columns" class="wp-image-24089" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/convert-columns-into-rows-in-sql-1024x488.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/convert-columns-into-rows-in-sql-300x143.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/convert-columns-into-rows-in-sql-766x365.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/11/convert-columns-into-rows-in-sql.jpg 1413w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">Dynamic SQL carries risks. If any heading value comes from user input, an attacker could try <strong>SQL injection</strong>, which means slipping extra SQL commands into the text. <code>QUOTENAME</code> neutralizes this for identifiers, but never concatenate raw user input anywhere else in the statement. </p>



<p class="wp-block-paragraph">Also remember that dynamic SQL does not benefit from <strong>ownership chaining</strong>, the rule that lets a procedure access tables its owner can access without extra grants. The caller needs permission on the underlying table, or the procedure must use <code>EXECUTE AS</code>.</p>



<p class="wp-block-paragraph">Wide results have limits too. A result set can hold a large number of columns, but a report with hundreds of dynamic columns is hard to read and slow to render. If the headings number in the dozens, consider doing the final layout in the reporting tool or application.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I log the generated <code>@sql</code> text to a debug table during testing. Seeing the exact statement makes dynamic pivot bugs obvious in seconds, instead of staring at a string in a debugger.</p>
</blockquote>



<h3 class="wp-block-heading">Pivot Dates and Months</h3>



<p class="wp-block-paragraph">Real reports often pivot dates. Raw dates make poor headings, because every day would become a column. Convert them to a month or a quarter first:</p>



<pre class="wp-block-preformatted"><code>SELECT Product,<br>       SUM(CASE WHEN MONTH(SaleDate) = 1 THEN Revenue ELSE 0 END) AS Jan,<br>       SUM(CASE WHEN MONTH(SaleDate) = 2 THEN Revenue ELSE 0 END) AS Feb,<br>       SUM(CASE WHEN MONTH(SaleDate) = 3 THEN Revenue ELSE 0 END) AS Mar<br>FROM dbo.DailySales<br>WHERE SaleDate >= '2026-01-01' AND SaleDate &lt; '2026-04-01'<br>GROUP BY Product;</code></pre>



<p class="wp-block-paragraph">This assumes a <code>DailySales</code> table with a <code>SaleDate</code> column. The date filter uses a half-open range, which includes January 1 and excludes April 1, and it keeps the column bare so SQL Server can use an index. For month functions, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-get-month-from-date/">getting the month from a date</a>. For the full date-pivot pattern, read <a rel="noopener" target="_blank" href="https://sqlserverguides.com/pivot-date-column-in-sql-server/">pivoting a date column</a>.</p>



<h3 class="wp-block-heading">Turn Rows Into a Single Column</h3>



<p class="wp-block-paragraph">Sometimes &#8220;rows into columns&#8221; means joining many rows into one text value, such as a comma-separated list of tags. That is a different task. Use <code>STRING_AGG</code>:</p>



<pre class="wp-block-preformatted"><code>SELECT Product, STRING_AGG(Quarter, ', ') WITHIN GROUP (ORDER BY Quarter) AS QuartersSold<br>FROM dbo.QuarterlySales<br>GROUP BY Product;</code></pre>



<p class="wp-block-paragraph">This returns lines such as &#8220;Q1, Q1, Q2, Q3, Q4&#8221; for Laptop. Use <code>SELECT DISTINCT</code> in a subquery if you want each quarter once. The result is text, so you can no longer aggregate it, which is why this approach suits display and exports more than analysis.</p>



<h3 class="wp-block-heading">Performance, Indexing, and Reuse</h3>



<p class="wp-block-paragraph">A pivot reads the source rows once, groups them, and spreads values across columns. The cost depends mostly on how many rows you scan, not on the pivot itself. To keep it fast:</p>



<ul class="wp-block-list">
<li><strong>Filter first.</strong> Restrict by date or tenant in the source subquery so you pivot fewer rows.</li>



<li><strong>Index the grouping and filter columns.</strong> An index on <code>(Product, Quarter)</code> that includes <code>Revenue</code> lets SQL Server read a narrow structure. See <a href="https://sqlserverguides.com/how-to-create-index-in-sql-server/" target="_blank" rel="noopener">how to create an index in SQL Server</a>. Check the actual execution plan first, because each index slows inserts and updates.</li>



<li><strong>Pre-aggregate large tables.</strong> For hundreds of millions of rows, keep a summary table updated nightly and pivot that instead.</li>



<li><strong>Use a CTE for clarity.</strong> A common table expression names an intermediate step; compare it with a temp table in <a href="https://sqlserverguides.com/sql-server-cte-vs-temp-table/" target="_blank" rel="noopener">CTE vs. temp table</a>.</li>
</ul>



<p class="wp-block-paragraph">Save the final query in a <strong>view</strong> or a stored procedure so every report uses the same layout; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-views/">SQL Server views</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/create-stored-procedure-in-sql-server/">creating a stored procedure</a>. For more tuning ideas, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-query-optimization-tips/">SQL query optimization tips</a>. In Azure SQL Database, use <strong>Query Store</strong>, which records query run times, to confirm that a change helped.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, the best pivot optimization is a date filter. Pivoting five years of history to show the last quarter wastes compute, which on serverless Azure SQL Database also wastes money.</p>
</blockquote>



<h3 class="wp-block-heading">Frequently Asked Questions</h3>



<h4 class="wp-block-heading">How do I convert rows into columns in SQL Server?</h4>



<p class="wp-block-paragraph">Use the <code>PIVOT</code> operator, or use <code>GROUP BY</code> with <code>SUM(CASE WHEN ... THEN ... END)</code> for each new column. Both turn distinct values from one column into column headings and aggregate a value column into the cells.</p>



<h4 class="wp-block-heading">What is the difference between PIVOT and CASE for pivoting?</h4>



<p class="wp-block-paragraph"><code>PIVOT</code> is shorter for one aggregate across many columns. <code>CASE</code> is more flexible because you can use several aggregates and custom logic per column, and it works in most SQL dialects. Performance is usually similar.</p>



<h4 class="wp-block-heading">How do I pivot when I do not know the column values in advance?</h4>



<p class="wp-block-paragraph">Build the column list from the data with <code>STRING_AGG</code> and <code>QUOTENAME</code>, assemble a dynamic <code>PIVOT</code> statement, and run it with <code>sp_executesql</code>. Validate inputs, and remember that the caller needs permission on the underlying table.</p>



<h4 class="wp-block-heading">Why does my PIVOT return duplicate rows or too many rows?</h4>



<p class="wp-block-paragraph">The source includes extra columns, such as an ID, and <code>PIVOT</code> groups by all of them. Select only the row identifier, pivot column, and value column in a subquery.</p>



<h4 class="wp-block-heading">How do I show zero instead of NULL in a pivot?</h4>



<p class="wp-block-paragraph">Wrap each pivoted column in <code>COALESCE(column, 0)</code>, or use <code>ELSE 0</code> inside the <code>CASE</code> expressions. Decide whether zero is accurate, since NULL can mean no data and zero means no sales.</p>



<p class="wp-block-paragraph">You learned how to convert rows into columns in SQL Server with <code>PIVOT</code>, conditional aggregation, and dynamic SQL, and how to handle NULLs, hidden grouping columns, and changing headings. Select only the columns the pivot needs, protect dynamic SQL with <code>QUOTENAME</code>, and give report users read-only access through a view. I hope you found this article helpful.</p>



<p class="wp-block-paragraph">You May Also Like</p>



<ul class="wp-block-list">
<li><a href="https://sqlserverguides.com/sql-server-split-string-into-rows/" target="_blank" rel="noopener">SQL Server string split into rows</a></li>



<li><a href="https://sqlserverguides.com/sql-server-json-to-table/" target="_blank" rel="noopener">Converting JSON to a table in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/sql-server-group-by-date/" target="_blank" rel="noopener">How to group by date in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/how-to-use-count-function-in-sql-server/" target="_blank" rel="noopener">How to use the COUNT function in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/sql-cheat-sheet/" target="_blank" rel="noopener">SQL cheat sheet</a></li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to Use CASE WHEN in SQL Server</title>
		<link>https://sqlserverguides.com/case-when-in-sql-server/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Wed, 04 Nov 2026 05:41:44 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[How to Use CASE WHEN in SQL Server]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=24072</guid>

					<description><![CDATA[A support lead once asked me why her &#8220;high priority&#8221; ticket report showed refunds as low priority. The developer had written a long chain of IF logic in the application, and every time the rules changed, the app and the report disagreed. We moved the rule into one T-SQL expression, and the two numbers matched ... <a title="How to Use CASE WHEN in SQL Server" class="read-more" href="https://sqlserverguides.com/case-when-in-sql-server/" aria-label="Read more about How to Use CASE WHEN in SQL Server">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A support lead once asked me why her &#8220;high priority&#8221; ticket report showed refunds as low priority. The developer had written a long chain of <code>IF</code> logic in the application, and every time the rules changed, the app and the report disagreed. We moved the rule into one T-SQL expression, and the two numbers matched on the first run.</p>



<p class="wp-block-paragraph">That expression was <code>CASE WHEN</code>, and learning <strong>how to use CASE WHEN in SQL Server</strong> is one of the best investments a developer or analyst can make. It adds if/then logic directly inside a query, so labels, buckets, custom sorting, and conditional totals all come from the database in one pass. This guide covers both forms of <code>CASE</code>, the common patterns, the NULL and data-type traps, performance, and safe use in production on SQL Server or Azure SQL Database.</p>



<h2 class="wp-block-heading">What CASE Does and the Two Forms</h2>



<p class="wp-block-paragraph"><code>CASE</code> is an <strong>expression</strong>, not a statement. That means it returns a value and can be used anywhere a value is allowed: in <code>SELECT</code>, <code>WHERE</code>, <code>ORDER BY</code>, <code>GROUP BY</code>, aggregates, and <code>UPDATE</code>. SQL Server evaluates the conditions from top to bottom and returns the result for the first one that is true.</p>



<p class="wp-block-paragraph">There are two forms.</p>



<p class="wp-block-paragraph"><strong>Simple CASE</strong> compares one expression to a list of values:</p>



<pre class="wp-block-preformatted"><code>CASE Status<br>    WHEN 'Shipped' THEN 'Done'<br>    WHEN 'Pending' THEN 'Open'<br>    ELSE 'Other'<br>END</code></pre>



<p class="wp-block-paragraph"><strong>Searched CASE</strong> tests full conditions, so it handles ranges, <code>AND</code>/<code>OR</code>, and NULL checks:</p>



<pre class="wp-block-preformatted"><code>CASE<br>    WHEN Amount &lt; 100 THEN 'Small'<br>    WHEN Amount &lt; 500 THEN 'Medium'<br>    ELSE 'Large'<br>END</code></pre>



<p class="wp-block-paragraph">Use simple <code>CASE</code> for exact matches and searched <code>CASE</code> for everything else. If you leave out <code>ELSE</code> and no condition matches, the result is NULL. Always add an <code>ELSE</code> unless NULL is truly what you want.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, I default to the searched form even for exact matches. It costs nothing, and it avoids a rewrite when someone asks for a range or a NULL check later.</p>
</blockquote>



<h3 class="wp-block-heading">Create a Practice Table</h3>



<p class="wp-block-paragraph">A <strong>schema</strong> is the structure of your tables. Use a small orders table with edge cases: a NULL amount, mixed statuses, and values on both sides of each band.</p>



<pre class="wp-block-preformatted"><code>CREATE TABLE dbo.Orders (<br>    OrderID    INT IDENTITY(1,1) PRIMARY KEY,<br>    CustomerID INT NOT NULL,<br>    Amount     DECIMAL(10,2) NULL,<br>    Status     VARCHAR(20) NOT NULL,<br>    OrderDate  DATE NOT NULL<br>);<br><br>INSERT INTO dbo.Orders (CustomerID, Amount, Status, OrderDate)<br>VALUES<br> (101,   45.00, 'Shipped',   '2026-10-01'),<br> (102,  250.00, 'Pending',   '2026-10-03'),<br> (103, 1200.00, 'Shipped',   '2026-10-05'),<br> (101,    NULL, 'Cancelled', '2026-10-06'),<br> (104,   89.99, 'Returned',  '2026-10-08'),<br> (105,  600.00, 'Pending',   '2026-10-09');</code></pre>



<p class="wp-block-paragraph">Run this only in a test database, because it inserts rows. Order 4 has a NULL amount on purpose.</p>



<p class="wp-block-paragraph">After executing the query above, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="912" height="568" src="https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-in-sql-server.jpg" alt="case when in sql server" class="wp-image-24073" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-in-sql-server.jpg 912w, https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-in-sql-server-300x187.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-in-sql-server-767x478.jpg 767w" sizes="auto, (max-width: 912px) 100vw, 912px" /></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="406" src="https://sqlserverguides.com/wp-content/uploads/2026/11/select-case-when-in-sql-server-1024x406.jpg" alt="select case when in sql server" class="wp-image-24074" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/select-case-when-in-sql-server-1024x406.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/select-case-when-in-sql-server-300x119.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/select-case-when-in-sql-server-766x304.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/11/select-case-when-in-sql-server.jpg 1187w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<h3 class="wp-block-heading">How to Use CASE WHEN in SQL Server SELECT Queries</h3>



<p class="wp-block-paragraph">The most common use is creating a label column. This query puts each order in a size band:</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    OrderID,<br>    Amount,<br>    CASE<br>        WHEN Amount IS NULL THEN 'No amount'<br>        WHEN Amount &lt; 100   THEN 'Small'<br>        WHEN Amount &lt; 500   THEN 'Medium'<br>        ELSE 'Large'<br>    END AS OrderSize<br>FROM dbo.Orders;</code></pre>



<p class="wp-block-paragraph">After executing the query above, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="894" height="573" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-use-case-when-in-sql-server.jpg" alt="how to use case when in sql server" class="wp-image-24075" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-use-case-when-in-sql-server.jpg 894w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-use-case-when-in-sql-server-300x192.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-use-case-when-in-sql-server-768x492.jpg 768w" sizes="auto, (max-width: 894px) 100vw, 894px" /></figure>
</div>


<p class="wp-block-paragraph">The results are Small, Medium, Large, No amount, Small, and Large for orders 1 to 6. Notice three details:</p>



<ul class="wp-block-list">
<li><strong>Order matters.</strong> SQL Server returns the first true branch. Because <code>Amount &lt; 100</code> comes before <code>Amount &lt; 500</code>, an order of 45 is Small, even though it is also below 500. If you reverse them, everything below 500 becomes Medium.</li>



<li><strong>The NULL check comes first.</strong> A NULL amount makes both <code>Amount &lt; 100</code> and <code>Amount &lt; 500</code> unknown, not true, so it would fall through to <code>ELSE</code> and be labeled Large. That is a silent bug. For more on this behavior, see <a href="https://sqlserverguides.com/sql-null/" target="_blank" rel="noopener">how SQL handles NULL</a>.</li>



<li><strong>Aliases name the result.</strong> <code>AS OrderSize</code> gives the new column a readable name.</li>
</ul>



<p class="wp-block-paragraph">You can also translate codes with a simple <code>CASE</code>:</p>



<pre class="wp-block-preformatted"><code>SELECT OrderID,<br>       CASE Status<br>           WHEN 'Shipped'   THEN 'Complete'<br>           WHEN 'Pending'   THEN 'In progress'<br>           WHEN 'Cancelled' THEN 'Closed'<br>           WHEN 'Returned'  THEN 'Closed'<br>           ELSE 'Unknown'<br>       END AS StatusGroup<br>FROM dbo.Orders;</code></pre>



<p class="wp-block-paragraph">After executing the query above, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="928" height="572" src="https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-then-in-sql-server.jpg" alt="case when then in sql server" class="wp-image-24080" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-then-in-sql-server.jpg 928w, https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-then-in-sql-server-767x473.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-then-in-sql-server-300x185.jpg 300w" sizes="auto, (max-width: 928px) 100vw, 928px" /></figure>
</div>


<p class="wp-block-paragraph">Keeping this mapping in one query, or better in a view, prevents the &#8220;app and report disagree&#8221; problem. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-views/">SQL Server views</a>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I write the NULL branch first in every searched <code>CASE</code>. It takes one line and removes a whole class of &#8220;why is this row in the wrong bucket&#8221; bugs.</p>
</blockquote>



<h3 class="wp-block-heading">The NULL Trap: <code>= NULL</code> Never Matches</h3>



<p class="wp-block-paragraph">This is the number one mistake. Inside <code>CASE</code>, <code>WHEN Amount = NULL</code> is never true, because comparing with NULL gives unknown.</p>



<pre class="wp-block-preformatted"><code><em>-- Wrong: never matches</em><br>CASE WHEN Amount = NULL THEN 'No amount' ELSE 'Has amount' END<br><br><em>-- Right</em><br>CASE WHEN Amount IS NULL THEN 'No amount' ELSE 'Has amount' END</code></pre>



<p class="wp-block-paragraph">The same trap exists in simple <code>CASE</code>. <code>CASE Amount WHEN NULL THEN ...</code> also never matches, because the simple form uses equality. Use the searched form with <code>IS NULL</code>.</p>



<p class="wp-block-paragraph">For shorter NULL handling, <code>COALESCE</code> and <code>ISNULL</code> often replace a whole <code>CASE</code>; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-coalesce-function/">COALESCE</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/isnull-function-in-sql-server/">ISNULL</a>. For choosing between them, read <a rel="noopener" target="_blank" href="https://sqlserverguides.com/nullif-vs-coalesce/">NULLIF vs. COALESCE</a>.</p>



<h3 class="wp-block-heading">Use CASE in ORDER BY for Custom Sorting</h3>



<p class="wp-block-paragraph">Alphabetical order is rarely the business order. <code>CASE</code> in <code>ORDER BY</code> lets you define your own ranking:</p>



<pre class="wp-block-preformatted"><code>SELECT OrderID, Status, OrderDate<br>FROM dbo.Orders<br>ORDER BY<br>    CASE Status<br>        WHEN 'Pending'   THEN 1<br>        WHEN 'Shipped'   THEN 2<br>        WHEN 'Returned'  THEN 3<br>        WHEN 'Cancelled' THEN 4<br>        ELSE 5<br>    END,<br>    OrderDate DESC;</code></pre>



<p class="wp-block-paragraph">After executing the query above, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="723" height="632" src="https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-in-sql-server-example.jpg" alt="case when in sql server example" class="wp-image-24076" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-in-sql-server-example.jpg 723w, https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-in-sql-server-example-300x262.jpg 300w" sizes="auto, (max-width: 723px) 100vw, 723px" /></figure>
</div>


<p class="wp-block-paragraph">The <code>CASE</code> maps each status to a number, and SQL Server sorts by that number first. Pending orders appear at the top, then Shipped, Returned, and Cancelled, with the newest order first inside each group. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/order-by-clause-in-sql-server/">ORDER BY in SQL Server</a> for the basics.</p>



<p class="wp-block-paragraph">A second use is putting NULLs last. SQL Server sorts NULLs first in ascending order, so this flips that:</p>



<pre class="wp-block-preformatted">sql<code>ORDER BY CASE WHEN Amount IS NULL THEN 1 ELSE 0 END, Amount;</code></pre>



<p class="wp-block-paragraph">The first sort key is 0 for rows with an amount and 1 for NULL rows, so NULLs fall to the end.</p>



<h3 class="wp-block-heading">Conditional Aggregation: Pivot-Style Reports</h3>



<p class="wp-block-paragraph">You can place <code>CASE</code> inside an aggregate function to count or sum only certain rows. This is called <strong>conditional aggregation</strong>, and it replaces many separate queries with one. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-sum-function-in-sql-server/">the SUM function</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-group-by-clause-in-sql-server/">GROUP BY</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    COUNT(*) AS TotalOrders,<br>    SUM(CASE WHEN Status = 'Shipped'   THEN 1 ELSE 0 END) AS ShippedCount,<br>    SUM(CASE WHEN Status = 'Pending'   THEN 1 ELSE 0 END) AS PendingCount,<br>    SUM(CASE WHEN Status = 'Shipped'   THEN Amount ELSE 0 END) AS ShippedRevenue,<br>    SUM(CASE WHEN Status = 'Pending'   THEN Amount ELSE 0 END) AS PendingRevenue<br>FROM dbo.Orders;</code></pre>



<p class="wp-block-paragraph">After executing the query above, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="263" src="https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-in-sql-server-where-clause-1024x263.jpg" alt="case when in sql server where clause" class="wp-image-24077" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-in-sql-server-where-clause-1024x263.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-in-sql-server-where-clause-300x77.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-in-sql-server-where-clause-767x197.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-in-sql-server-where-clause.jpg 1522w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">With the sample data, this returns 6 total orders, 2 shipped, 2 pending, 1,245.00 in shipped revenue, and 850.00 in pending revenue. Each <code>CASE</code> returns 1 or the amount when the row qualifies and 0 otherwise, and <code>SUM</code> adds the results.</p>



<p class="wp-block-paragraph">Use <code>SUM(CASE ... THEN 1 ELSE 0 END)</code>, not <code>COUNT(CASE ... THEN 1 ELSE 0 END)</code>. <code>COUNT</code> counts every non-NULL value, so 0 is counted too. If you want <code>COUNT</code>, omit the <code>ELSE</code> so non-matching rows become NULL. For more on <code>COUNT</code>, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-count-function-in-sql-server/">the COUNT function</a>.</p>



<p class="wp-block-paragraph">For a full pivot with dynamic columns, SQL Server also has a <code>PIVOT</code> operator; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/pivot-in-sql-server-rows-to-columns/">pivoting rows to columns</a>. Conditional aggregation is often easier to read and works with any version.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I use conditional aggregation for dashboards because one query returns every tile. Ten small queries each scan the table; one <code>CASE</code>-based query scans it once.</p>
</blockquote>



<h3 class="wp-block-heading">Use CASE in UPDATE Statements</h3>



<p class="wp-block-paragraph"><code>CASE</code> lets one <code>UPDATE</code> apply different values to different rows. </p>



<p class="wp-block-paragraph"><strong>Warning:</strong> <code>UPDATE</code> overwrites data. Take a backup, preview with a <code>SELECT</code>, and use a transaction. See <a href="https://sqlserverguides.com/table-backup-in-sql-server/" target="_blank" rel="noopener">table backup in SQL Server</a>.</p>



<pre class="wp-block-preformatted"><code><em>-- Preview first</em><br>SELECT OrderID, Status,<br>       CASE WHEN Status = 'Pending' AND OrderDate &lt; '2026-10-05'<br>            THEN 'Delayed' ELSE Status END AS NewStatus<br>FROM dbo.Orders;<br><br>BEGIN TRANSACTION;<br><br>UPDATE dbo.Orders<br>SET Status = CASE<br>                 WHEN Status = 'Pending' AND OrderDate &lt; '2026-10-05' THEN 'Delayed'<br>                 ELSE Status<br>             END<br>WHERE Status = 'Pending';<br><br>SELECT @@ROWCOUNT AS RowsUpdated;<br><br><em>-- COMMIT TRANSACTION;   -- after verifying the count</em><br><em>-- ROLLBACK TRANSACTION; -- to undo</em></code></pre>



<p class="wp-block-paragraph">After executing the query above, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="470" src="https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-syntax-in-sql-server-1024x470.jpg" alt="case when syntax in sql server" class="wp-image-24078" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-syntax-in-sql-server-1024x470.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-syntax-in-sql-server-300x138.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-syntax-in-sql-server-1536x704.jpg 1536w, https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-syntax-in-sql-server-768x352.jpg 768w, https://sqlserverguides.com/wp-content/uploads/2026/11/case-when-syntax-in-sql-server.jpg 1570w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">The preview shows exactly what will change. The <code>WHERE Status = 'Pending'</code> limits the rows the update touches, and <code>ELSE Status</code> keeps the original value for rows that do not match. Without the <code>WHERE</code>, SQL Server would rewrite every row, including unchanged ones, which wastes log space and takes more locks. </p>



<p class="wp-block-paragraph">Check <code>@@ROWCOUNT</code> before you commit, and keep the transaction short to avoid blocking other sessions. When the new values come from another table, see <a href="https://sqlserverguides.com/sql-server-update-table-from-another-table/" target="_blank" rel="noopener">updating a table from another table</a>.</p>



<h3 class="wp-block-heading">Pitfalls: Data Types, Evaluation Order, and Division</h3>



<p class="wp-block-paragraph"><strong>Return types must be compatible.</strong> Every <code>THEN</code> and <code>ELSE</code> result in one <code>CASE</code> must resolve to a single data type, chosen by SQL Server&#8217;s <strong>data type precedence</strong>, the ranking that decides which type wins when types are mixed. If one branch returns a number and another returns text, SQL Server tries to convert the text to a number and fails:</p>



<pre class="wp-block-preformatted"><code><em>-- Fails when the ELSE branch is reached: 'Unknown' cannot become an INT</em><br>SELECT CASE WHEN Amount > 100 THEN 1 ELSE 'Unknown' END FROM dbo.Orders;</code></pre>



<p class="wp-block-paragraph">Make the types match, for example by casting the number to text. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/cast-function-in-sql-server/">the CAST function</a> and the related error <a rel="noopener" target="_blank" href="https://sqlserverguides.com/error-converting-data-type-varchar-to-numeric/">converting varchar to numeric</a>.</p>



<p class="wp-block-paragraph"><strong>Guard division, but know the limits.</strong> <code>CASE</code> is the standard way to avoid divide-by-zero:</p>



<pre class="wp-block-preformatted"><code>SELECT CASE WHEN Amount = 0 OR Amount IS NULL THEN NULL<br>            ELSE 100.0 / Amount END AS Ratio<br>FROM dbo.Orders;</code></pre>



<p class="wp-block-paragraph">SQL Server generally evaluates <code>CASE</code> branches in order, but the documentation warns that it does not guarantee evaluation order in every case, especially when aggregates appear inside the expression. For simple division, <code>NULLIF</code> is shorter and safer: <code>100.0 / NULLIF(Amount, 0)</code>.</p>



<p class="wp-block-paragraph"><strong>Nesting has a limit.</strong> SQL Server allows <code>CASE</code> expressions to nest up to 10 levels. If you need that many, move the rules into a lookup table and join to it. A mapping table is easier to change than code; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/join-in-sql-server/">joins in SQL Server</a>.</p>



<p class="wp-block-paragraph"><strong>Know the alternatives.</strong> <code>IIF</code> is shorthand for a two-branch <code>CASE</code>; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-iif-logical-function/">the IIF function</a>. <code>CHOOSE</code> picks from a list by position; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/choose-function-in-sql-server/">the CHOOSE function</a>. Use <code>CASE</code> when you have more than two outcomes or need the clearest logic.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I once reviewed a <code>CASE</code> with twelve nested levels that nobody on the team dared touch. We replaced it with a six-row lookup table, and the next rule change took two minutes.</p>
</blockquote>



<h3 class="wp-block-heading">Use CASE in WHERE and Keep Queries Fast</h3>



<p class="wp-block-paragraph">You can place <code>CASE</code> in a <code>WHERE</code> clause, but you usually should not. Compare these two queries:</p>



<pre class="wp-block-preformatted"><code><em>-- Hard for the optimizer: the column is hidden inside an expression</em><br>SELECT OrderID FROM dbo.Orders<br>WHERE CASE WHEN Status = 'Shipped' THEN 1 ELSE 0 END = 1;<br><br><em>-- Better: a plain, index-friendly condition</em><br>SELECT OrderID FROM dbo.Orders<br>WHERE Status = 'Shipped';</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="398" src="https://sqlserverguides.com/wp-content/uploads/2026/11/sql-server-case-when-in-1024x398.jpg" alt="sql server case when in" class="wp-image-24079" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/sql-server-case-when-in-1024x398.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/sql-server-case-when-in-300x116.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/sql-server-case-when-in-768x298.jpg 768w, https://sqlserverguides.com/wp-content/uploads/2026/11/sql-server-case-when-in.jpg 1311w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">A condition is <strong>sargable</strong> when SQL Server can use an index seek to find matching rows. Wrapping a column in a <code>CASE</code> or function usually makes it non-sargable, which forces a scan. Use ordinary <code>AND</code>/<code>OR</code> conditions in <code>WHERE</code>; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/where-condition-in-sql-server/">the WHERE condition</a>. Reserve <code>CASE</code> for the <code>SELECT</code> list, <code>ORDER BY</code>, and aggregates.</p>



<p class="wp-block-paragraph">When you do filter on a computed bucket often, an index on the base column helps, and a computed column can store the label. Create indexes only after checking the actual execution plan; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-create-index-in-sql-server/">how to create an index in SQL Server</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-query-optimization-tips/">SQL query optimization tips</a>. Every index slows writes and takes storage.</p>



<p class="wp-block-paragraph">In Azure SQL Database, use <strong>Query Store</strong>, which records query run times and plans, to confirm that a rewrite helped.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, <code>CASE</code> itself is rarely the performance problem. The slow part is almost always putting it in <code>WHERE</code> or joining on its result against a big table.</p>
</blockquote>



<h2 class="wp-block-heading">Secure and Test CASE-Based Reports</h2>



<p class="wp-block-paragraph">Reports that bucket customers or flag risk are business logic, so treat them like code.</p>



<ol class="wp-block-list">
<li><strong>Put shared rules in a view or stored procedure</strong> so every report uses the same definition.</li>



<li><strong>Use a least-privilege reporting user.</strong> Grant <code>SELECT</code> on the view only. See <a href="https://sqlserverguides.com/sql-server-grant-select-on-table/" target="_blank" rel="noopener">how to grant SELECT</a>.</li>



<li><strong>Use Microsoft Entra ID authentication</strong> and managed identities instead of SQL passwords in scripts.</li>



<li><strong>Require encrypted connections.</strong> Verify with <code>SELECT encrypt_option FROM sys.dm_exec_connections WHERE session_id = @@SPID;</code>.</li>



<li><strong>Test boundaries.</strong> Include values at each band edge, plus NULLs, in your test data.</li>
</ol>



<pre class="wp-block-preformatted"><code>CREATE USER [orders-report] FROM EXTERNAL PROVIDER;<br>GRANT SELECT ON OBJECT::dbo.vw_OrderSize TO [orders-report];</code></pre>



<p class="wp-block-paragraph"><code>FROM EXTERNAL PROVIDER</code> creates a contained database user mapped to an Entra identity, so no password is stored. The grant allows reading one view. If a bad <code>UPDATE ... CASE</code> ever damages data, Azure SQL Database supports point-in-time restore into a new database, so you can compare and copy back correct values. Confirm your retention window in advance.</p>



<h3 class="wp-block-heading">CASE WHEN Quality Checklist</h3>



<ul class="wp-block-list">
<li><strong>Order your branches.</strong> The first true condition wins, so put narrow rules before broad ones.</li>



<li><strong>Handle NULL explicitly.</strong> Use <code>IS NULL</code>, never <code>= NULL</code>, and put that branch first.</li>



<li><strong>Always add <code>ELSE</code>.</strong> A missing <code>ELSE</code> quietly returns NULL.</li>



<li><strong>Match result types.</strong> Every branch must convert to one compatible data type.</li>



<li><strong>Keep <code>CASE</code> out of <code>WHERE</code>.</strong> Use plain conditions to preserve index seeks.</li>



<li><strong>Preview before updating.</strong> Run the <code>SELECT</code>, use a transaction, and check <code>@@ROWCOUNT</code>.</li>
</ul>



<h3 class="wp-block-heading">Frequently Asked Questions</h3>



<h4 class="wp-block-heading">What is CASE WHEN in SQL Server?</h4>



<p class="wp-block-paragraph"><code>CASE WHEN</code> is a conditional expression that returns different values based on conditions. It evaluates <code>WHEN</code> tests in order and returns the result of the first true one, or the <code>ELSE</code> value if none match. You can use it in <code>SELECT</code>, <code>ORDER BY</code>, <code>GROUP BY</code>, aggregates, and <code>UPDATE</code>.</p>



<h4 class="wp-block-heading">What is the difference between simple and searched CASE?</h4>



<p class="wp-block-paragraph">Simple <code>CASE</code> compares one expression to a list of values for equality. Searched <code>CASE</code> evaluates full Boolean conditions, so it handles ranges, <code>AND</code>/<code>OR</code>, and <code>IS NULL</code>. Searched <code>CASE</code> is more flexible and is the safer default.</p>



<h4 class="wp-block-heading">How do I handle NULL in CASE WHEN?</h4>



<p class="wp-block-paragraph">Use <code>WHEN column IS NULL</code>, not <code>= NULL</code>, and place it first so NULL rows do not fall into <code>ELSE</code>. For simple replacements, <code>COALESCE</code> or <code>ISNULL</code> may be shorter.</p>



<h4 class="wp-block-heading">Can I use CASE WHEN in a WHERE clause?</h4>



<p class="wp-block-paragraph">Yes, but it often prevents index seeks and slows queries. Prefer ordinary conditions with <code>AND</code> and <code>OR</code> in <code>WHERE</code>, and use <code>CASE</code> in the select list, <code>ORDER BY</code>, or aggregates.</p>



<h4 class="wp-block-heading">How many conditions can a CASE statement have?</h4>



<p class="wp-block-paragraph">There is no small fixed limit on the number of <code>WHEN</code> branches, but <code>CASE</code> expressions can nest only up to 10 levels. If you need dozens of rules, move them into a lookup table and join to it.</p>



<p class="wp-block-paragraph">You learned how to use CASE WHEN in SQL Server for labels, custom sorting, conditional totals, and updates, and how to avoid NULL, data type, and performance traps. Put the NULL branch first, always include <code>ELSE</code>, and preview every <code>UPDATE</code> with a <code>SELECT</code> and a transaction before you commit. I hope you found this article helpful.</p>



<p class="wp-block-paragraph">You May Also Like</p>



<ul class="wp-block-list">
<li><a href="https://sqlserverguides.com/sql-select-query-examples/" target="_blank" rel="noopener">SQL SELECT query examples</a></li>



<li><a href="https://sqlserverguides.com/sql-server-top-clause/" target="_blank" rel="noopener">SQL Server TOP clause</a></li>



<li><a href="https://sqlserverguides.com/sql-wildcard-characters/" target="_blank" rel="noopener">SQL wildcard characters</a></li>



<li><a href="https://sqlserverguides.com/how-to-use-if-else-in-sql-server-stored-procedure/" target="_blank" rel="noopener">How to use IF ELSE in a SQL Server stored procedure</a></li>



<li><a href="https://sqlserverguides.com/sql-cheat-sheet/" target="_blank" rel="noopener">SQL cheat sheet</a></li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to Find Percentage of Total in SQL</title>
		<link>https://sqlserverguides.com/how-to-find-percentage-of-total-in-sql-server/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Tue, 03 Nov 2026 17:06:01 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[How to Find Percentage of Total in SQL]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=24061</guid>

					<description><![CDATA[A finance director once forwarded me a screenshot of her new sales dashboard. Every region showed a share of revenue of exactly 0 percent, except the largest one, which showed 100. The query looked fine. It divided each region&#8217;s sales by total sales and multiplied by 100. The columns held whole numbers, and SQL Server ... <a title="How to Find Percentage of Total in SQL" class="read-more" href="https://sqlserverguides.com/how-to-find-percentage-of-total-in-sql-server/" aria-label="Read more about How to Find Percentage of Total in SQL">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A finance director once forwarded me a screenshot of her new sales dashboard. Every region showed a share of revenue of exactly 0 percent, except the largest one, which showed 100. The query looked fine. It divided each region&#8217;s sales by total sales and multiplied by 100. The columns held whole numbers, and SQL Server did what it does with whole numbers: it threw away the decimals before the multiplication.</p>



<p class="wp-block-paragraph">That is the most common trap when you learn <strong>how to find percentage of total in SQL</strong>. The idea is simple: divide each part by the whole. Getting it right means choosing the right way to compute the whole, handling integer math and zero totals, and deciding what &#8220;total&#8221; means after filters and groups. This guide builds the answer step by step in T-SQL, then covers performance and secure reporting in SQL Server and Azure SQL Database.</p>



<h2 class="wp-block-heading">Create a Practice Table</h2>



<p class="wp-block-paragraph">A <strong>schema</strong> is the structure of your tables. Use a small sales table so you can check every result by hand.</p>



<pre class="wp-block-preformatted"><code>CREATE TABLE dbo.Sales (<br>    SalesID  INT IDENTITY(1,1) PRIMARY KEY,<br>    Region   VARCHAR(20)   NOT NULL,<br>    Category VARCHAR(20)   NOT NULL,<br>    Amount   DECIMAL(10,2) NOT NULL<br>);<br><br>INSERT INTO dbo.Sales (Region, Category, Amount)<br>VALUES<br> ('West',  'Hardware', 1200.00),<br> ('West',  'Software',  800.00),<br> ('East',  'Hardware',  600.00),<br> ('East',  'Software', 1400.00),<br> ('South', 'Hardware',  500.00),<br> ('South', 'Software',  500.00);</code></pre>



<p class="wp-block-paragraph">After executing the above query, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="888" height="452" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-percentage-of-total-in-sql.jpg" alt="how to find percentage of total in sql" class="wp-image-24062" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-percentage-of-total-in-sql.jpg 888w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-percentage-of-total-in-sql-766x390.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-percentage-of-total-in-sql-300x153.jpg 300w" sizes="auto, (max-width: 888px) 100vw, 888px" /></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="962" height="477" src="https://sqlserverguides.com/wp-content/uploads/2026/11/sql-how-to-calculate-percentage-of-total.jpg" alt="sql how to calculate percentage of total" class="wp-image-24063" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/sql-how-to-calculate-percentage-of-total.jpg 962w, https://sqlserverguides.com/wp-content/uploads/2026/11/sql-how-to-calculate-percentage-of-total-300x149.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/sql-how-to-calculate-percentage-of-total-766x380.jpg 766w" sizes="auto, (max-width: 962px) 100vw, 962px" /></figure>
</div>


<p class="wp-block-paragraph">Run this only in a test database, because it inserts rows. The grand total is 5,000. Region totals are West 2,000, East 2,000, and South 1,000, so the expected shares are 40, 40, and 20 percent. Category totals are Hardware 2,300 (46 percent) and Software 2,700 (54 percent). Knowing the answers up front lets you verify each query.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, a six-row table with totals you can add in your head catches more percentage bugs than a million-row production table. I always verify the logic on tiny data first.</p>
</blockquote>



<h3 class="wp-block-heading">The Basic Idea and the Integer Division Trap</h3>



<p class="wp-block-paragraph">A percentage of total is <code>part / total * 100</code>. In T-SQL, the data type of the operands decides how division behaves. If both sides are integers, SQL Server performs <strong>integer division</strong> and drops the remainder.</p>



<pre class="wp-block-preformatted"><code>SELECT 1200 / 5000 * 100      AS WrongPercent,   <em>-- 0</em><br>       1200 * 100.0 / 5000    AS RightPercent;   <em>-- 24.000000</em></code></pre>



<p class="wp-block-paragraph">After executing the above query, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="199" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-percentage-of-total-in-sql-server-1024x199.jpg" alt="how to find percentage of total in sql server" class="wp-image-24064" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-percentage-of-total-in-sql-server-1024x199.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-percentage-of-total-in-sql-server-300x58.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-percentage-of-total-in-sql-server-763x148.jpg 763w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-percentage-of-total-in-sql-server.jpg 1047w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">The first expression divides 1200 by 5000 and gets 0 because both are integers, then multiplies 0 by 100. The second multiplies by <code>100.0</code> first, which makes the whole expression decimal. Always start with <code>100.0</code> or cast a value to <code>DECIMAL</code> before dividing. For more on conversions, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/cast-function-in-sql-server/">the CAST function</a>.</p>



<p class="wp-block-paragraph">This is exactly what broke the finance dashboard. In our table <code>Amount</code> is already <code>DECIMAL</code>, but in many real tables the measure is an <code>INT</code> count.</p>



<h3 class="wp-block-heading">How to Find Percentage of Total in SQL With a Subquery</h3>



<p class="wp-block-paragraph">The oldest method computes the total in a separate query. A <strong>scalar subquery</strong> returns a single value and can be used inside another query. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-subquery/">SQL subqueries</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    SalesID,<br>    Region,<br>    Amount,<br>    CAST(Amount * 100.0 / (SELECT SUM(Amount) FROM dbo.Sales) AS DECIMAL(5,2)) AS PctOfTotal<br>FROM dbo.Sales<br>ORDER BY SalesID;</code></pre>



<p class="wp-block-paragraph">After executing the above query, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="337" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-calculate-percentage-of-total-in-sql-1024x337.jpg" alt="how to calculate percentage of total in sql" class="wp-image-24065" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-calculate-percentage-of-total-in-sql-1024x337.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-calculate-percentage-of-total-in-sql-300x99.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-calculate-percentage-of-total-in-sql-1536x506.jpg 1536w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-calculate-percentage-of-total-in-sql-765x252.jpg 765w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-calculate-percentage-of-total-in-sql.jpg 1585w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">The inner <code>SELECT SUM(Amount)</code> calculates 5,000 once, and each row divides by it. Results are 24.00, 16.00, 12.00, 28.00, 10.00, and 10.00. They add up to 100.</p>



<p class="wp-block-paragraph">This approach works on any SQL Server version and is easy to read. The trade-off is that SQL Server reads the table for the main query and again for the total. On small tables that is trivial. On big tables the second scan adds cost. For the <code>SUM</code> function itself, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-sum-function-in-sql-server/">how to use SUM in SQL Server</a>.</p>



<h3 class="wp-block-heading">Use a Window Function for the Cleanest Result</h3>



<p class="wp-block-paragraph">A <strong>window function</strong> calculates across a set of rows without collapsing them. With <code>SUM(Amount) OVER ()</code>, the empty parentheses mean &#8220;the whole result set.&#8221; Read more in <a rel="noopener" target="_blank" href="https://sqlserverguides.com/window-functions-in-sql-server/">window functions in SQL Server</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-over-clause/">the OVER clause</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    SalesID,<br>    Region,<br>    Amount,<br>    CAST(Amount * 100.0 / SUM(Amount) OVER () AS DECIMAL(5,2)) AS PctOfTotal<br>FROM dbo.Sales<br>ORDER BY SalesID;</code></pre>



<p class="wp-block-paragraph">After executing the above query, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="360" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-percentage-of-total-sql-1024x360.jpg" alt="how to find percentage of total sql" class="wp-image-24066" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-percentage-of-total-sql-1024x360.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-percentage-of-total-sql-300x105.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-percentage-of-total-sql-766x269.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-percentage-of-total-sql.jpg 1444w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">This returns the same numbers as the subquery version. SQL Server calculates the total in the same pass and attaches it to every row. The query is shorter, and it avoids repeating the table name. This is the method I recommend for most reports.</p>



<h3 class="wp-block-heading">Protect Against Divide-by-Zero</h3>



<p class="wp-block-paragraph">If the total is zero, for example when a filter returns no sales or all amounts net to zero, you get a divide-by-zero error. Use <code>NULLIF</code>, which returns NULL when two values are equal, so the division returns NULL instead of failing. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/nullif-vs-coalesce/">NULLIF vs. COALESCE</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    Region,<br>    Amount,<br>    CAST(Amount * 100.0 / NULLIF(SUM(Amount) OVER (), 0) AS DECIMAL(5,2)) AS PctOfTotal<br>FROM dbo.Sales;</code></pre>



<p class="wp-block-paragraph">After executing the above query, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="267" src="https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-percentage-of-total-in-sql-1024x267.jpg" alt="calculate percentage of total in sql" class="wp-image-24067" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-percentage-of-total-in-sql-1024x267.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-percentage-of-total-in-sql-767x200.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-percentage-of-total-in-sql-300x78.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-percentage-of-total-in-sql-1536x400.jpg 1536w, https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-percentage-of-total-in-sql.jpg 1581w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">When the total is 0, the denominator becomes NULL and the percentage is NULL. Decide whether the report should show NULL, 0, or a message. Wrap with <code>COALESCE</code> if you want zero.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I once watched a nightly report fail because a single region had no sales on a holiday. Adding <code>NULLIF</code> to every denominator is now a habit, not an afterthought.</p>
</blockquote>



<h3 class="wp-block-heading">Percentage of Total by Group</h3>



<p class="wp-block-paragraph">Business questions usually ask for shares by region or category, not by row. Combine <code>GROUP BY</code> with a window function over the grouped results. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-group-by-clause-in-sql-server/">how to use GROUP BY in SQL Server</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    Region,<br>    SUM(Amount) AS RegionSales,<br>    CAST(SUM(Amount) * 100.0 / SUM(SUM(Amount)) OVER () AS DECIMAL(5,2)) AS PctOfTotal<br>FROM dbo.Sales<br>GROUP BY Region<br>ORDER BY RegionSales DESC;</code></pre>



<p class="wp-block-paragraph">After executing the above query, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="228" src="https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Percentage-of-Total-in-SQL-server-1024x228.jpg" alt="Find Percentage of Total in SQL server" class="wp-image-24068" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Percentage-of-Total-in-SQL-server-1024x228.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Percentage-of-Total-in-SQL-server-300x67.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Percentage-of-Total-in-SQL-server-767x171.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Percentage-of-Total-in-SQL-server-1536x342.jpg 1536w, https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Percentage-of-Total-in-SQL-server.jpg 1602w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">The inner <code>SUM(Amount)</code> is the group total. The outer <code>SUM(... ) OVER ()</code> adds those group totals together to get the grand total. This works because window functions run after <code>GROUP BY</code>. The result is West 40.00, East 40.00, and South 20.00.</p>



<p class="wp-block-paragraph">The nested <code>SUM(SUM(...))</code> looks odd the first time you see it. If it confuses your team, use a <strong>CTE</strong> (common table expression), a named temporary result you can query immediately. Compare it with a temp table in <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-cte-vs-temp-table/">CTE vs. temp table</a>.</p>



<pre class="wp-block-preformatted"><code>WITH RegionTotals AS (<br>    SELECT Region, SUM(Amount) AS RegionSales<br>    FROM dbo.Sales<br>    GROUP BY Region<br>)<br>SELECT<br>    Region,<br>    RegionSales,<br>    CAST(RegionSales * 100.0 / SUM(RegionSales) OVER () AS DECIMAL(5,2)) AS PctOfTotal<br>FROM RegionTotals<br>ORDER BY RegionSales DESC;</code></pre>



<p class="wp-block-paragraph">After executing the above query, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="371" src="https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-percentage-of-total-sql-server-1024x371.jpg" alt="calculate percentage of total sql server" class="wp-image-24069" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-percentage-of-total-sql-server-1024x371.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-percentage-of-total-sql-server-300x109.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-percentage-of-total-sql-server-1536x557.jpg 1536w, https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-percentage-of-total-sql-server-767x278.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-percentage-of-total-sql-server.jpg 1586w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">It returns the same result in two clear steps: aggregate first, then calculate the share.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I use the CTE version when the query will be maintained by someone else. Two readable steps beat one clever expression that nobody wants to touch.</p>
</blockquote>



<h3 class="wp-block-heading">Percentage Within a Group</h3>



<p class="wp-block-paragraph">Sometimes the &#8220;whole&#8221; is a subgroup. For example, you want each category&#8217;s share within its region. Add <code>PARTITION BY</code> to the window, which restarts the calculation for each group. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-partition-by/">PARTITION BY</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    Region,<br>    Category,<br>    SUM(Amount) AS CategorySales,<br>    CAST(SUM(Amount) * 100.0<br>         / SUM(SUM(Amount)) OVER (PARTITION BY Region) AS DECIMAL(5,2)) AS PctOfRegion<br>FROM dbo.Sales<br>GROUP BY Region, Category<br>ORDER BY Region, Category;</code></pre>



<p class="wp-block-paragraph">With <code>PARTITION BY Region</code>, the denominator is that region&#8217;s total. The output is East Hardware 30.00 and Software 70.00, South 50.00 and 50.00, and West Hardware 60.00 and Software 40.00. Each region&#8217;s percentages add to 100.</p>



<p class="wp-block-paragraph">Be clear about which total you are using. The three versions you may need are percent of the grand total, percent of the group, and percent of a filtered set. Label the column so readers know, for example <code>PctOfRegion</code> rather than just <code>Pct</code>.</p>



<h3 class="wp-block-heading">Combine Percentages With Filters Correctly</h3>



<p class="wp-block-paragraph">Filters change the denominator. This catches many people. A <code>WHERE</code> clause runs before window functions, so the total only includes the filtered rows.</p>



<pre class="wp-block-preformatted"><code><em>-- Percent of Hardware sales only (denominator excludes Software)</em><br>SELECT Region,<br>       SUM(Amount) AS HardwareSales,<br>       CAST(SUM(Amount) * 100.0 / SUM(SUM(Amount)) OVER () AS DECIMAL(5,2)) AS PctOfHardware<br>FROM dbo.Sales<br>WHERE Category = 'Hardware'<br>GROUP BY Region;</code></pre>



<p class="wp-block-paragraph">Here the total is 2,300, not 5,000. If you want each region&#8217;s Hardware sales as a share of all sales, calculate the percentage first and filter afterward:</p>



<pre class="wp-block-preformatted"><code>WITH Shares AS (<br>    SELECT Region, Category, Amount,<br>           Amount * 100.0 / SUM(Amount) OVER () AS PctOfGrandTotal<br>    FROM dbo.Sales<br>)<br>SELECT Region, Category, CAST(PctOfGrandTotal AS DECIMAL(5,2)) AS PctOfGrandTotal<br>FROM Shares<br>WHERE Category = 'Hardware';</code></pre>



<p class="wp-block-paragraph">The CTE calculates shares against all 5,000, then the outer <code>WHERE</code> filters the rows. The difference between <code>WHERE</code> before and after the window is explained further in <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-having-vs-where/">HAVING vs. WHERE</a>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> When a stakeholder says &#8220;the numbers don&#8217;t add up,&#8221; I ask one question: &#8220;Percent of what?&#8221; Nine times out of ten the filter changed the denominator.</p>
</blockquote>



<h3 class="wp-block-heading">Add a Cumulative Percentage</h3>



<p class="wp-block-paragraph">A running total of percentages shows how fast sales accumulate, which supports Pareto-style questions like &#8220;which regions make up 80 percent of revenue?&#8221;</p>



<pre class="wp-block-preformatted"><code>WITH RegionTotals AS (<br>    SELECT Region, SUM(Amount) AS RegionSales<br>    FROM dbo.Sales<br>    GROUP BY Region<br>)<br>SELECT<br>    Region,<br>    RegionSales,<br>    CAST(SUM(RegionSales) OVER (ORDER BY RegionSales DESC, Region<br>                                ROWS UNBOUNDED PRECEDING) * 100.0<br>         / SUM(RegionSales) OVER () AS DECIMAL(5,2)) AS CumulativePct<br>FROM RegionTotals<br>ORDER BY RegionSales DESC, Region;</code></pre>



<p class="wp-block-paragraph">The window with <code>ORDER BY</code> and <code>ROWS UNBOUNDED PRECEDING</code> adds up values from the first row to the current one. With ties between West and East, the tie-breaker <code>Region</code> keeps the order stable. The result is West 40.00, East 80.00, and South 100.00. Always specify the <code>ROWS</code> frame explicitly; the default frame can behave differently with ties and is usually slower.</p>



<h3 class="wp-block-heading">Format and Round the Results</h3>



<p class="wp-block-paragraph">Keep percentages numeric in the database and format them in the application when possible. If you need text in T-SQL, use <code>CAST</code> and <code>CONCAT</code>:</p>



<pre class="wp-block-preformatted"><code>SELECT Region,<br>       CONCAT(CAST(SUM(Amount) * 100.0 / SUM(SUM(Amount)) OVER () AS DECIMAL(5,1)), '%') AS Share<br>FROM dbo.Sales<br>GROUP BY Region;</code></pre>



<p class="wp-block-paragraph">For more on rounding, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/format-number-to-2-decimal-places-in-sql-server/">formatting numbers to two decimal places</a>. Be aware that rounded values may not sum to exactly 100. Three shares of 33.33 add to 99.99. That is normal. If an exact 100 is required, adjust the largest item or round only at display time and explain that totals may differ slightly.</p>



<h3 class="wp-block-heading">Make It Fast and Reusable</h3>



<p class="wp-block-paragraph">A window over the whole result must see every row to compute the total, so these queries scan or aggregate the data they use. Three habits keep them fast:</p>



<ul class="wp-block-list">
<li><strong>Aggregate before windowing.</strong> The grouped version works on a few rows after <code>GROUP BY</code>, not thousands of detail rows.</li>



<li><strong>Index the columns involved.</strong> An index on <code>Region</code> that includes <code>Amount</code> lets SQL Server read a narrow structure. See <a href="https://sqlserverguides.com/how-to-create-index-in-sql-server/" target="_blank" rel="noopener">how to create an index in SQL Server</a>. Check the execution plan first, because every index slows inserts and updates.</li>



<li><strong>Pre-aggregate for dashboards.</strong> If a report reads hundreds of millions of rows, store daily or monthly totals in a summary table and calculate percentages from that.</li>
</ul>



<p class="wp-block-paragraph">For large analytic tables, a <strong>columnstore index</strong>, which stores data by column and compresses it, can speed aggregations. Test it against your workload first. For more tips, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-query-optimization-tips/">SQL query optimization tips</a>.</p>



<p class="wp-block-paragraph">Package the logic in a <strong>view</strong> so every report uses the same definition of &#8220;percent of total.&#8221; See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-views/">SQL Server views</a>.</p>



<pre class="wp-block-preformatted"><code>CREATE OR ALTER VIEW dbo.vw_RegionShare<br>AS<br>SELECT<br>    Region,<br>    SUM(Amount) AS RegionSales,<br>    CAST(SUM(Amount) * 100.0 / NULLIF(SUM(SUM(Amount)) OVER (), 0) AS DECIMAL(5,2)) AS PctOfTotal<br>FROM dbo.Sales<br>GROUP BY Region;</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, definitions drift when ten reports each calculate their own percentage. A single view makes the denominator an agreed business rule.</p>
</blockquote>



<h3 class="wp-block-heading">Frequently Asked Questions</h3>



<h4 class="wp-block-heading">How do I calculate percentage of total in SQL Server?</h4>



<p class="wp-block-paragraph">Divide each value by the total and multiply by 100.0, for example <code>Amount * 100.0 / SUM(Amount) OVER ()</code>. The empty <code>OVER ()</code> makes the window cover the whole result, so every row gets the grand total.</p>



<h4 class="wp-block-heading">Why does my percentage of total return 0?</h4>



<p class="wp-block-paragraph">You are probably dividing two integers, and SQL Server drops the decimals. Multiply by <code>100.0</code> before dividing or cast one value to <code>DECIMAL</code>.</p>



<h4 class="wp-block-heading">How do I calculate percentage within each group?</h4>



<p class="wp-block-paragraph">Use <code>PARTITION BY</code> in the window, such as <code>SUM(Amount) OVER (PARTITION BY Region)</code>. The denominator then becomes each group&#8217;s total, and the percentages in each group add to 100.</p>



<h4 class="wp-block-heading">How do I avoid divide-by-zero errors?</h4>



<p class="wp-block-paragraph">Wrap the denominator in <code>NULLIF(total, 0)</code>. When the total is zero, the result becomes NULL instead of an error, and you can replace it with <code>COALESCE</code> if you prefer zero.</p>



<h4 class="wp-block-heading">Is a subquery or a window function better for percent of total?</h4>



<p class="wp-block-paragraph">Both give the same answer. The window function is shorter and calculates the total in the same pass, while a scalar subquery works on every version and is easy to read. For large tables, compare execution plans.</p>



<p class="wp-block-paragraph">You learned how to find percentage of total in SQL using subqueries, window functions, grouped totals, and <code>PARTITION BY</code>, and how to avoid integer division and divide-by-zero errors. Name your denominator clearly, verify results against a hand-calculated total, and give report users read-only access through a view. I hope you found this article helpful.</p>



<p class="wp-block-paragraph">You May Also Like</p>



<ul class="wp-block-list">
<li><a href="https://sqlserverguides.com/how-to-use-avg-function-in-sql-server/" target="_blank" rel="noopener">How to use the AVG function in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/how-to-use-count-function-in-sql-server/" target="_blank" rel="noopener">How to use the COUNT function in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/sql-rank-vs-dense-rank/" target="_blank" rel="noopener">RANK vs. DENSE_RANK in SQL</a></li>



<li><a href="https://sqlserverguides.com/sql-server-floor/" target="_blank" rel="noopener">SQL Server FLOOR function</a></li>



<li><a href="https://sqlserverguides.com/sql-server-group-by-date/" target="_blank" rel="noopener">How to group by date in SQL Server</a></li>
</ul>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to Find NULL Values in a Table in SQL Server</title>
		<link>https://sqlserverguides.com/how-to-find-null-values-in-a-table-in-sql-server/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Tue, 03 Nov 2026 12:07:54 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[How to Find NULL Values in a Table in SQL Server]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=24053</guid>

					<description><![CDATA[A marketing analyst once asked me why the &#8220;customers without an email&#8221; report returned zero rows. The company had 12,000 customers and a visible gap in its email data. The query read WHERE Email = NULL. It ran without errors, returned nothing, and the campaign went out to the wrong list. That mistake is one ... <a title="How to Find NULL Values in a Table in SQL Server" class="read-more" href="https://sqlserverguides.com/how-to-find-null-values-in-a-table-in-sql-server/" aria-label="Read more about How to Find NULL Values in a Table in SQL Server">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A marketing analyst once asked me why the &#8220;customers without an email&#8221; report returned zero rows. The company had 12,000 customers and a visible gap in its email data. The query read <code>WHERE Email = NULL</code>. It ran without errors, returned nothing, and the campaign went out to the wrong list.</p>



<p class="wp-block-paragraph">That mistake is one of the most common in T-SQL, and it is why learning <strong>how to find null values in a table in SQL Server</strong> matters. The right operator is only the start. You also need to count NULLs per column, tell them apart from empty strings, find them across an entire table, and decide what to do next. This guide covers each of those tasks with T-SQL, then shows how to speed up NULL searches and run them safely in SQL Server or Azure SQL Database.</p>



<h2 class="wp-block-heading">What NULL Means in SQL Server</h2>



<p class="wp-block-paragraph">A <strong>NULL</strong> is a marker for a missing or unknown value. It is not zero, not an empty string, and not a space. Because NULL means &#8220;unknown,&#8221; SQL Server uses <strong>three-valued logic</strong>: a comparison can be true, false, or unknown. Any normal comparison with NULL, such as <code>Email = NULL</code> or <code>Email &lt;&gt; 'a@b.com'</code>, evaluates to unknown, and a <code>WHERE</code> clause only keeps rows that evaluate to true. That is why the analyst&#8217;s query returned nothing.</p>



<p class="wp-block-paragraph">For a deeper look, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-null/">how SQL handles NULL</a>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, most NULL bugs come from treating NULL like a value. I say &#8220;unknown&#8221; out loud when I read a query, and it makes the wrong operators stand out.</p>
</blockquote>



<h3 class="wp-block-heading">Create a Practice Table</h3>



<p class="wp-block-paragraph">Test on a small table with deliberate edge cases. A <strong>schema</strong> is the structure of your tables, and sample rows that include NULLs, empty strings, and the text &#8220;NULL&#8221; will show you every trap.</p>



<pre class="wp-block-preformatted"><code>CREATE TABLE dbo.Customers (<br>    CustomerID    INT IDENTITY(1,1) PRIMARY KEY,<br>    FullName      VARCHAR(100) NOT NULL,<br>    Email         VARCHAR(150) NULL,<br>    Phone         VARCHAR(30)  NULL,<br>    Region        VARCHAR(30)  NULL,<br>    LastOrderDate DATE         NULL<br>);<br><br>INSERT INTO dbo.Customers (FullName, Email, Phone, Region, LastOrderDate)<br>VALUES<br> ('Ava Chen',    'ava@example.com', '555-0101', 'West',  '2026-10-02'),<br> ('Ben Ortiz',   NULL,              '555-0102', 'East',  '2026-09-18'),<br> ('Cara Singh',  '',                NULL,       'West',  NULL),<br> ('Dan Weber',   'NULL',            '555-0104', NULL,    '2026-08-30'),<br> ('Eli Brooks',  NULL,              NULL,       NULL,    NULL);</code></pre>



<p class="wp-block-paragraph">After executing the query above, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="962" height="546" src="https://sqlserverguides.com/wp-content/uploads/2026/11/Find-NULL-Values-in-a-Table-in-SQL-Server.jpg" alt="" class="wp-image-24054" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/Find-NULL-Values-in-a-Table-in-SQL-Server.jpg 962w, https://sqlserverguides.com/wp-content/uploads/2026/11/Find-NULL-Values-in-a-Table-in-SQL-Server-300x170.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/Find-NULL-Values-in-a-Table-in-SQL-Server-766x435.jpg 766w" sizes="auto, (max-width: 962px) 100vw, 962px" /></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="430" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-null-values-in-table-sql-1024x430.jpg" alt="how to find null values in table sql" class="wp-image-24055" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-null-values-in-table-sql-1024x430.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-null-values-in-table-sql-300x126.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-null-values-in-table-sql-767x322.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-null-values-in-table-sql.jpg 1375w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">Run this only in a test database, because it inserts rows. Notice that Cara&#8217;s email is an empty string, Dan&#8217;s email is the five-letter text <code>NULL</code>, and Eli has missing values in four columns. Only Ben&#8217;s and Eli&#8217;s emails are real NULLs.</p>



<h3 class="wp-block-heading">How to Find NULL Values in a Table in SQL Server With IS NULL</h3>



<p class="wp-block-paragraph">Use <code>IS NULL</code> to find rows where a column is missing, and <code>IS NOT NULL</code> for rows that have a value. These are the only comparison operators that work with NULL. See also the <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-where-clause-tutorial/">WHERE clause tutorial</a>.</p>



<pre class="wp-block-preformatted"><code><em>-- Customers with no email on file</em><br>SELECT CustomerID, FullName, Email<br>FROM dbo.Customers<br>WHERE Email IS NULL;</code></pre>



<p class="wp-block-paragraph">After executing the query above, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="736" height="315" src="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-NULL-Values-in-a-Table-in-SQL-Server.jpg" alt="How to Find NULL Values in a Table in SQL Server" class="wp-image-24056" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-NULL-Values-in-a-Table-in-SQL-Server.jpg 736w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-NULL-Values-in-a-Table-in-SQL-Server-300x128.jpg 300w" sizes="auto, (max-width: 736px) 100vw, 736px" /></figure>
</div>


<p class="wp-block-paragraph">This returns Ben and Eli. It does not return Cara (empty string) or Dan (text &#8220;NULL&#8221;), which are values, not NULLs. To list customers who do have an email:</p>



<pre class="wp-block-preformatted"><code>SELECT CustomerID, FullName, Email<br>FROM dbo.Customers<br>WHERE Email IS NOT NULL;</code></pre>



<p class="wp-block-paragraph">After executing the query above, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="743" height="325" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-null-values-in-sql-table.jpg" alt="how to find null values in sql table" class="wp-image-24057" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-null-values-in-sql-table.jpg 743w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-null-values-in-sql-table-300x131.jpg 300w" sizes="auto, (max-width: 743px) 100vw, 743px" /></figure>
</div>


<p class="wp-block-paragraph">You can combine tests. To find customers missing both email and phone:</p>



<pre class="wp-block-preformatted"><code>SELECT CustomerID, FullName<br>FROM dbo.Customers<br>WHERE Email IS NULL<br>  AND Phone IS NULL;</code></pre>



<p class="wp-block-paragraph">After executing the query above, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="660" height="296" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-null-values-in-a-table-in-sql.jpg" alt="how to find null values in a table in sql" class="wp-image-24058" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-null-values-in-a-table-in-sql.jpg 660w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-null-values-in-a-table-in-sql-300x135.jpg 300w" sizes="auto, (max-width: 660px) 100vw, 660px" /></figure>
</div>


<p class="wp-block-paragraph">Only Eli matches. Use <code>OR</code> to find rows missing either one.</p>



<h3 class="wp-block-heading">The Wrong Way: <code>= NULL</code></h3>



<pre class="wp-block-preformatted"><code><em>-- Returns no rows: comparison with NULL is never true</em><br>SELECT * FROM dbo.Customers WHERE Email = NULL;</code></pre>



<p class="wp-block-paragraph">This runs without error but returns nothing. SQL Server has a legacy setting called <code>ANSI_NULLS</code> that changes this behavior, but turning it off is deprecated and risky because it makes your code behave differently from standard SQL. Always use <code>IS NULL</code>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I add a code review rule: any <code>= NULL</code> or <code>&lt;&gt; NULL</code> is rejected on sight. It has caught real bugs in reports that had been wrong for months.</p>
</blockquote>



<h3 class="wp-block-heading">Find Empty Strings and Fake NULLs Too</h3>



<p class="wp-block-paragraph">Real data is messy. Some systems store missing values as empty strings, spaces, or the text &#8220;NULL.&#8221; If you only search for real NULLs, you miss those. Check all three:</p>



<pre class="wp-block-preformatted"><code>SELECT CustomerID, FullName, Email<br>FROM dbo.Customers<br>WHERE Email IS NULL<br>   OR LTRIM(RTRIM(Email)) = ''<br>   OR UPPER(LTRIM(RTRIM(Email))) = 'NULL';</code></pre>



<p class="wp-block-paragraph">This catches true NULLs, blank or whitespace-only values, and the literal text &#8220;NULL.&#8221; The function <code>LTRIM(RTRIM(...))</code> removes spaces on both sides. The query now returns Ben, Cara, Dan, and Eli.</p>



<p class="wp-block-paragraph">Another approach is <code>NULLIF</code>, which returns NULL when two values are equal. It turns blanks into NULLs for a quick test; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/nullif-vs-coalesce/">NULLIF vs. COALESCE</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT CustomerID, FullName<br>FROM dbo.Customers<br>WHERE NULLIF(LTRIM(RTRIM(Email)), '') IS NULL;</code></pre>



<p class="wp-block-paragraph">This treats empty and whitespace-only emails as missing. Be aware that wrapping a column in functions stops SQL Server from using a normal index seek on that column, which matters on large tables. We cover that in the performance section.</p>



<h3 class="wp-block-heading">Count NULLs in One Column or Across a Table</h3>



<p class="wp-block-paragraph">Counting reveals how serious a data quality problem is. <code>COUNT(*)</code> counts all rows, while <code>COUNT(column)</code> counts only non-NULL values. The difference is the NULL count. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-count-function-in-sql-server/">the COUNT function</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    COUNT(*)                          AS TotalRows,<br>    COUNT(Email)                      AS EmailsPresent,<br>    COUNT(*) - COUNT(Email)           AS EmailNulls,<br>    SUM(CASE WHEN Phone IS NULL THEN 1 ELSE 0 END)  AS PhoneNulls,<br>    SUM(CASE WHEN Region IS NULL THEN 1 ELSE 0 END) AS RegionNulls<br>FROM dbo.Customers;</code></pre>



<p class="wp-block-paragraph">With the sample data, there are 5 rows, 3 emails present (including the empty string and the text), 2 NULL emails, 2 NULL phones, and 2 NULL regions. The <code>SUM(CASE ...)</code> pattern counts rows that meet a condition. For more on <code>CASE</code>, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/case-statement-in-sql-server/">the CASE statement</a>.</p>



<h3 class="wp-block-heading">Check Every Nullable Column Automatically</h3>



<p class="wp-block-paragraph">On a table with 40 columns, typing each one is slow and error-prone. Use the system catalog to build the query. The <code>sys.columns</code> view lists each column and whether it allows NULL. To explore a table&#8217;s columns manually, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-query-to-get-column-names-from-table-in-sql-server/">getting column names from a table</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/describe-table-in-sql-server/">describing a table</a>.</p>



<pre class="wp-block-preformatted"><code>DECLARE @sql NVARCHAR(MAX);<br><br>SELECT @sql = N'SELECT COUNT(*) AS TotalRows, '<br>    + STRING_AGG(<br>        CAST(N'SUM(CASE WHEN ' + QUOTENAME(c.name) + N' IS NULL THEN 1 ELSE 0 END) AS '<br>             + QUOTENAME(c.name + N'_Nulls') AS NVARCHAR(MAX)),<br>        N', ')<br>    + N' FROM dbo.Customers;'<br>FROM sys.columns AS c<br>WHERE c.object_id = OBJECT_ID(N'dbo.Customers')<br>  AND c.is_nullable = 1;<br><br>EXEC sys.sp_executesql @sql;</code></pre>



<p class="wp-block-paragraph">Here is what happens:</p>



<ul class="wp-block-list">
<li><strong><code>sys.columns</code></strong> returns one row per column, and <code>is_nullable = 1</code> keeps only columns that can hold NULL. A <code>NOT NULL</code> column can never contain one, so counting it is wasteful.</li>



<li><strong><code>QUOTENAME</code></strong> wraps names in brackets and escapes special characters, which prevents broken SQL and <strong>SQL injection</strong>, an attack where untrusted text changes the query.</li>



<li><strong><code>STRING_AGG</code></strong> joins the generated pieces with commas. It requires SQL Server 2017 or later and is available in Azure SQL Database. The <code>CAST</code> to <code>NVARCHAR(MAX)</code> prevents truncation on wide tables.</li>



<li><strong><code>sp_executesql</code></strong> runs the generated statement. This is <strong>dynamic SQL</strong>, which builds a statement as text at run time; see <a href="https://sqlserverguides.com/dynamic-sql/" target="_blank" rel="noopener">dynamic SQL</a> for the risks and benefits.</li>
</ul>



<p class="wp-block-paragraph">Replace <code>dbo.Customers</code> with your own table. For a variable table name, validate it against <code>sys.tables</code> first and never concatenate raw user input.</p>



<p class="wp-block-paragraph">To find which tables contain a column you care about before you scan, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-find-table-with-column-name/">finding a table by column name</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-check-if-column-exists/">checking if a column exists</a>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I run this catalog-driven count on every new data source before I trust it. A column that is 90 percent NULL usually means a broken import, not a real business fact.</p>
</blockquote>



<h3 class="wp-block-heading">Handle NULLs in Results and Joins</h3>



<p class="wp-block-paragraph">Finding NULLs is half the job. The other half is stopping them from breaking your logic.</p>



<p class="wp-block-paragraph"><strong>Replace NULLs in output.</strong> Use <code>ISNULL</code>, which takes two arguments, or <code>COALESCE</code>, which accepts a list and returns the first non-NULL. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/isnull-function-in-sql-server/">ISNULL</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-coalesce-function/">COALESCE</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT FullName,<br>       COALESCE(Email, '(no email)') AS ContactEmail,<br>       IIF(Phone IS NULL, 'Missing', 'On file') AS PhoneStatus<br>FROM dbo.Customers;</code></pre>



<p class="wp-block-paragraph"><code>COALESCE</code> is standard SQL and works with more than two values. <code>ISNULL</code> is T-SQL specific and takes the data type of the first argument, which can silently truncate a longer replacement. For simple branching, <code>IIF</code> is a short form of <code>CASE</code>; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-iif-logical-function/">the IIF function</a>.</p>



<p class="wp-block-paragraph"><strong>Watch out for <code>NOT IN</code>.</strong> If a subquery returns even one NULL, <code>NOT IN</code> returns no rows at all, because the comparison with NULL is unknown.</p>



<pre class="wp-block-preformatted"><code><em>-- Dangerous if the subquery can return NULL</em><br>SELECT CustomerID FROM dbo.Customers<br>WHERE CustomerID NOT IN (SELECT CustomerID FROM dbo.Orders);</code></pre>



<p class="wp-block-paragraph">Use <code>NOT EXISTS</code> or a left join with <code>IS NULL</code> instead. With a <strong>left join</strong>, which keeps every row from the left table, unmatched right-side columns come back as NULL; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-left-join/">SQL LEFT JOIN</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT c.CustomerID, c.FullName<br>FROM dbo.Customers AS c<br>LEFT JOIN dbo.Orders AS o ON o.CustomerID = c.CustomerID<br>WHERE o.OrderID IS NULL;</code></pre>



<p class="wp-block-paragraph">This finds customers with no orders. Test for NULL on the right table&#8217;s key column, which is never NULL for a real match. Also remember that two NULLs are not equal in a join condition, so rows with NULL join keys never match.</p>



<p class="wp-block-paragraph"><strong>Aggregates ignore NULLs.</strong> <code>AVG</code>, <code>SUM</code>, and <code>COUNT(column)</code> skip them. An average over a column that is half NULL covers only the known values, which may mislead readers.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I once debugged a revenue report where <code>NOT IN</code> quietly returned an empty list after a single NULL appeared in a lookup table. Since then I default to <code>NOT EXISTS</code>.</p>
</blockquote>



<h3 class="wp-block-heading">Fix the Data: Defaults, Constraints, and Cleanup</h3>



<p class="wp-block-paragraph">Once you know where the NULLs are, decide whether they belong. A <strong>constraint</strong> is a rule SQL Server enforces on data; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-constraints/">SQL constraints</a>. If a column should always have a value, make it <code>NOT NULL</code>, but clean the data first or the change will fail. Read <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-change-column-to-not-null/">how to change a column to NOT NULL</a>.</p>



<p class="wp-block-paragraph"><strong>Warning:</strong> the update below overwrites data. Back up first, run it in a transaction, and check the row count. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/table-backup-in-sql-server/">table backup in SQL Server</a>.</p>



<pre class="wp-block-preformatted"><code><em>-- Preview what will change</em><br>SELECT CustomerID, Region<br>FROM dbo.Customers<br>WHERE Region IS NULL;<br><br>BEGIN TRANSACTION;<br><br>UPDATE dbo.Customers<br>SET Region = 'Unknown'<br>WHERE Region IS NULL;<br><br>SELECT @@ROWCOUNT AS RowsUpdated;<br><br><em>-- COMMIT TRANSACTION;  -- run after you verify the count</em><br><em>-- ROLLBACK TRANSACTION; -- or undo</em></code></pre>



<p class="wp-block-paragraph">The preview <code>SELECT</code> uses the same <code>WHERE</code> clause, so you see exactly which rows will change. <code>@@ROWCOUNT</code> shows how many rows the last statement affected. Leaving the transaction open holds locks, so commit or roll back promptly.</p>



<p class="wp-block-paragraph">Replacing NULL with a made-up value like &#8216;Unknown&#8217; is a design choice. Sometimes NULL is the honest answer, and a placeholder can distort counts and averages. Decide with the business owner, and document the rule.</p>



<p class="wp-block-paragraph">For broader design guidance, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-database-design-best-practices/">SQL database design best practices</a>.</p>



<h3 class="wp-block-heading">Make NULL Searches Fast</h3>



<p class="wp-block-paragraph">A search for NULLs is only slow if SQL Server must read every row. Two habits help.</p>



<p class="wp-block-paragraph"><strong>Keep the column bare.</strong> This query is <strong>sargable</strong>, meaning SQL Server can use an index seek:</p>



<pre class="wp-block-preformatted"><code>WHERE LastOrderDate IS NULL</code></pre>



<p class="wp-block-paragraph">This one is not, because the function hides the column from the index:</p>



<pre class="wp-block-preformatted"><code>WHERE ISNULL(LastOrderDate, '1900-01-01') = '1900-01-01'</code></pre>



<p class="wp-block-paragraph"><strong>Use a filtered index</strong> if you often search for rare NULLs. A <strong>filtered index</strong> covers only rows that meet a condition, so it stays small. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-create-index-in-sql-server/">how to create an index in SQL Server</a>.</p>



<pre class="wp-block-preformatted"><code>CREATE NONCLUSTERED INDEX IX_Customers_NoEmail<br>ON dbo.Customers (CustomerID)<br>WHERE Email IS NULL;</code></pre>



<p class="wp-block-paragraph">This index lists only customers with no email, which suits a recurring &#8220;fix missing emails&#8221; report. The trade-off is extra storage and a small write cost. If NULLs are common, such as half the table, an index on them rarely helps. Check the actual execution plan first, and keep statistics fresh; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/update-stats-on-table-sql-server/">updating statistics on a table</a>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, nobody notices a missing index on a NULL check until the report grows from thousands of rows to millions. I test NULL queries on a table copy sized like production.</p>
</blockquote>



<h3 class="wp-block-heading">NULL Audit Checklist</h3>



<ul class="wp-block-list">
<li><strong>Use <code>IS NULL</code>, never <code>= NULL</code>.</strong> Comparisons with NULL are never true.</li>



<li><strong>Look beyond real NULLs.</strong> Check for empty strings, spaces, and placeholder text such as &#8220;NULL.&#8221;</li>



<li><strong>Count per column.</strong> Use <code>COUNT(*) - COUNT(column)</code> or <code>SUM(CASE ...)</code>, and generate queries from <code>sys.columns</code> for wide tables.</li>



<li><strong>Avoid <code>NOT IN</code> with nullable subqueries.</strong> Prefer <code>NOT EXISTS</code> or a left join.</li>



<li><strong>Back up before cleanup.</strong> Preview, use a transaction, and check <code>@@ROWCOUNT</code>.</li>



<li><strong>Limit access.</strong> Give audit jobs <code>SELECT</code> through an Entra identity over encrypted connections.</li>
</ul>



<h3 class="wp-block-heading">Frequently Asked Questions</h3>



<h4 class="wp-block-heading">How do I find NULL values in a SQL Server table?</h4>



<p class="wp-block-paragraph">Use <code>WHERE column_name IS NULL</code>. To find rows that have a value, use <code>IS NOT NULL</code>. Do not use <code>= NULL</code>, because it never returns rows.</p>



<h4 class="wp-block-heading">How do I count NULL values in a column?</h4>



<p class="wp-block-paragraph">Use <code>COUNT(*) - COUNT(column_name)</code> or <code>SUM(CASE WHEN column_name IS NULL THEN 1 ELSE 0 END)</code>. <code>COUNT(column_name)</code> skips NULLs, so the difference is the NULL count.</p>



<h4 class="wp-block-heading">Why does <code>WHERE column = NULL</code> return no rows?</h4>



<p class="wp-block-paragraph">NULL means unknown, so any normal comparison with it evaluates to unknown, not true. SQL Server only returns rows where the condition is true. Use <code>IS NULL</code> instead.</p>



<h4 class="wp-block-heading">How do I find all columns that contain NULLs?</h4>



<p class="wp-block-paragraph">Query <code>sys.columns</code> for nullable columns, then build a <code>SUM(CASE WHEN ... IS NULL ...)</code> expression for each with <code>STRING_AGG</code> and run it through <code>sp_executesql</code>. Use <code>QUOTENAME</code> for safety.</p>



<h4 class="wp-block-heading">What is the difference between NULL and an empty string?</h4>



<p class="wp-block-paragraph">NULL means no value is known, while an empty string is a known value with zero length. They behave differently in comparisons and aggregates, so search for both when cleaning data.</p>



<p class="wp-block-paragraph">You learned how to find null values in a table in SQL Server with <code>IS NULL</code>, count them per column, catch empty and fake NULLs, and handle them safely in joins and updates. Always preview with a <code>SELECT</code>, back up before changing data, and give audit jobs read-only access through an Entra identity. I hope you found this article helpful.</p>



<p class="wp-block-paragraph">You May Also Like</p>



<ul class="wp-block-list">
<li><a href="https://sqlserverguides.com/how-to-find-duplicate-records-in-sql/" target="_blank" rel="noopener">How to find duplicate records in SQL</a></li>



<li><a href="https://sqlserverguides.com/sql-normalization/" target="_blank" rel="noopener">SQL normalization explained</a></li>



<li><a href="https://sqlserverguides.com/sql-distinct/" target="_blank" rel="noopener">SQL DISTINCT</a></li>



<li><a href="https://sqlserverguides.com/sql-server-table-naming-conventions/" target="_blank" rel="noopener">SQL Server table naming conventions</a></li>



<li><a href="https://sqlserverguides.com/primary-key/" target="_blank" rel="noopener">Primary key in SQL Server</a></li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to Find Consecutive Dates in SQL</title>
		<link>https://sqlserverguides.com/how-to-find-consecutive-dates-in-sql/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Tue, 03 Nov 2026 04:04:17 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[How to Find Consecutive Dates in SQL]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=24041</guid>

					<description><![CDATA[A product manager once asked me for a list of users who had logged in five days in a row, so the team could send them a loyalty reward. An analyst had already tried. The query joined the login table to itself five times, took several minutes on a table with a few million rows, ... <a title="How to Find Consecutive Dates in SQL" class="read-more" href="https://sqlserverguides.com/how-to-find-consecutive-dates-in-sql/" aria-label="Read more about How to Find Consecutive Dates in SQL">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A product manager once asked me for a list of users who had logged in five days in a row, so the team could send them a loyalty reward. An analyst had already tried. The query joined the login table to itself five times, took several minutes on a table with a few million rows, and still missed users who logged in twice on the same day. The rewards email went out a week late.</p>



<p class="wp-block-paragraph">That request is a classic problem: <strong>how to find consecutive dates in SQL</strong>. The good news is that SQL Server has a clean, fast pattern for it, and it needs no loops and no stack of self joins. This guide builds that pattern step by step with T-SQL, then shows how to find streak lengths, longest and current streaks, and missing dates, and how to index the table.</p>



<h2 class="wp-block-heading">Build a Practice Table</h2>



<p class="wp-block-paragraph">A <strong>schema</strong> is the structure of your tables. Use a simple login log, the kind found in SaaS apps and audit trails.</p>



<pre class="wp-block-preformatted"><code>CREATE TABLE dbo.UserLogins (<br>    LoginID INT IDENTITY(1,1) PRIMARY KEY,<br>    UserID  INT NOT NULL,<br>    LoginAt DATETIME2(0) NOT NULL<br>);<br><br>INSERT INTO dbo.UserLogins (UserID, LoginAt)<br>VALUES<br> (1, '2026-10-01 09:10'), (1, '2026-10-02 08:05'), (1, '2026-10-02 17:45'),<br> (1, '2026-10-03 10:30'), (1, '2026-10-05 11:00'), (1, '2026-10-06 12:15'),<br> (1, '2026-10-09 07:50'),<br> (2, '2026-10-01 14:00'), (2, '2026-10-03 13:20'), (2, '2026-10-04 09:40'),<br> (2, '2026-10-05 16:10'), (2, '2026-10-06 08:30'), (2, '2026-10-07 10:05');</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="867" height="351" src="https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Consecutive-Dates-in-SQL.jpg" alt="Find Consecutive Dates in SQL" class="wp-image-24042" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Consecutive-Dates-in-SQL.jpg 867w, https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Consecutive-Dates-in-SQL-300x121.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Consecutive-Dates-in-SQL-766x310.jpg 766w" sizes="auto, (max-width: 867px) 100vw, 867px" /></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="328" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-get-consecutive-dates-in-sql-1024x328.jpg" alt="how to get consecutive dates in sql" class="wp-image-24043" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-get-consecutive-dates-in-sql-1024x328.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-get-consecutive-dates-in-sql-300x96.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-get-consecutive-dates-in-sql-766x245.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-get-consecutive-dates-in-sql.jpg 1413w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">Run this only in a test database. User 1 has two logins on October 2, which is a deliberate trap. Expected streaks are:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>User</th><th>Streak</th><th>Days</th></tr></thead><tbody><tr><td>1</td><td>Oct 1 to Oct 3</td><td>3</td></tr><tr><td>1</td><td>Oct 5 to Oct 6</td><td>2</td></tr><tr><td>1</td><td>Oct 9</td><td>1</td></tr><tr><td>2</td><td>Oct 1</td><td>1</td></tr><tr><td>2</td><td>Oct 3 to Oct 7</td><td>5</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The <code>LoginAt</code> column stores a date and time. Consecutive-date logic works on dates, so you must remove the time part first. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-date-data-types/">SQL Server date data types</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-datetime-to-date/">converting datetime to date</a>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, every streak bug I have fixed involved a date the developer never tested: two events on the same day, a gap over a weekend, or a time component hiding in the column. I put all three in my test data before writing any query.</p>
</blockquote>



<h3 class="wp-block-heading">The Core Pattern: Date Minus Row Number</h3>



<p class="wp-block-paragraph">This technique is part of what DBAs call <strong>gaps and islands</strong>: an island is an unbroken run of values, and a gap is a break between islands. The trick is simple. If dates are consecutive and you subtract an increasing row number from each date, every date in the same streak produces the same result.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Date</th><th>Row number</th><th>Date minus row number</th></tr></thead><tbody><tr><td>Oct 1</td><td>1</td><td>Sep 30</td></tr><tr><td>Oct 2</td><td>2</td><td>Sep 30</td></tr><tr><td>Oct 3</td><td>3</td><td>Sep 30</td></tr><tr><td>Oct 5</td><td>4</td><td>Oct 1</td></tr><tr><td>Oct 6</td><td>5</td><td>Oct 1</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Oct 1 to Oct 3 all land on Sep 30, so they form one group. The jump to Oct 5 produces a new value, which starts a new group. That result becomes your grouping key.</p>



<pre class="wp-block-preformatted"><code>WITH DistinctDays AS (<br>    SELECT DISTINCT<br>        UserID,<br>        CAST(LoginAt AS DATE) AS LoginDate<br>    FROM dbo.UserLogins<br>),<br>Numbered AS (<br>    SELECT<br>        UserID,<br>        LoginDate,<br>        DATEADD(DAY,<br>                -ROW_NUMBER() OVER (PARTITION BY UserID ORDER BY LoginDate),<br>                LoginDate) AS GroupKey<br>    FROM DistinctDays<br>)<br>SELECT<br>    UserID,<br>    MIN(LoginDate) AS StreakStart,<br>    MAX(LoginDate) AS StreakEnd,<br>    COUNT(*)       AS DaysInStreak<br>FROM Numbered<br>GROUP BY UserID, GroupKey<br>ORDER BY UserID, StreakStart;</code></pre>



<p class="wp-block-paragraph">Here is what each part does:</p>



<ul class="wp-block-list">
<li><strong><code>DISTINCT</code> with <code>CAST(... AS DATE)</code></strong> keeps one row per user per day. Without it, user 1&#8217;s two logins on October 2 would break the pattern, because the row number would advance twice for one date. See <a href="https://sqlserverguides.com/sql-distinct/" target="_blank" rel="noopener">DISTINCT</a>.</li>



<li><strong><code>ROW_NUMBER() OVER (PARTITION BY ...)</code></strong> numbers each user&#8217;s dates from 1 upward. <code>PARTITION BY</code> restarts the numbering for each user; read <a href="https://sqlserverguides.com/row_number-function-in-sql-server/" target="_blank" rel="noopener">ROW_NUMBER</a>, <a href="https://sqlserverguides.com/sql-partition-by/" target="_blank" rel="noopener">PARTITION BY</a>, and <a href="https://sqlserverguides.com/sql-over-clause/" target="_blank" rel="noopener">the OVER clause</a>.</li>



<li><strong><code>DATEADD(DAY, -n, date)</code></strong> subtracts the row number from the date. See <a href="https://sqlserverguides.com/dateadd-function-in-sql-server/" target="_blank" rel="noopener">DATEADD</a>.</li>



<li><strong><code>GROUP BY UserID, GroupKey</code></strong> collapses each island into one row, with its start, end, and length.</li>
</ul>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="525" src="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-Consecutive-Dates-in-SQL-1024x525.jpg" alt="How to Find Consecutive Dates in SQL" class="wp-image-24044" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-Consecutive-Dates-in-SQL-1024x525.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-Consecutive-Dates-in-SQL-300x154.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-Consecutive-Dates-in-SQL-767x393.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-Consecutive-Dates-in-SQL.jpg 1438w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">A <strong>CTE</strong> (common table expression) is a named, temporary result that the next query can use. If you wonder when to use one instead of a temp table, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-cte-vs-temp-table/">CTE vs. temp table</a>.</p>



<p class="wp-block-paragraph">The result matches the table above. This pattern reads the data once, sorts it once per user, and scales much better than repeated self joins.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I always run the <code>Numbered</code> CTE by itself first and look at the <code>GroupKey</code> column. If the keys in one visible streak are not identical, the date cleanup is wrong, and every later step will be wrong too.</p>
</blockquote>



<h3 class="wp-block-heading">How to Find Users With a Minimum Streak</h3>



<p class="wp-block-paragraph">The original request was &#8220;five days in a row.&#8221; Add <code>HAVING</code> to filter the grouped islands. Read about <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-having-vs-where/">HAVING vs. WHERE</a> if you need a refresher.</p>



<pre class="wp-block-preformatted"><code>WITH DistinctDays AS (<br>    SELECT DISTINCT UserID, CAST(LoginAt AS DATE) AS LoginDate<br>    FROM dbo.UserLogins<br>),<br>Numbered AS (<br>    SELECT UserID, LoginDate,<br>           DATEADD(DAY,<br>                   -ROW_NUMBER() OVER (PARTITION BY UserID ORDER BY LoginDate),<br>                   LoginDate) AS GroupKey<br>    FROM DistinctDays<br>)<br>SELECT UserID, MIN(LoginDate) AS StreakStart, MAX(LoginDate) AS StreakEnd,<br>       COUNT(*) AS DaysInStreak<br>FROM Numbered<br>GROUP BY UserID, GroupKey<br>HAVING COUNT(*) &gt;= 5;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="426" src="https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Consecutive-Dates-in-SQL-server-1024x426.jpg" alt="Find Consecutive Dates in SQL Server" class="wp-image-24046" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Consecutive-Dates-in-SQL-server-1024x426.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Consecutive-Dates-in-SQL-server-300x125.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Consecutive-Dates-in-SQL-server-767x319.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Consecutive-Dates-in-SQL-server.jpg 1481w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">With the sample data, only user 2 qualifies, with a five-day streak from October 3 to October 7. Change the number in <code>HAVING</code> for any threshold. A user can appear more than once if they had several qualifying streaks, so use <code>SELECT DISTINCT UserID</code> if the business only wants a list of users.</p>



<h3 class="wp-block-heading">Find Each User&#8217;s Longest Streak</h3>



<p class="wp-block-paragraph">To keep only the best streak per user, rank the islands:</p>



<pre class="wp-block-preformatted"><code>WITH DistinctDays AS (<br>    SELECT DISTINCT UserID, CAST(LoginAt AS DATE) AS LoginDate<br>    FROM dbo.UserLogins<br>),<br>Numbered AS (<br>    SELECT UserID, LoginDate,<br>           DATEADD(DAY,<br>                   -ROW_NUMBER() OVER (PARTITION BY UserID ORDER BY LoginDate),<br>                   LoginDate) AS GroupKey<br>    FROM DistinctDays<br>),<br>Streaks AS (<br>    SELECT UserID, MIN(LoginDate) AS StreakStart, MAX(LoginDate) AS StreakEnd,<br>           COUNT(*) AS DaysInStreak<br>    FROM Numbered<br>    GROUP BY UserID, GroupKey<br>),<br>Ranked AS (<br>    SELECT *,<br>           ROW_NUMBER() OVER (PARTITION BY UserID<br>                              ORDER BY DaysInStreak DESC, StreakEnd DESC) AS rn<br>    FROM Streaks<br>)<br>SELECT UserID, StreakStart, StreakEnd, DaysInStreak<br>FROM Ranked<br>WHERE rn = 1;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="434" src="https://sqlserverguides.com/wp-content/uploads/2026/11/get-consecutive-dates-in-sql-1024x434.jpg" alt="get consecutive dates in sql" class="wp-image-24047" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/get-consecutive-dates-in-sql-1024x434.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/get-consecutive-dates-in-sql-767x325.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/11/get-consecutive-dates-in-sql-300x127.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/get-consecutive-dates-in-sql.jpg 1483w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">The tie-breaker <code>StreakEnd DESC</code> makes the result stable when two streaks have the same length: the most recent one wins. User 1 returns Oct 1 to Oct 3, and user 2 returns Oct 3 to Oct 7.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, stakeholders change the rule from &#8220;5 in a row&#8221; to &#8220;longest streak&#8221; to &#8220;current streak&#8221; in the same meeting. I build the <code>Streaks</code> CTE once and reuse it for all three questions.</p>
</blockquote>



<h3 class="wp-block-heading">Find the Current Streak</h3>



<p class="wp-block-paragraph">A &#8220;current streak&#8221; is one that includes today, or yesterday if today&#8217;s activity has not happened yet. Use a variable for the as-of date so you can test repeatably. For more on working with today&#8217;s date, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-days-between-date-and-today/">getting the days between a date and today</a>.</p>



<pre class="wp-block-preformatted"><code>DECLARE @AsOf DATE = '2026-10-09';  <em>-- use CAST(GETDATE() AS DATE) in production</em><br><br>WITH DistinctDays AS (<br>    SELECT DISTINCT UserID, CAST(LoginAt AS DATE) AS LoginDate<br>    FROM dbo.UserLogins<br>),<br>Numbered AS (<br>    SELECT UserID, LoginDate,<br>           DATEADD(DAY,<br>                   -ROW_NUMBER() OVER (PARTITION BY UserID ORDER BY LoginDate),<br>                   LoginDate) AS GroupKey<br>    FROM DistinctDays<br>)<br>SELECT UserID, MIN(LoginDate) AS StreakStart, MAX(LoginDate) AS StreakEnd,<br>       COUNT(*) AS DaysInStreak<br>FROM Numbered<br>GROUP BY UserID, GroupKey<br>HAVING MAX(LoginDate) &gt;= DATEADD(DAY, -1, @AsOf);</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="404" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-identify-consecutive-dates-in-sql-1024x404.jpg" alt="how to identify consecutive dates in sql" class="wp-image-24048" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-identify-consecutive-dates-in-sql-1024x404.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-identify-consecutive-dates-in-sql-768x303.jpg 768w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-identify-consecutive-dates-in-sql-300x118.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-identify-consecutive-dates-in-sql.jpg 1520w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">This keeps streaks that ended today or yesterday. With the sample data and an as-of date of October 9, user 1&#8217;s one-day streak qualifies. Passing the date as a parameter also makes the logic easy to unit test. Time zones matter here: if your users are in several zones, decide whether &#8220;a day&#8221; is based on UTC or on the user&#8217;s local time, and store the rule in the query.</p>



<h3 class="wp-block-heading">An Alternative: LAG to Flag Streak Breaks</h3>



<p class="wp-block-paragraph">The row-number trick is compact but a little abstract. The <code>LAG</code> approach is easier to explain to teammates. <code>LAG</code> reads a value from the previous row. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/window-functions-in-sql-server/">window functions in SQL Server</a>.</p>



<pre class="wp-block-preformatted"><code>WITH DistinctDays AS (<br>    SELECT DISTINCT UserID, CAST(LoginAt AS DATE) AS LoginDate<br>    FROM dbo.UserLogins<br>),<br>Flagged AS (<br>    SELECT UserID, LoginDate,<br>           CASE WHEN DATEDIFF(DAY,<br>                     LAG(LoginDate) OVER (PARTITION BY UserID ORDER BY LoginDate),<br>                     LoginDate) = 1<br>                THEN 0 ELSE 1 END AS IsNewStreak<br>    FROM DistinctDays<br>),<br>Grouped AS (<br>    SELECT UserID, LoginDate,<br>           SUM(IsNewStreak) OVER (PARTITION BY UserID ORDER BY LoginDate<br>                                  ROWS UNBOUNDED PRECEDING) AS StreakID<br>    FROM Flagged<br>)<br>SELECT UserID, StreakID,<br>       MIN(LoginDate) AS StreakStart,<br>       MAX(LoginDate) AS StreakEnd,<br>       COUNT(*) AS DaysInStreak<br>FROM Grouped<br>GROUP BY UserID, StreakID<br>ORDER BY UserID, StreakStart;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="450" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-check-consecutive-dates-in-sql-1024x450.jpg" alt="" class="wp-image-24049" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-check-consecutive-dates-in-sql-1024x450.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-check-consecutive-dates-in-sql-300x132.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-check-consecutive-dates-in-sql-766x337.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-check-consecutive-dates-in-sql-1536x676.jpg 1536w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-check-consecutive-dates-in-sql.jpg 1546w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">The logic reads like a sentence. If the gap from the previous date is exactly one day, this row continues the streak, so the flag is 0. Otherwise it starts a new streak, so the flag is 1. For the first row, <code>LAG</code> returns NULL, <code>DATEDIFF</code> returns NULL, the comparison is not true, and the flag becomes 1, which is correct. A running <code>SUM</code> of the flags then gives each streak an ID. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/datediff-function-in-sql-server/">DATEDIFF</a> for how it counts boundaries.</p>



<p class="wp-block-paragraph">Both approaches give the same answer. The row-number version needs one window function; the <code>LAG</code> version needs two, but each step is easier to debug. Pick the one your team can maintain.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I use the <code>LAG</code> version when I am teaching or handing the query to someone new, and the date-minus-row-number version when I want the shortest code. Whichever you choose, add a comment explaining why it works.</p>
</blockquote>



<h3 class="wp-block-heading">Find Missing Dates (Gaps)</h3>



<p class="wp-block-paragraph">The opposite question is just as common: when did a user skip days? Use <code>LEAD</code>, which reads the next row&#8217;s value.</p>



<pre class="wp-block-preformatted"><code>WITH DistinctDays AS (<br>    SELECT DISTINCT UserID, CAST(LoginAt AS DATE) AS LoginDate<br>    FROM dbo.UserLogins<br>),<br>WithNext AS (<br>    SELECT UserID, LoginDate,<br>           LEAD(LoginDate) OVER (PARTITION BY UserID ORDER BY LoginDate) AS NextDate<br>    FROM DistinctDays<br>)<br>SELECT UserID,<br>       DATEADD(DAY, 1, LoginDate)  AS GapStart,<br>       DATEADD(DAY, -1, NextDate)  AS GapEnd,<br>       DATEDIFF(DAY, LoginDate, NextDate) - 1 AS MissingDays<br>FROM WithNext<br>WHERE DATEDIFF(DAY, LoginDate, NextDate) &gt; 1<br>ORDER BY UserID, GapStart;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="412" src="https://sqlserverguides.com/wp-content/uploads/2026/11/check-consecutive-dates-in-sql-1024x412.jpg" alt="check consecutive dates in sql" class="wp-image-24050" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/check-consecutive-dates-in-sql-1024x412.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/check-consecutive-dates-in-sql-300x121.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/check-consecutive-dates-in-sql-766x308.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/11/check-consecutive-dates-in-sql-1536x618.jpg 1536w, https://sqlserverguides.com/wp-content/uploads/2026/11/check-consecutive-dates-in-sql.jpg 1596w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">For user 1, this returns Oct 4 (1 day) and Oct 7 to Oct 8 (2 days). The final row for each user has no next date, so <code>DATEDIFF</code> is NULL and the row is filtered out, which is the behavior you want. These gap reports help with churn detection, missing sensor readings, and compliance checks.</p>



<h3 class="wp-block-heading">Why Self Joins Are the Wrong Tool</h3>



<p class="wp-block-paragraph">You may see answers like this:</p>



<pre class="wp-block-preformatted"><code>SELECT a.UserID, a.LoginDate<br>FROM DistinctDays a<br>JOIN DistinctDays b ON b.UserID = a.UserID AND b.LoginDate = DATEADD(DAY, 1, a.LoginDate)<br>JOIN DistinctDays c ON c.UserID = a.UserID AND c.LoginDate = DATEADD(DAY, 2, a.LoginDate);</code></pre>



<p class="wp-block-paragraph">It finds three-day streaks, but it has serious problems. You need one extra join per day of the streak, so a seven-day rule needs seven joins. It returns overlapping rows for longer streaks, so a ten-day streak is reported several times. And performance falls as the table grows. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/join-in-sql-server/">joins in SQL Server</a> for when self joins are the right tool. For streaks of any length, the window approach is almost always better.</p>



<p class="wp-block-paragraph">Loops are also a poor fit. A row-by-row <code>WHILE</code> loop processes one date at a time, which is slow and hard to read; set-based T-SQL does the same job in one pass.</p>



<h3 class="wp-block-heading">Make It Fast: Indexing and Date Types</h3>



<p class="wp-block-paragraph">A <strong>clustered index</strong> defines how rows are physically stored, and a <strong>nonclustered index</strong> is a separate sorted structure that speeds up lookups. The streak query partitions by user and sorts by date. An index that already has that order lets SQL Server skip an expensive sort. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-create-index-in-sql-server/">how to create an index in SQL Server</a>.</p>



<p class="wp-block-paragraph">Because the query casts <code>LoginAt</code> to a date, you can store the date part in a <strong>persisted computed column</strong>, a column calculated from others and stored physically so it can be indexed.</p>



<pre class="wp-block-preformatted"><code>ALTER TABLE dbo.UserLogins<br>ADD LoginDate AS CAST(LoginAt AS DATE) PERSISTED;<br><br>CREATE NONCLUSTERED INDEX IX_UserLogins_User_LoginDate<br>ON dbo.UserLogins (UserID, LoginDate);</code></pre>



<p class="wp-block-paragraph">Test this on a non-production copy first. Adding a persisted column to a large table can take time and locks. The index supports <code>PARTITION BY UserID ORDER BY LoginDate</code> and lets SQL Server read each user&#8217;s dates in order. The trade-off is extra storage and slightly slower inserts, because every new login updates the index.</p>



<p class="wp-block-paragraph">Other tuning tips:</p>



<ul class="wp-block-list">
<li><strong>Filter by a date range first.</strong> If you only need the last 90 days, add a range such as <code>LoginAt &gt;= @Start AND LoginAt &lt; @End</code> so the engine reads less. See <a href="https://sqlserverguides.com/sql-server-filter-by-date/" target="_blank" rel="noopener">filtering by date</a> and <a href="https://sqlserverguides.com/sql-query-optimization-tips/" target="_blank" rel="noopener">SQL query optimization tips</a>.</li>



<li><strong>Pre-aggregate large logs.</strong> For hundreds of millions of rows, keep a small daily table with one row per user per day, updated by a nightly job, and run streak queries against that.</li>



<li><strong>Check the actual execution plan</strong> for Sort operators and scans. In Azure SQL Database, use Query Store to compare run time before and after the index.<strong>Pro Tip:</strong> In my experience, a daily summary table beats any clever index once the log gets huge. It turns &#8220;scan every login ever&#8221; into &#8220;read one row per user per day.&#8221;</li>
</ul>



<h3 class="wp-block-heading">Frequently Asked Questions</h3>



<h4 class="wp-block-heading">How do I find consecutive dates in SQL Server?</h4>



<p class="wp-block-paragraph">Remove duplicate days, number each user&#8217;s dates with <code>ROW_NUMBER()</code>, and subtract that number from the date. Dates in the same streak give the same result, so you can group by it. Then use <code>MIN</code>, <code>MAX</code>, and <code>COUNT</code> to get the start, end, and length.</p>



<h4 class="wp-block-heading">How do I find users who logged in three days in a row?</h4>



<p class="wp-block-paragraph">Use the grouping pattern and add <code>HAVING COUNT(*) &gt;= 3</code>. Apply <code>DISTINCT UserID</code> if you only need a list of users instead of each streak.</p>



<h4 class="wp-block-heading">What is the difference between ROW_NUMBER and LAG for streaks?</h4>



<p class="wp-block-paragraph"><code>ROW_NUMBER</code> with date subtraction creates the group key in one step and is shorter. <code>LAG</code> compares each date to the previous one, flags breaks, and builds groups with a running sum; it is longer but easier to read and debug.</p>



<h4 class="wp-block-heading">How do I find missing dates between rows?</h4>



<p class="wp-block-paragraph">Use <code>LEAD</code> to get each row&#8217;s next date, then keep rows where <code>DATEDIFF(DAY, LoginDate, NextDate) &gt; 1</code>. The gap runs from one day after the current date to one day before the next date.</p>



<h4 class="wp-block-heading">Why does my streak query miscount when users log in twice a day?</h4>



<p class="wp-block-paragraph">Duplicate rows advance <code>ROW_NUMBER</code> without advancing the date, which breaks the pattern. Select <code>DISTINCT UserID, CAST(LoginAt AS DATE)</code> first so each day counts once.</p>



<p class="wp-block-paragraph">You learned how to find consecutive dates in SQL Server by cleaning dates, numbering rows, grouping streaks, and using <code>LAG</code> and <code>LEAD</code> for flags and gaps. Always reduce your data to one row per entity per day before applying streak logic, and give report users read-only access to a view instead of the raw log. I hope you found this article helpful.</p>



<p class="wp-block-paragraph">You May Also Like</p>



<ul class="wp-block-list">
<li><a href="https://sqlserverguides.com/sql-server-max-date/" target="_blank" rel="noopener">How to find the maximum date in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/sql-server-min-date/" target="_blank" rel="noopener">How to find the minimum date in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/sql-server-add-days-to-date/" target="_blank" rel="noopener">How to add days to a date in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/sql-server-weekday-from-date/" target="_blank" rel="noopener">How to get the weekday from a date in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/sql-server-get-the-latest-record-by-date/" target="_blank" rel="noopener">How to get the latest record by date in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/how-to-find-null-values-in-a-table-in-sql-server/" target="_blank" rel="noreferrer noopener">How to Find NULL Values in a Table in SQL Server</a></li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to Generate Sequence Number in SQL Server (4 Ways)</title>
		<link>https://sqlserverguides.com/how-to-generate-sequence-number-in-sql-server/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Mon, 02 Nov 2026 16:47:04 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[How to Generate Sequence Number in SQL Server]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=24026</guid>

					<description><![CDATA[A small e-commerce team once asked me why two customers had received the same order number on Black Friday. The app read MAX(OrderNumber), added 1, and inserted the row. Under light traffic it worked for months. Under load, two sessions read the same maximum at the same moment and both wrote the same &#8220;next&#8221; number. ... <a title="How to Generate Sequence Number in SQL Server (4 Ways)" class="read-more" href="https://sqlserverguides.com/how-to-generate-sequence-number-in-sql-server/" aria-label="Read more about How to Generate Sequence Number in SQL Server (4 Ways)">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A small e-commerce team once asked me why two customers had received the same order number on Black Friday. The app read <code>MAX(OrderNumber)</code>, added 1, and inserted the row. Under light traffic it worked for months. Under load, two sessions read the same maximum at the same moment and both wrote the same &#8220;next&#8221; number.</p>



<p class="wp-block-paragraph">That bug is why choosing how to <strong>generate sequence number in SQL Server</strong> matters more than it looks. SQL Server gives you four solid options: <code>IDENTITY</code> columns, <code>SEQUENCE</code> objects, <code>ROW_NUMBER()</code> for query results, and a counter table for business-specific numbering. Each fits a different job. This guide shows them with T-SQL, explains gaps and concurrency, and covers permissions and testing in SQL Server and Azure SQL Database, so you can pick the right tool and avoid the Black Friday bug.</p>



<h2 class="wp-block-heading">Choose the Right Method First</h2>



<p class="wp-block-paragraph">Different needs call for different tools. A <strong>sequence number</strong> here means an ordered number generated by the database, either stored in a table or created in a query result.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Need</th><th class="has-text-align-left" data-align="left">Best tool</th><th class="has-text-align-left" data-align="left">Stored permanently?</th></tr></thead><tbody><tr><td>Unique row ID in one table</td><td><code>IDENTITY</code></td><td>Yes</td></tr><tr><td>One number series shared by several tables, or numbers created before insert</td><td><code>SEQUENCE</code></td><td>Yes</td></tr><tr><td>Numbering rows in a report or query</td><td><code>ROW_NUMBER()</code></td><td>No</td></tr><tr><td>Gap-free or per-tenant numbers, such as invoices</td><td>Counter table</td><td>Yes</td></tr><tr><td>A list of numbers, such as 1 to 1000</td><td><code>GENERATE_SERIES</code></td><td>No</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Start with the first question: does the number need to be stored, or only displayed? A stored number needs <code>IDENTITY</code>, <code>SEQUENCE</code>, or a counter table. A displayed number almost always needs <code>ROW_NUMBER()</code>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, most sequence problems come from using a stored-number tool for a display-number job, or the reverse. I write one sentence first: &#8220;This number must be <em></em><em></em><em></em>.&#8221; Unique, ordered, gap-free, or per-customer. The answer picks the tool.</p>
</blockquote>



<h3 class="wp-block-heading">Method 1: Use IDENTITY for Automatic Row IDs</h3>



<p class="wp-block-paragraph">An <strong>identity column</strong> is a column where SQL Server generates the next value automatically for each new row. It is the simplest and most common choice. If you need a refresher, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-create-an-identity-column-in-sql-server/">how to create an identity column in SQL Server</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-create-table-with-identity-column/">creating a table with an identity column</a>.</p>



<pre class="wp-block-preformatted"><code>CREATE TABLE dbo.Orders (<br>    OrderID     INT IDENTITY(1,1) PRIMARY KEY,<br>    CustomerID  INT NOT NULL,<br>    OrderDate   DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME()<br>);<br><br>INSERT INTO dbo.Orders (CustomerID) VALUES (101), (102), (103);</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="318" src="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Generate-a-Number-Sequence-in-SQL-Server-1024x318.jpg" alt="How to Generate a Number Sequence in SQL Server" class="wp-image-24027" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Generate-a-Number-Sequence-in-SQL-Server-1024x318.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Generate-a-Number-Sequence-in-SQL-Server-300x93.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Generate-a-Number-Sequence-in-SQL-Server-766x238.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Generate-a-Number-Sequence-in-SQL-Server.jpg 1200w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="203" src="https://sqlserverguides.com/wp-content/uploads/2026/11/Generate-Sequence-Number-in-SQL-Server-1024x203.jpg" alt="Generate Sequence Number in SQL Server" class="wp-image-24028" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/Generate-Sequence-Number-in-SQL-Server-1024x203.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/Generate-Sequence-Number-in-SQL-Server-766x152.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/11/Generate-Sequence-Number-in-SQL-Server-300x59.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/Generate-Sequence-Number-in-SQL-Server.jpg 1235w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph"><code>IDENTITY(1,1)</code> means the first value is 1 and each new row adds 1. You do not insert a value for <code>OrderID</code>; SQL Server supplies it. The <strong>primary key</strong> makes the column unique and creates a clustered index by default, which is the sorted structure that stores the table&#8217;s rows. New identity values are always increasing, so inserts append to the end of the index, which keeps page splits low.</p>



<p class="wp-block-paragraph">To get the value that was just created in your session, use <code>SCOPE_IDENTITY()</code>:</p>



<pre class="wp-block-preformatted"><code>INSERT INTO dbo.Orders (CustomerID) VALUES (104);<br>SELECT SCOPE_IDENTITY() AS NewOrderID;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="978" height="262" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-generate-5-digit-sequence-number-in-sql-server.jpg" alt="how to generate 5 digit sequence number in sql server" class="wp-image-24029" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-generate-5-digit-sequence-number-in-sql-server.jpg 978w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-generate-5-digit-sequence-number-in-sql-server-765x205.jpg 765w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-generate-5-digit-sequence-number-in-sql-server-300x80.jpg 300w" sizes="auto, (max-width: 978px) 100vw, 978px" /></figure>
</div>


<p class="wp-block-paragraph">Prefer <code>SCOPE_IDENTITY()</code> over <code>@@IDENTITY</code>, because <code>@@IDENTITY</code> can return a value generated by a trigger on another table. Read more in <a rel="noopener" target="_blank" href="https://sqlserverguides.com/scope-identity-sql-server/">SCOPE_IDENTITY in SQL Server</a>.</p>



<h4 class="wp-block-heading">Identity Gaps Are Normal</h4>



<p class="wp-block-paragraph">Identity values are <strong>not gap-free</strong>. A rolled-back transaction still consumes a value. A failed insert consumes one too. After some unexpected restarts, SQL Server can skip a block of values because it caches identity numbers in memory. Treat identity as &#8220;unique and increasing,&#8221; not &#8220;consecutive.&#8221;</p>



<p class="wp-block-paragraph">If you absolutely need to insert your own value, such as during a data migration, you must turn on <code>IDENTITY_INSERT</code> for that table; see <a href="https://sqlserverguides.com/sql-server-set-identity_insert/" target="_blank" rel="noopener">SET IDENTITY_INSERT</a>. If you forget, you get the error described in <a href="https://sqlserverguides.com/cannot-insert-explicit-value-for-identity-column-in-table/" target="_blank" rel="noopener">cannot insert explicit value for identity column</a>. To reset the counter, see <a href="https://sqlserverguides.com/how-to-reset-identity-column-in-sql-server/" target="_blank" rel="noopener">how to reset an identity column</a>. </p>



<p class="wp-block-paragraph"><strong>Warning:</strong> reseeding to a value that already exists can cause primary key violations later.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I once saw a developer reseed an identity to 0 on a table that still had rows. Everything worked until the counter reached an existing ID, then inserts started failing at random. I now only reseed after confirming the table is empty or the new seed is above the maximum.</p>
</blockquote>



<h3 class="wp-block-heading">Method 2: Use a SEQUENCE Object for Shared or Pre-Generated Numbers</h3>



<p class="wp-block-paragraph">A <strong>sequence</strong> is a database object, separate from any table, that hands out numbers. Use it when several tables need numbers from one series, when the app needs the number before inserting, or when you want a number on a column that cannot be an identity. </p>



<p class="wp-block-paragraph">Sequences are supported in SQL Server 2012 and later, and in Azure SQL Database and Azure SQL Managed Instance.</p>



<pre class="wp-block-preformatted"><code>CREATE SEQUENCE dbo.OrderNumberSeq<br>    AS BIGINT<br>    START WITH 100000<br>    INCREMENT BY 1<br>    MINVALUE 100000<br>    NO CYCLE<br>    CACHE 50;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="788" height="491" src="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Generate-Sequence-Number-in-SQL-Server.jpg" alt="How to Generate Sequence Number in SQL Server" class="wp-image-24030" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Generate-Sequence-Number-in-SQL-Server.jpg 788w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Generate-Sequence-Number-in-SQL-Server-300x187.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Generate-Sequence-Number-in-SQL-Server-767x478.jpg 767w" sizes="auto, (max-width: 788px) 100vw, 788px" /></figure>
</div>


<p class="wp-block-paragraph">This creates a sequence of <code>BIGINT</code> values starting at 100000. <code>NO CYCLE</code> means SQL Server raises an error at the maximum value instead of starting over, which prevents silent duplicates. <code>CACHE 50</code> keeps 50 values in memory so SQL Server does not write to disk for every number. </p>



<p class="wp-block-paragraph">The trade-off is that a crash or restart can skip up to the cached amount. If you need fewer gaps, use <code>NO CACHE</code>, at some cost in speed.</p>



<p class="wp-block-paragraph">Attach it to a column with a default constraint:</p>



<pre class="wp-block-preformatted"><code>ALTER TABLE dbo.Orders<br>ADD OrderNumber BIGINT NOT NULL<br>    CONSTRAINT DF_Orders_OrderNumber<br>    DEFAULT (NEXT VALUE FOR dbo.OrderNumberSeq),<br>    CONSTRAINT UQ_Orders_OrderNumber UNIQUE (OrderNumber);</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="342" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-generate-sequence-number-in-sql-select-query-1024x342.jpg" alt="how to generate sequence number in sql select query" class="wp-image-24031" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-generate-sequence-number-in-sql-select-query-1024x342.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-generate-sequence-number-in-sql-select-query-300x100.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-generate-sequence-number-in-sql-select-query-767x256.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-generate-sequence-number-in-sql-select-query.jpg 1130w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph"><code>NEXT VALUE FOR</code> pulls the next number. The <code>UNIQUE</code> constraint is important: a sequence does not enforce uniqueness by itself, because someone can still insert a manual value. The constraint closes that hole. Note that adding a <code>NOT NULL</code> column with a default to a large table can take time and locks, so test it first.</p>



<p class="wp-block-paragraph">You can also request a number manually:</p>



<pre class="wp-block-preformatted"><code>DECLARE @n BIGINT = NEXT VALUE FOR dbo.OrderNumberSeq;<br>SELECT @n AS ReservedNumber;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="198" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-generate-sequence-number-in-sql-1024x198.jpg" alt="how to generate sequence number in sql" class="wp-image-24032" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-generate-sequence-number-in-sql-1024x198.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-generate-sequence-number-in-sql-300x58.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-generate-sequence-number-in-sql-764x148.jpg 764w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-generate-sequence-number-in-sql.jpg 1063w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">This is useful when the app must show or log the number before the row is saved. Be aware that if the transaction later fails, that number is used up. Sequences, like identity, can have gaps.</p>



<p class="wp-block-paragraph">For bulk loads, reserve a block at once with <code>sp_sequence_get_range</code>, which returns a start and end for the block. That is much faster than asking one row at a time.</p>



<h4 class="wp-block-heading">IDENTITY vs. SEQUENCE</h4>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Question</th><th class="has-text-align-left" data-align="left">IDENTITY</th><th class="has-text-align-left" data-align="left">SEQUENCE</th></tr></thead><tbody><tr><td>Tied to one table</td><td>Yes</td><td>No</td></tr><tr><td>Get the number before insert</td><td>Not directly</td><td>Yes</td></tr><tr><td>Shared across tables</td><td>No</td><td>Yes</td></tr><tr><td>Can be changed with <code>ALTER</code></td><td>Limited</td><td>Yes (<code>ALTER SEQUENCE</code>)</td></tr><tr><td>Simplicity</td><td>Higher</td><td>Lower</td></tr></tbody></table></figure>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, teams reach for sequences too early. I use <code>IDENTITY</code> by default and switch to a sequence only when I need a shared series or the number before the insert.</p>
</blockquote>



<h3 class="wp-block-heading">Method 3: Use ROW_NUMBER() to Number Query Results</h3>



<p class="wp-block-paragraph">Sometimes you only need numbering in a result, such as a ranked leaderboard or a paged list. <code>ROW_NUMBER()</code> is a <strong>window function</strong>, a function that calculates across a set of rows without collapsing them. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/row_number-function-in-sql-server/">ROW_NUMBER in SQL Server</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-over-clause/">the OVER clause</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    ROW_NUMBER() OVER (ORDER BY OrderDate, OrderID) AS RowNum,<br>    OrderID,<br>    CustomerID,<br>    OrderDate<br>FROM dbo.Orders;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="367" src="https://sqlserverguides.com/wp-content/uploads/2026/11/Generate-Sequence-Number-in-SQL-1024x367.jpg" alt="Generate Sequence Number in SQL" class="wp-image-24033" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/Generate-Sequence-Number-in-SQL-1024x367.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/Generate-Sequence-Number-in-SQL-300x107.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/Generate-Sequence-Number-in-SQL-768x275.jpg 768w, https://sqlserverguides.com/wp-content/uploads/2026/11/Generate-Sequence-Number-in-SQL.jpg 1206w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">The <code>ORDER BY</code> inside <code>OVER</code> sets the numbering order. Always add a tie-breaker, such as <code>OrderID</code>, so the numbers are stable when two rows share a date.</p>



<p class="wp-block-paragraph">To restart the numbering for each customer, add <code>PARTITION BY</code>; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-partition-by/">PARTITION BY</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    CustomerID,<br>    OrderID,<br>    ROW_NUMBER() OVER (<br>        PARTITION BY CustomerID<br>        ORDER BY OrderDate, OrderID) AS CustomerOrderNo<br>FROM dbo.Orders;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="400" src="https://sqlserverguides.com/wp-content/uploads/2026/11/Generate-Sequence-Number-SQL-Server-1024x400.jpg" alt="Generate Sequence Number SQL Server" class="wp-image-24034" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/Generate-Sequence-Number-SQL-Server-1024x400.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/Generate-Sequence-Number-SQL-Server-300x117.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/Generate-Sequence-Number-SQL-Server-768x300.jpg 768w, https://sqlserverguides.com/wp-content/uploads/2026/11/Generate-Sequence-Number-SQL-Server.jpg 1093w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">Each customer&#8217;s orders are numbered 1, 2, 3, and so on. The number exists only in the result. If you save it into a table, it will not update when rows are added or deleted.</p>



<p class="wp-block-paragraph">A common use is deduplication and &#8220;latest row&#8221; queries, where <code>ROW_NUMBER() = 1</code> picks one row per group. It is also faster than old tricks that used a self join or correlated subquery.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, a window function on a large unindexed sort is where reports slow down. Before I tune anything, I check the execution plan for a Sort operator, then consider an index that matches the <code>PARTITION BY</code> and <code>ORDER BY</code> columns.</p>
</blockquote>



<h3 class="wp-block-heading">Method 4: Build Business Numbers With a Counter Table</h3>



<p class="wp-block-paragraph">Invoices often need numbers that are consecutive and reset per tenant or per year, such as <code>INV-2026-000124</code>. Identity and sequences allow gaps, and neither restarts per tenant. For gap-free or per-tenant numbers, use a small <strong>counter table</strong> and update it inside the same transaction as the insert.</p>



<pre class="wp-block-preformatted"><code>CREATE TABLE dbo.InvoiceCounters (<br>    TenantID   INT NOT NULL,<br>    YearNo     INT NOT NULL,<br>    LastNumber INT NOT NULL,<br>    CONSTRAINT PK_InvoiceCounters PRIMARY KEY (TenantID, YearNo)<br>);<br><br>INSERT INTO dbo.InvoiceCounters VALUES (1, 2026, 0);</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="315" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-add-sequence-number-in-sql-1024x315.jpg" alt="how to add sequence number in sql" class="wp-image-24035" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-add-sequence-number-in-sql-1024x315.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-add-sequence-number-in-sql-767x236.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-add-sequence-number-in-sql-300x92.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-add-sequence-number-in-sql.jpg 1307w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1022" height="243" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-create-sequence-in-sql.jpg" alt="how to create sequence in sql" class="wp-image-24036" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-create-sequence-in-sql.jpg 1022w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-create-sequence-in-sql-765x182.jpg 765w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-create-sequence-in-sql-300x71.jpg 300w" sizes="auto, (max-width: 1022px) 100vw, 1022px" /></figure>
</div>


<p class="wp-block-paragraph">Then increment and read the value in one atomic statement:</p>



<pre class="wp-block-preformatted"><code>DECLARE @TenantID INT = 1, @YearNo INT = 2026, @n INT;<br><br>BEGIN TRANSACTION;<br><br>UPDATE dbo.InvoiceCounters<br>SET @n = LastNumber = LastNumber + 1<br>WHERE TenantID = @TenantID AND YearNo = @YearNo;<br><br>SELECT CONCAT('INV-', @YearNo, '-', RIGHT(CONCAT('000000', @n), 6)) AS InvoiceNumber;<br><br>COMMIT TRANSACTION;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="357" src="https://sqlserverguides.com/wp-content/uploads/2026/11/sequence-in-sql-with-example-1024x357.jpg" alt="sequence in sql with example" class="wp-image-24037" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/sequence-in-sql-with-example-1024x357.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/sequence-in-sql-with-example-300x105.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/sequence-in-sql-with-example-766x267.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/11/sequence-in-sql-with-example-1536x536.jpg 1536w, https://sqlserverguides.com/wp-content/uploads/2026/11/sequence-in-sql-with-example.jpg 1577w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">The statement <code>SET @n = LastNumber = LastNumber + 1</code> updates the row and captures the new value in a single step. SQL Server takes an <strong>update lock</strong> on that row, so a second session waits until the first transaction commits. </p>



<p class="wp-block-paragraph">That serialization is what prevents duplicates and gaps. It also means invoice creation for the same tenant is serialized, which is a throughput limit. Keep the transaction short, and put the counter update as late in the transaction as possible.</p>



<p class="wp-block-paragraph">If the transaction rolls back, the counter update rolls back too, so no number is lost. The <code>RIGHT(CONCAT('000000', @n), 6)</code> expression pads with leading zeros; for other ways, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/convert-int-to-string-with-leading-zero-in-sql-server/">converting an integer to a string with leading zeros</a>. Wrap this logic in a stored procedure so every app uses the same code; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/create-stored-procedure-in-sql-server/">creating a stored procedure</a>.</p>



<p class="wp-block-paragraph">Under heavy load, long transactions here can cause <strong>blocking</strong>, where one session waits on another&#8217;s locks, and in the worst case a <strong>deadlock</strong>, where two sessions wait on each other. Read about <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-deadlock-victim/">deadlock victims</a> and keep the update order consistent in every procedure.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I once saw an invoice procedure hold the counter lock while it called a slow tax service. Invoice creation backed up for minutes. Now I do slow calls first and update the counter last, right before the commit.</p>
</blockquote>



<h3 class="wp-block-heading">Generate a Simple List of Numbers</h3>



<p class="wp-block-paragraph">To create a list such as 1 to 100, use <code>GENERATE_SERIES</code>, available in SQL Server 2022 and Azure SQL Database.</p>



<pre class="wp-block-preformatted"><code>SELECT value AS Number<br>FROM GENERATE_SERIES(1, 100);</code></pre>



<p class="wp-block-paragraph">Add a third argument for the step, as in <code>GENERATE_SERIES(10, 100, 10)</code>. For older versions, a recursive CTE works but is slower and limited by <code>MAXRECURSION</code>. Avoid row-by-row loops to build numbers; a set-based approach is almost always faster than a <code>WHILE</code> loop. If you do need a loop, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/while-loop-in-sql-server-stored-procedure/">WHILE loops in stored procedures</a>.</p>



<p class="wp-block-paragraph">Number lists are handy for date tables and test data. To load them into another table, use <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-insert-into-select-statement/">INSERT INTO &#8230; SELECT</a>.</p>



<h3 class="wp-block-heading">Never Use MAX + 1</h3>



<p class="wp-block-paragraph">The pattern that broke the Black Friday orders looks like this:</p>



<pre class="wp-block-preformatted"><code><em>-- Unsafe under concurrency: do not use</em><br>DECLARE @next INT = (SELECT MAX(OrderNumber) + 1 FROM dbo.Orders);<br>INSERT INTO dbo.Orders (OrderNumber, CustomerID) VALUES (@next, 101);</code></pre>



<p class="wp-block-paragraph">Two sessions can both read the same maximum before either inserts. Locking hints can make it safer, but they add complexity and blocking, and a tool built for the job is simpler. Use <code>IDENTITY</code>, a <code>SEQUENCE</code>, or the counter table. If you want to review how <code>MAX</code> works, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-max-function-in-sql-server/">the MAX function</a>.</p>



<h3 class="wp-block-heading">Frequently Asked Questions</h3>



<h4 class="wp-block-heading">How do I generate a sequence number in SQL Server?</h4>



<p class="wp-block-paragraph">Use an <code>IDENTITY</code> column for automatic row numbers, a <code>SEQUENCE</code> object with <code>NEXT VALUE FOR</code> for shared or pre-assigned numbers, or <code>ROW_NUMBER()</code> to number query results. The best choice depends on whether the number is stored or only displayed.</p>



<h4 class="wp-block-heading">What is the difference between IDENTITY and SEQUENCE?</h4>



<p class="wp-block-paragraph"><code>IDENTITY</code> belongs to one table and fills in automatically. A <code>SEQUENCE</code> is a separate object that multiple tables can use, and you can fetch its value before inserting. Both can have gaps.</p>



<h4 class="wp-block-heading">Why does my identity column skip numbers?</h4>



<p class="wp-block-paragraph">Rolled-back transactions and failed inserts consume values, and cached values can be lost after a restart. Identity guarantees uniqueness and increasing order, not consecutive values.</p>



<h4 class="wp-block-heading">How do I get gap-free invoice numbers in SQL Server?</h4>



<p class="wp-block-paragraph">Use a counter table updated inside the same transaction as the invoice insert, so a rollback also rolls back the number. Keep the transaction short, because this approach serializes writers for the same counter.</p>



<h4 class="wp-block-heading">How do I restart row numbers for each group?</h4>



<p class="wp-block-paragraph">Use <code>ROW_NUMBER() OVER (PARTITION BY group_column ORDER BY sort_column)</code>. Numbering restarts at 1 for each partition and exists only in the query result.</p>



<p class="wp-block-paragraph">You learned four ways to generate sequence number values in SQL Server: <code>IDENTITY</code>, <code>SEQUENCE</code>, <code>ROW_NUMBER()</code>, and counter tables, and why each behaves differently with gaps and concurrency. Never calculate the next number with <code>MAX + 1</code>; let the database generate it, enforce uniqueness with a constraint, and test parallel inserts before you go live. I hope you found this article helpful.</p>



<p class="wp-block-paragraph">You May Also Like</p>



<ul class="wp-block-list">
<li><a href="https://sqlserverguides.com/how-to-insert-identity-column-in-sql-server/" target="_blank" rel="noopener">How to insert an identity column in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/how-to-add-identity-to-existing-column-in-sql-server/" target="_blank" rel="noopener">How to add identity to an existing column in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/sql-server-create-table-with-primary-key/" target="_blank" rel="noopener">How to create a table with a primary key in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/sql-server-add-primary-key-to-existing-table/" target="_blank" rel="noopener">How to add a primary key to an existing table</a></li>



<li><a href="https://sqlserverguides.com/truncate-table-in-sql-server/" target="_blank" rel="noopener">How to truncate a table in SQL Server</a></li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to Find Even and Odd Numbers in SQL Server</title>
		<link>https://sqlserverguides.com/how-to-find-even-and-odd-numbers-in-sql-server/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Mon, 02 Nov 2026 09:29:16 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[How to Find Even and Odd Numbers in SQL Server]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=24016</guid>

					<description><![CDATA[A warehouse team once asked me to split pick tickets between two packing lines. Odd quantities went to Line A, which handled loose items, and even quantities went to Line B, which handled boxed pairs. The developer wrote WHERE Quantity % 2 = 1 and shipped it. Within a week, returns adjustments were landing on ... <a title="How to Find Even and Odd Numbers in SQL Server" class="read-more" href="https://sqlserverguides.com/how-to-find-even-and-odd-numbers-in-sql-server/" aria-label="Read more about How to Find Even and Odd Numbers in SQL Server">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A warehouse team once asked me to split pick tickets between two packing lines. Odd quantities went to Line A, which handled loose items, and even quantities went to Line B, which handled boxed pairs. The developer wrote <code>WHERE Quantity % 2 = 1</code> and shipped it. Within a week, returns adjustments were landing on neither line, because negative quantities such as -3 were being skipped.</p>



<p class="wp-block-paragraph">That bug is why <strong>how to find even and odd numbers in SQL Server</strong> is a better question than it looks. The basic test is one operator. A production-safe version has to handle negatives, NULLs, decimals, text columns, and performance on large tables. This guide covers each case in T-SQL, shows how to generate and label number sequences, and explains how to keep these checks fast and safe in Azure SQL Database or SQL Server.</p>



<h2 class="wp-block-heading">Create a Practice Table With Edge Cases</h2>



<p class="wp-block-paragraph">Test on a small table that includes the awkward values. A <strong>schema</strong> is the structure of your tables, and sample rows with edge cases reveal bugs early. If you need background on identity columns, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-create-table-with-identity-column/">creating a table with an identity column</a>.</p>



<pre class="wp-block-preformatted"><code>CREATE TABLE dbo.StockAdjustments (<br>    AdjustmentID INT IDENTITY(1,1) PRIMARY KEY,<br>    SKU          VARCHAR(20) NOT NULL,<br>    QtyChange    INT NULL<br>);<br><br>INSERT INTO dbo.StockAdjustments (SKU, QtyChange)<br>VALUES<br> ('A-100', 10),<br> ('A-101', 7),<br> ('A-102', -3),<br> ('A-103', -8),<br> ('A-104', 0),<br> ('A-105', NULL),<br> ('A-106', 15);</code></pre>



<p class="wp-block-paragraph">After executing the above query, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="952" height="375" src="https://sqlserverguides.com/wp-content/uploads/2026/11/find-even-and-odd-numbers-in-sql-server.jpg" alt="find even and odd numbers in sql server" class="wp-image-24017" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/find-even-and-odd-numbers-in-sql-server.jpg 952w, https://sqlserverguides.com/wp-content/uploads/2026/11/find-even-and-odd-numbers-in-sql-server-767x302.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/11/find-even-and-odd-numbers-in-sql-server-300x118.jpg 300w" sizes="auto, (max-width: 952px) 100vw, 952px" /></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="980" height="582" src="https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Even-and-Odd-Numbers-in-SQL.jpg" alt="Find Even and Odd Numbers in SQL" class="wp-image-24018" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Even-and-Odd-Numbers-in-SQL.jpg 980w, https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Even-and-Odd-Numbers-in-SQL-300x178.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Even-and-Odd-Numbers-in-SQL-768x456.jpg 768w" sizes="auto, (max-width: 980px) 100vw, 980px" /></figure>
</div>


<p class="wp-block-paragraph">The table has positive and negative numbers, zero, and a NULL. A <strong>NULL</strong> is a missing or unknown value, not zero. Run this only in a test database, because it inserts rows.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, every number-logic bug I have fixed involved a value the developer never tested: zero, a negative, or NULL. I put all three in my sample data before writing the first query.</p>
</blockquote>



<h3 class="wp-block-heading">The Modulo Method to Find Even and Odd Numbers in SQL Server</h3>



<p class="wp-block-paragraph">The <strong>modulo operator</strong> (<code>%</code>) returns the remainder after division. Any even number divided by 2 leaves a remainder of 0. Any odd number leaves a remainder of 1 or, for negatives in SQL Server, -1.</p>



<pre class="wp-block-preformatted"><code>SELECT AdjustmentID, QtyChange<br>FROM dbo.StockAdjustments<br>WHERE QtyChange % 2 = 0;</code></pre>



<p class="wp-block-paragraph">After executing the above query, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="677" height="287" src="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-Even-and-Odd-Numbers-in-SQL-Server.jpg" alt="How to Find Even and Odd Numbers in SQL Server" class="wp-image-24019" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-Even-and-Odd-Numbers-in-SQL-Server.jpg 677w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-Even-and-Odd-Numbers-in-SQL-Server-300x127.jpg 300w" sizes="auto, (max-width: 677px) 100vw, 677px" /></figure>
</div>


<p class="wp-block-paragraph">This returns 10, -8, and 0. Zero is even, which is mathematically correct. The NULL row is not returned, because <code>NULL % 2</code> is NULL, and a comparison with NULL is never true. Learn more in <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-null/">how SQL handles NULL</a> and the <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-where-clause-tutorial/">WHERE clause tutorial</a>.</p>



<h4 class="wp-block-heading">The Negative Number Trap</h4>



<p class="wp-block-paragraph">Now try the obvious query for odd numbers:</p>



<pre class="wp-block-preformatted"><code><em>-- Misses negative odd numbers</em><br>SELECT AdjustmentID, QtyChange<br>FROM dbo.StockAdjustments<br>WHERE QtyChange % 2 = 1;</code></pre>



<p class="wp-block-paragraph">After executing the above query, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="912" height="561" src="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-Even-and-Odd-Numbers-SQL-Server.jpg" alt="How to Find Even and Odd Numbers SQL Server" class="wp-image-24022" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-Even-and-Odd-Numbers-SQL-Server.jpg 912w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-Even-and-Odd-Numbers-SQL-Server-767x472.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-Even-and-Odd-Numbers-SQL-Server-300x185.jpg 300w" sizes="auto, (max-width: 912px) 100vw, 912px" /></figure>
</div>


<p class="wp-block-paragraph">After executing the above query, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="669" height="263" src="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-Even-and-Odd-Numbers-in-SQL.jpg" alt="" class="wp-image-24020" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-Even-and-Odd-Numbers-in-SQL.jpg 669w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Find-Even-and-Odd-Numbers-in-SQL-300x118.jpg 300w" sizes="auto, (max-width: 669px) 100vw, 669px" /></figure>
</div>


<p class="wp-block-paragraph">This returns 7 and 15 but not -3. In SQL Server, the result of <code>%</code> takes the sign of the dividend, so <code>-3 % 2</code> equals -1, not 1. The fix is to test for &#8220;not even&#8221; instead:</p>



<pre class="wp-block-preformatted"><code>SELECT AdjustmentID, QtyChange<br>FROM dbo.StockAdjustments<br>WHERE QtyChange % 2 &lt;&gt; 0;</code></pre>



<p class="wp-block-paragraph">After executing the above query, I got the expected output as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="670" height="281" src="https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Even-and-Odd-Numbers-SQL.jpg" alt="Find Even and Odd Numbers SQL" class="wp-image-24021" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Even-and-Odd-Numbers-SQL.jpg 670w, https://sqlserverguides.com/wp-content/uploads/2026/11/Find-Even-and-Odd-Numbers-SQL-300x126.jpg 300w" sizes="auto, (max-width: 670px) 100vw, 670px" /></figure>
</div>


<p class="wp-block-paragraph">Now the result is 7, -3, and 15. Using <code>&lt;&gt; 0</code> works for positive and negative numbers. Alternatively, wrap the result in <code>ABS()</code>, which returns the absolute value, as in <code>ABS(QtyChange % 2) = 1</code>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I never write <code>% 2 = 1</code> anymore. I use <code>% 2 &lt;&gt; 0</code> for odd numbers, so negative values are handled without thinking about it.</p>
</blockquote>



<h4 class="wp-block-heading">Use CASE to Label Each Row</h4>



<p class="wp-block-paragraph">Often you need a label, not a filter. <code>CASE</code> evaluates conditions in order and returns the first match; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/case-statement-in-sql-server/">the CASE statement in SQL Server</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    AdjustmentID,<br>    QtyChange,<br>    CASE<br>        WHEN QtyChange IS NULL THEN 'Unknown'<br>        WHEN QtyChange % 2 = 0 THEN 'Even'<br>        ELSE 'Odd'<br>    END AS Parity<br>FROM dbo.StockAdjustments;</code></pre>



<p class="wp-block-paragraph">The NULL check comes first so the row is labeled <code>Unknown</code> instead of falling into <code>ELSE</code> and being called odd. That mistake is common: any NULL that fails the even test would land in the odd bucket.</p>



<p class="wp-block-paragraph">For a simple two-way choice, <code>IIF</code> is shorter; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-iif-logical-function/">the IIF function</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT AdjustmentID,<br>       IIF(QtyChange % 2 = 0, 'Even', 'Odd') AS Parity<br>FROM dbo.StockAdjustments<br>WHERE QtyChange IS NOT NULL;</code></pre>



<p class="wp-block-paragraph"><code>IIF</code> is shorthand for a two-branch <code>CASE</code>. It cannot express an &#8220;Unknown&#8221; third case on its own, so filter NULLs out first, as above.</p>



<h3 class="wp-block-heading">The Bitwise Method</h3>



<p class="wp-block-paragraph">A <strong>bitwise AND</strong> (<code>&amp;</code>) compares the binary bits of two numbers. In binary, an odd number always has its last bit set to 1. So <code>value &amp; 1</code> returns 1 for odd numbers and 0 for even ones, including negatives, which SQL Server stores in two&#8217;s complement form.</p>



<pre class="wp-block-preformatted"><code>SELECT AdjustmentID, QtyChange,<br>       QtyChange &amp; 1 AS IsOdd<br>FROM dbo.StockAdjustments<br>WHERE QtyChange IS NOT NULL;</code></pre>



<p class="wp-block-paragraph">For -3, the result is 1, so the negative number is correctly seen as odd. This avoids the sign problem of <code>%</code>. The limits are that <code>&amp;</code> works only on integer types, not <code>DECIMAL</code>, and many readers find <code>% 2</code> clearer. I use bitwise checks in code where the data is always integer and I want one result for any sign, and <code>% 2 &lt;&gt; 0</code> everywhere else.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Method</th><th class="has-text-align-left" data-align="left">Handles negatives</th><th class="has-text-align-left" data-align="left">Works on decimals</th><th class="has-text-align-left" data-align="left">Readability</th></tr></thead><tbody><tr><td><code>% 2 = 0</code> (even test)</td><td>Yes</td><td>Only for whole values</td><td>High</td></tr><tr><td><code>% 2 = 1</code> (odd test)</td><td>No</td><td>No</td><td>High, but buggy</td></tr><tr><td><code>% 2 &lt;&gt; 0</code> (odd test)</td><td>Yes</td><td>Whole values</td><td>High</td></tr><tr><td><code>&amp; 1</code></td><td>Yes</td><td>No (integers only)</td><td>Medium</td></tr></tbody></table></figure>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I keep the bitwise form out of shared code unless I add a comment. A teammate who does not recognize <code>&amp; 1</code> may &#8220;fix&#8221; it later, so I write <code>-- 1 = odd</code> beside it.</p>
</blockquote>



<h3 class="wp-block-heading">Decimals, Text Columns, and Data Types</h3>



<p class="wp-block-paragraph">The modulo operator accepts integer, money, and numeric types. It rejects <code>FLOAT</code> and <code>REAL</code>, and it does not make sense for fractions: <code>7.5 % 2</code> returns 1.5, which is neither 0 nor 1. If a column stores decimals, decide what &#8220;even&#8221; means first. Many teams round or cast to an integer, or reject non-whole values.</p>



<pre class="wp-block-preformatted"><code>SELECT 7.5 % 2 AS DecimalRemainder,   <em>-- 1.5</em><br>       CAST(8.0 AS INT) % 2 AS WholeValue;  <em>-- 0</em></code></pre>



<p class="wp-block-paragraph">If the number is stored as text, convert it before testing. A <strong>data type</strong> tells SQL Server what kind of value a column holds. Use <code>TRY_CONVERT</code>, which returns NULL instead of an error when conversion fails; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/try_convert-function-in-sql-server/">TRY_CONVERT</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/cast-function-in-sql-server/">the CAST function</a>.</p>



<pre class="wp-block-preformatted">sql<code>SELECT Value,
       CASE
           WHEN TRY_CONVERT(INT, Value) IS NULL THEN 'Not a whole number'
           WHEN TRY_CONVERT(INT, Value) % 2 = 0 THEN 'Even'
           ELSE 'Odd'
       END AS Parity
FROM (VALUES ('12'), ('7'), ('abc'), ('3.5')) AS v(Value);</code></pre>



<p class="wp-block-paragraph">Without <code>TRY_CONVERT</code>, the text <code>'abc'</code> would raise a conversion error; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/error-converting-data-type-varchar-to-numeric/">the varchar to numeric conversion error</a>. Storing numbers in text columns causes these problems repeatedly, so fix the data type if you control the schema.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, &#8220;even or odd&#8221; bugs on imported data are usually text-cleanup bugs. Check with <code>TRY_CONVERT</code> first and fix the bad rows before the logic runs.</p>
</blockquote>



<h3 class="wp-block-heading">Count and Sum Even and Odd Values</h3>



<p class="wp-block-paragraph">Reports often need totals by parity. Combine <code>GROUP BY</code> with the parity test; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-count-function-in-sql-server/">how COUNT works</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-sum-function-in-sql-server/">the SUM function</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    CASE WHEN QtyChange % 2 = 0 THEN 'Even' ELSE 'Odd' END AS Parity,<br>    COUNT(*)        AS Rows_,<br>    SUM(QtyChange)  AS TotalQty<br>FROM dbo.StockAdjustments<br>WHERE QtyChange IS NOT NULL<br>GROUP BY CASE WHEN QtyChange % 2 = 0 THEN 'Even' ELSE 'Odd' END;</code></pre>



<p class="wp-block-paragraph">With the sample data, even rows are 10, -8, and 0, with 3 rows and a total of 2. Odd rows are 7, -3, and 15, also 3 rows with a total of 19. The <code>CASE</code> expression appears in both <code>SELECT</code> and <code>GROUP BY</code>, because SQL Server needs the same expression in each.</p>



<h3 class="wp-block-heading">Generate a List of Even and Odd Numbers</h3>



<p class="wp-block-paragraph">To produce a range of numbers, use <code>GENERATE_SERIES</code>, available in SQL Server 2022 and Azure SQL Database. It returns a sequence of numbers.</p>



<pre class="wp-block-preformatted"><code>SELECT value AS EvenNumber<br>FROM GENERATE_SERIES(1, 20)<br>WHERE value % 2 = 0;</code></pre>



<p class="wp-block-paragraph">This returns 2, 4, 6, up to 20. Its second argument is the stop value, and an optional third argument sets the step. You can skip filtering by stepping directly: <code>GENERATE_SERIES(2, 20, 2)</code> returns only the even numbers, which is more efficient.</p>



<p class="wp-block-paragraph">On older versions, use a recursive <strong>CTE</strong> (common table expression), a named temporary result you can query right away.</p>



<pre class="wp-block-preformatted"><code>WITH Numbers AS (<br>    SELECT 1 AS n<br>    UNION ALL<br>    SELECT n + 1 FROM Numbers WHERE n &lt; 20<br>)<br>SELECT n AS OddNumber<br>FROM Numbers<br>WHERE n % 2 &lt;&gt; 0<br>OPTION (MAXRECURSION 100);</code></pre>



<p class="wp-block-paragraph"><code>MAXRECURSION</code> sets a safety limit on how many times the CTE can call itself. The default is 100. Raise it deliberately for larger ranges, because it protects you from runaway loops. For more row-by-row logic, a loop is possible but slower; see <a href="https://sqlserverguides.com/while-loop-in-sql-server-stored-procedure/" target="_blank" rel="noopener">WHILE loops in stored procedures</a>. In my experience, set-based code like <code>GENERATE_SERIES</code> beats loops almost every time.</p>



<h3 class="wp-block-heading">Frequently Asked Questions</h3>



<h4 class="wp-block-heading">How do I check if a number is even or odd in SQL Server?</h4>



<p class="wp-block-paragraph">Use the modulo operator. <code>value % 2 = 0</code> means even, and <code>value % 2 &lt;&gt; 0</code> means odd. The <code>&lt;&gt; 0</code> form also works for negative numbers.</p>



<h4 class="wp-block-heading">Why does <code>% 2 = 1</code> miss some odd numbers?</h4>



<p class="wp-block-paragraph">In SQL Server, the sign of the result matches the sign of the dividend, so <code>-3 % 2</code> returns -1. Test for <code>&lt;&gt; 0</code> or use <code>ABS(value % 2) = 1</code>.</p>



<h4 class="wp-block-heading">Is zero even or odd in SQL?</h4>



<p class="wp-block-paragraph">Zero is even, because <code>0 % 2</code> returns 0. Your query will classify it as even.</p>



<h4 class="wp-block-heading">How do I get only even rows from a table?</h4>



<p class="wp-block-paragraph">If you mean rows at even positions, number them with <code>ROW_NUMBER()</code> and filter <code>rn % 2 = 0</code>. If you mean even ID values, filter <code>ID % 2 = 0</code>, but remember that gaps in an identity column make this different from alternating rows.</p>



<h4 class="wp-block-heading">Can I use an index to speed up even and odd filters?</h4>



<p class="wp-block-paragraph">Not directly, because <code>value % 2</code> is not sargable. A persisted computed column can be indexed, but a two-value column has low selectivity, so test the execution plan before adding it.</p>



<p class="wp-block-paragraph">You learned how to find even and odd numbers in SQL Server with modulo, bitwise AND, <code>CASE</code>, <code>GENERATE_SERIES</code>, and <code>ROW_NUMBER</code>, and how to handle negatives, NULLs, and text values. Test your logic against zero, negative, and NULL values, and give report users read-only access through an Entra identity. I hope you found this article helpful.</p>



<p class="wp-block-paragraph">You May Also Like</p>



<ul class="wp-block-list">
<li><a href="https://sqlserverguides.com/case-when-in-sql-server/" target="_blank" rel="noreferrer noopener">How to Use CASE WHEN in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/how-to-generate-sequence-number-in-sql-server/" target="_blank" rel="noreferrer noopener">How to Generate Sequence Number in SQL Server (4 Ways)</a></li>



<li><a href="https://sqlserverguides.com/how-to-use-avg-function-in-sql-server/" target="_blank" rel="noopener">How to use the AVG function in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/how-to-use-max-function-in-sql-server/" target="_blank" rel="noopener">How to use the MAX function in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/how-to-use-min-function-in-sql-server/" target="_blank" rel="noopener">How to use the MIN function in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/top-10-sql-server-commands-you-must-know/" target="_blank" rel="noopener">Top 10 SQL Server commands you must know</a></li>



<li><a href="https://sqlserverguides.com/sql-cheat-sheet/" target="_blank" rel="noopener">SQL cheat sheet</a></li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to Swap Two Column Values in SQL</title>
		<link>https://sqlserverguides.com/how-to-swap-two-column-values-in-sql/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Mon, 02 Nov 2026 06:15:57 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[How to Swap Two Column Values in SQL]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=24003</guid>

					<description><![CDATA[A customer success team once imported 40,000 contacts from a CRM export. A week later, support noticed that thousands of records greeted customers as &#8220;Dear Johnson,&#8221; with first and last names reversed. The developer who found it said, &#8220;I&#8217;ll just copy one column into the other.&#8221; He stopped when he realized the first UPDATE would ... <a title="How to Swap Two Column Values in SQL" class="read-more" href="https://sqlserverguides.com/how-to-swap-two-column-values-in-sql/" aria-label="Read more about How to Swap Two Column Values in SQL">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A customer success team once imported 40,000 contacts from a CRM export. A week later, support noticed that thousands of records greeted customers as &#8220;Dear Johnson,&#8221; with first and last names reversed. The developer who found it said, &#8220;I&#8217;ll just copy one column into the other.&#8221; He stopped when he realized the first <code>UPDATE</code> would overwrite the data he needed for the second.</p>



<p class="wp-block-paragraph">That is the classic trap behind <strong>how to swap two column values in SQL</strong>. In many languages you need a temporary variable. In SQL Server you can often do it with a single statement, if you understand how <code>UPDATE</code> evaluates its values. You also need a safety plan, because a bad swap overwrites data in place. This guide shows the safe one-statement method, previews, rollback options, and what to check in Azure SQL Database or SQL Server before touching production.</p>



<h2 class="wp-block-heading">Why a Swap Needs a Plan</h2>



<p class="wp-block-paragraph">Suppose you run two separate updates:</p>



<pre class="wp-block-preformatted"><code>UPDATE dbo.Contacts SET FirstName = LastName;  <em>-- dangerous</em><br>UPDATE dbo.Contacts SET LastName = FirstName;</code></pre>



<p class="wp-block-paragraph">After the first statement, <code>FirstName</code> already contains the last name, and the original first name is gone. The second statement copies the same value back. Both columns now hold the last name. This is permanent data loss unless you have a backup.</p>



<p class="wp-block-paragraph">SQL Server avoids this problem when you do both assignments in one <code>UPDATE</code>. The reason is a behavior often called <strong>all-at-once evaluation</strong>: every expression on the right side of <code>SET</code> reads the row&#8217;s original values, before any column is changed. That makes a swap clean and atomic, meaning it either fully happens or does not happen.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, the real risk is not the syntax. It is running a swap on the wrong rows. I always build the <code>WHERE</code> clause first, preview it with <code>SELECT</code>, and only then convert it to <code>UPDATE</code>.</p>
</blockquote>



<h3 class="wp-block-heading">Create a Practice Table</h3>



<p class="wp-block-paragraph">Work in a test database first. This example models the contact import problem. A <strong>schema</strong> is the structure of your tables, and a <strong>batch</strong> here means a group of rows loaded together.</p>



<pre class="wp-block-preformatted"><code>CREATE TABLE dbo.Contacts (<br>    ContactID   INT IDENTITY(1,1) PRIMARY KEY,<br>    FirstName   VARCHAR(50) NOT NULL,<br>    LastName    VARCHAR(50) NOT NULL,<br>    ImportBatch INT NOT NULL<br>);<br><br>INSERT INTO dbo.Contacts (FirstName, LastName, ImportBatch)<br>VALUES<br> ('Chen',    'Ava',   7),   <em>-- reversed</em><br> ('Ortiz',   'Ben',   7),   <em>-- reversed</em><br> ('Cara',    'Singh', 6),   <em>-- correct</em><br> ('Weber',   'Dan',   7),   <em>-- reversed</em><br> ('Eli',     'Brooks',6);   <em>-- correct</em></code></pre>



<p class="wp-block-paragraph">Batch 7 contains the reversed names. Batches 6 and below are correct, which is why the filter matters. If you need a refresher on creating tables, see how to <a rel="noopener" target="_blank" href="https://sqlserverguides.com/create-a-table-in-sql-server-management-studio-using-query/">create a table in SSMS using a query</a>.</p>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="931" height="387" src="https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-column-values-in-sql.jpg" alt="swap two column values in sql" class="wp-image-24004" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-column-values-in-sql.jpg 931w, https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-column-values-in-sql-300x125.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-column-values-in-sql-767x319.jpg 767w" sizes="auto, (max-width: 931px) 100vw, 931px" /></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="426" src="https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-column-values-in-sql-server-1024x426.jpg" alt="swap two column values in sql server" class="wp-image-24005" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-column-values-in-sql-server-1024x426.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-column-values-in-sql-server-767x319.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-column-values-in-sql-server-300x125.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-column-values-in-sql-server.jpg 1147w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<h3 class="wp-block-heading">How to Swap Two Column Values in SQL with One UPDATE</h3>



<p class="wp-block-paragraph">Preview first. A <code>SELECT</code> costs nothing and changes nothing.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    ContactID,<br>    FirstName AS CurrentFirst,<br>    LastName  AS CurrentLast,<br>    LastName  AS NewFirst,<br>    FirstName AS NewLast<br>FROM dbo.Contacts<br>WHERE ImportBatch = 7;</code></pre>



<p class="wp-block-paragraph">This shows what the data looks like now and what it will look like after the swap. Check a handful of rows by eye. Then run the update. <strong>Warning: this statement overwrites data.</strong></p>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="660" height="503" src="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Swap-Two-Column-Values-in-SQL.jpg" alt="How to Swap Two Column Values in SQL" class="wp-image-24006" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Swap-Two-Column-Values-in-SQL.jpg 660w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Swap-Two-Column-Values-in-SQL-300x229.jpg 300w" sizes="auto, (max-width: 660px) 100vw, 660px" /></figure>
</div>


<pre class="wp-block-preformatted"><code>BEGIN TRANSACTION;<br><br>UPDATE dbo.Contacts<br>SET FirstName = LastName,<br>    LastName  = FirstName<br>WHERE ImportBatch = 7;<br><br>SELECT @@ROWCOUNT AS RowsSwapped;</code></pre>



<p class="wp-block-paragraph">Here is what each part does:</p>



<ul class="wp-block-list">
<li><strong><code>BEGIN TRANSACTION</code></strong> starts a transaction, a unit of work you can undo until you commit.</li>



<li><strong>The <code>SET</code> list</strong> assigns each column the other&#8217;s original value. SQL Server evaluates both right-hand sides before writing.</li>



<li><strong><code>WHERE ImportBatch = 7</code></strong> limits the change to bad rows. Learn more in the <a href="https://sqlserverguides.com/sql-where-clause-tutorial/" target="_blank" rel="noopener">WHERE clause tutorial</a>.</li>



<li><strong><code>@@ROWCOUNT</code></strong> returns how many rows the previous statement changed. Compare it with the count from your preview.</li>
</ul>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="717" height="455" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-swap-two-values-in-same-column-in-sql.jpg" alt="how to swap two values in same column in sql" class="wp-image-24007" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-swap-two-values-in-same-column-in-sql.jpg 717w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-swap-two-values-in-same-column-in-sql-300x190.jpg 300w" sizes="auto, (max-width: 717px) 100vw, 717px" /></figure>
</div>


<p class="wp-block-paragraph">If the count and sample rows look right, run <code>COMMIT TRANSACTION;</code>. If anything looks wrong, run <code>ROLLBACK TRANSACTION;</code> and the table returns to its previous state.</p>



<p class="wp-block-paragraph">Leave the transaction open only briefly. An open transaction holds <strong>locks</strong>, which are protections that stop other sessions from changing the same rows. A long-running transaction can cause <strong>blocking</strong>, where other queries wait for your locks to be released.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I once watched a developer leave a transaction open over lunch in SSMS. Every report that touched the table hung. I now run <code>BEGIN TRANSACTION</code>, check the row count, and commit or roll back right away.</p>
</blockquote>



<h3 class="wp-block-heading">Preserve the Old Values Before You Swap</h3>



<p class="wp-block-paragraph">A swap is easy to reverse if you know the original values, and impossible to reverse if you do not. Back up the affected rows first. You can create a copy of the table or just the rows you plan to change. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/table-backup-in-sql-server/">table backup in SQL Server</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-copy-table-to-another-table/">copying a table to another table</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT ContactID, FirstName, LastName, ImportBatch<br>INTO dbo.Contacts_Backup_20261102<br>FROM dbo.Contacts<br>WHERE ImportBatch = 7;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="995" height="377" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-swap-two-column-values-in-sql-server.jpg" alt="how to swap two column values in sql server" class="wp-image-24008" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-swap-two-column-values-in-sql-server.jpg 995w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-swap-two-column-values-in-sql-server-300x114.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-swap-two-column-values-in-sql-server-765x290.jpg 765w" sizes="auto, (max-width: 995px) 100vw, 995px" /></figure>
</div>


<p class="wp-block-paragraph"><code>SELECT ... INTO</code> creates a new table and copies the rows. It is a quick safety copy, though it does not copy indexes or constraints.</p>



<p class="wp-block-paragraph">You can also record changes as they happen using the <code>OUTPUT</code> clause, which returns the old and new values from the same <code>UPDATE</code>.</p>



<pre class="wp-block-preformatted"><code>CREATE TABLE dbo.Contacts_SwapLog (<br>    ContactID INT NOT NULL,<br>    OldFirst  VARCHAR(50) NOT NULL,<br>    OldLast   VARCHAR(50) NOT NULL,<br>    NewFirst  VARCHAR(50) NOT NULL,<br>    NewLast   VARCHAR(50) NOT NULL,<br>    ChangedAt DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME()<br>);<br><br>UPDATE dbo.Contacts<br>SET FirstName = LastName,<br>    LastName  = FirstName<br>OUTPUT inserted.ContactID,<br>       deleted.FirstName, deleted.LastName,<br>       inserted.FirstName, inserted.LastName<br>INTO dbo.Contacts_SwapLog (ContactID, OldFirst, OldLast, NewFirst, NewLast)<br>WHERE ImportBatch = 7;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="411" src="https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-column-values-sql-server-1024x411.jpg" alt="swap two column values sql server" class="wp-image-24009" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-column-values-sql-server-1024x411.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-column-values-sql-server-300x120.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-column-values-sql-server-766x307.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-column-values-sql-server.jpg 1167w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="348" src="https://sqlserverguides.com/wp-content/uploads/2026/11/Swap-Two-Column-Values-SQL-1024x348.jpg" alt="Swap Two Column Values SQL" class="wp-image-24010" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/Swap-Two-Column-Values-SQL-1024x348.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/Swap-Two-Column-Values-SQL-768x261.jpg 768w, https://sqlserverguides.com/wp-content/uploads/2026/11/Swap-Two-Column-Values-SQL-300x102.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/Swap-Two-Column-Values-SQL.jpg 1412w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">In <code>OUTPUT</code>, <code>deleted</code> holds the row values before the change and <code>inserted</code> holds them after. This gives you an audit trail with no extra queries. If someone later asks what changed, you have the answer.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, an <code>OUTPUT ... INTO</code> log table takes two minutes to write and has saved me hours. It also gives auditors a clear record of who changed what and when.</p>
</blockquote>



<h3 class="wp-block-heading">Swap Columns With Different Data Types or Constraints</h3>



<p class="wp-block-paragraph">The one-statement swap is simple when both columns are the same type. Real tables are messier.</p>



<h4 class="wp-block-heading">Mismatched Data Types</h4>



<p class="wp-block-paragraph">If one column is <code>INT</code> and the other is <code>VARCHAR</code>, SQL Server must convert values. Conversion can fail on non-numeric text. Use explicit conversion and test it before updating. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/cast-function-in-sql-server/">the CAST function</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/try_convert-function-in-sql-server/">TRY_CONVERT</a>, which returns NULL instead of an error. If you hit a failure, read about the <a rel="noopener" target="_blank" href="https://sqlserverguides.com/error-converting-data-type-varchar-to-numeric/">varchar to numeric conversion error</a>.</p>



<pre class="wp-block-preformatted"><code><em>-- Find rows that would fail before you update anything</em><br>SELECT ContactID, FirstName<br>FROM dbo.Contacts<br>WHERE TRY_CONVERT(INT, FirstName) IS NULL;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="322" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-swap-two-values-sql-1024x322.jpg" alt="how to swap two values sql" class="wp-image-24011" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-swap-two-values-sql-1024x322.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-swap-two-values-sql-300x94.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-swap-two-values-sql-766x241.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-swap-two-values-sql.jpg 1087w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<h4 class="wp-block-heading">Different Lengths</h4>



<p class="wp-block-paragraph">If one column is <code>VARCHAR(20)</code> and the other is <code>VARCHAR(100)</code>, moving a long value into the short column fails. SQL Server raises a truncation error; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/string-or-binary-data-would-be-truncated-in-table/">string or binary data would be truncated</a>. Check the longest value first with <code>MAX(LEN(column))</code>.</p>



<h4 class="wp-block-heading">NOT NULL and CHECK Constraints</h4>



<p class="wp-block-paragraph">A <strong>constraint</strong> is a rule that SQL Server enforces on data. If one column allows NULL and the other does not, swapping a NULL into the <code>NOT NULL</code> column fails. A <code>CHECK</code> constraint can also block a swap. For example, a shipment table might require <code>DeliveryDate &gt;= PickupDate</code>. If the dates were reversed by mistake, swapping them fixes the data, but swapping correct rows would break the rule. Read more in <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-constraints/">SQL constraints</a>.</p>



<h4 class="wp-block-heading">Triggers</h4>



<p class="wp-block-paragraph">A <strong>trigger</strong> is code that runs automatically when data changes. An update trigger will fire during your swap and may write audit rows, send events, or block the change. Know what exists before you update. Review <a rel="noopener" target="_blank" href="https://sqlserverguides.com/trigger-sql-server/">triggers in SQL Server</a>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I always check for triggers and check constraints on the table before a bulk change. Finding out halfway through a production update is a bad time to learn that a trigger sends an email per row.</p>
</blockquote>



<h4 class="wp-block-heading">Conditional Swaps and Large Tables</h4>



<p class="wp-block-paragraph">Sometimes you only want to swap when a rule is true. For example, swap only when the &#8220;first name&#8221; looks like it belongs in the last name column. Use <code>CASE</code>, which returns different values depending on conditions. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/case-statement-in-sql-server/">the CASE statement</a>.</p>



<pre class="wp-block-preformatted"><code>UPDATE dbo.Contacts<br>SET FirstName = CASE WHEN ImportBatch = 7 THEN LastName  ELSE FirstName END,<br>    LastName  = CASE WHEN ImportBatch = 7 THEN FirstName ELSE LastName  END;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="252" src="https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-values-in-same-column-in-sql-1024x252.jpg" alt="swap two values in same column in sql" class="wp-image-24012" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-values-in-same-column-in-sql-1024x252.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-values-in-same-column-in-sql-300x74.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-values-in-same-column-in-sql-768x189.jpg 768w, https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-values-in-same-column-in-sql.jpg 1430w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">Both <code>CASE</code> expressions use the original row values, so the swap still works. The <code>ELSE</code> branches keep other rows unchanged. A <code>WHERE</code> clause is usually better, though, because it avoids touching rows you do not need to change. Every touched row writes to the <strong>transaction log</strong>, the file that records changes so SQL Server can recover and roll back.</p>



<h4 class="wp-block-heading">Update in Batches</h4>



<p class="wp-block-paragraph">For millions of rows, one giant update can fill the log, hold locks for a long time, and block users. Update in batches instead. You can control the loop with a <code>WHILE</code> statement; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/while-loop-in-sql-server-stored-procedure/">WHILE loops in a stored procedure</a>.</p>



<pre class="wp-block-preformatted"><code>DECLARE @BatchSize INT = 5000;<br>DECLARE @Rows INT = 1;<br><br>WHILE @Rows > 0<br>BEGIN<br>    UPDATE TOP (@BatchSize) dbo.Contacts<br>    SET FirstName = LastName,<br>        LastName  = FirstName,<br>        ImportBatch = -7          <em>-- marks the row as fixed</em><br>    WHERE ImportBatch = 7;<br><br>    SET @Rows = @@ROWCOUNT;<br>END;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="714" src="https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-values-in-same-column-sql-1024x714.jpg" alt="swap two values in same column sql" class="wp-image-24013" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-values-in-same-column-sql-1024x714.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-values-in-same-column-sql-768x535.jpg 768w, https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-values-in-same-column-sql-300x209.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/swap-two-values-in-same-column-sql.jpg 1151w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">This is an important design point. A swap is <strong>not idempotent</strong>, meaning running it twice returns the data to the original wrong state. Here, the batch marker changes from 7 to -7 as each row is fixed, so the loop never picks the same row twice. Without a marker, a retry could swap rows back. After the loop, update the marker to its final value if needed.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, a swap without a &#8220;fixed&#8221; flag is a bug waiting for a retry. I either use a marker column like this or wrap everything in one transaction that I can cleanly roll back.</p>
</blockquote>



<h3 class="wp-block-heading">Swap Values Between Two Tables or Rows</h3>



<p class="wp-block-paragraph">Sometimes the values you need to swap live in different places. If the correct values are in another table, use an update with a join. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-update-table-from-another-table/">updating a table from another table</a>. For loading corrected data into a staging copy first, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-insert-into-select-statement/">INSERT INTO &#8230; SELECT</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-create-temp-table-from-select/">creating a temp table from a SELECT</a>.</p>



<p class="wp-block-paragraph">If the goal is to swap the column names instead of the values, that is a metadata change. You can rename columns, but it breaks code, views, and reports that use the old names. Review <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-rename-column-name-in-sql-server/">how to rename a column in SQL Server</a> before trying that on a shared system. For large tables, a name swap is fast because it changes no data, but it is riskier for applications. Check dependencies first.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I avoid renaming columns in a shared database unless I have a list of every query and report that uses them. Swapping data is reversible with a backup. A silent app break is much harder to trace.</p>
</blockquote>



<h3 class="wp-block-heading">Frequently Asked Questions</h3>



<h4 class="wp-block-heading">How do I swap two column values in SQL Server?</h4>



<p class="wp-block-paragraph">Use one <code>UPDATE</code> with both assignments, such as <code>SET A = B, B = A</code>. SQL Server reads the original row values for both, so no temporary column is needed. Preview with <code>SELECT</code> and use a <code>WHERE</code> clause.</p>



<h4 class="wp-block-heading">Can I swap columns without a temporary variable?</h4>



<p class="wp-block-paragraph">Yes, in SQL Server a single <code>UPDATE</code> handles it. Some other database systems evaluate assignments differently, so test on sample data if you use another platform.</p>



<h4 class="wp-block-heading">How do I swap two columns with different data types?</h4>



<p class="wp-block-paragraph">Convert explicitly with <code>CAST</code> or <code>TRY_CONVERT</code> and test for failures first. Also compare column lengths and nullability, because truncation and NULL violations are common errors.</p>



<h4 class="wp-block-heading">Can I undo a column swap?</h4>



<p class="wp-block-paragraph">Yes, if you saved the original data, used a transaction that you can roll back, or have a backup. Azure SQL Database also supports point-in-time restore within your retention period.</p>



<h4 class="wp-block-heading">Is swapping column names the same as swapping values?</h4>



<p class="wp-block-paragraph">No. Renaming changes metadata only and can break queries, views, and apps that use the old names. Swapping values changes the data in each row.</p>



<p class="wp-block-paragraph">You learned how to swap two column values in SQL Server with a single <code>UPDATE</code>, why it avoids data loss, and how to preview, log, batch, and recover from the change. Before any swap on real data, save the original values and confirm you can restore them. I hope you found this article helpful.</p>



<p class="wp-block-paragraph">You May Also Like</p>



<ul class="wp-block-list">
<li><a href="https://sqlserverguides.com/how-to-alter-table-in-sql-server/" target="_blank" rel="noopener">How to alter a table in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/how-to-combine-two-columns-in-sql/" target="_blank" rel="noopener">How to combine two columns in SQL</a></li>



<li><a href="https://sqlserverguides.com/sql-server-change-column-to-not-null/" target="_blank" rel="noopener">How to change a column to NOT NULL in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/sql-server-check-if-column-exists/" target="_blank" rel="noopener">How to check if a column exists in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/sql-server-find-table-with-column-name/" target="_blank" rel="noopener">How to find a table with a column name in SQL Server</a></li>
</ul>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>SQL Query to Count Employees in Each Department</title>
		<link>https://sqlserverguides.com/sql-query-to-count-employees-in-each-department/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Sun, 01 Nov 2026 17:37:21 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[SQL Query to Count Employees in Each Department]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=23992</guid>

					<description><![CDATA[An HR analyst once sent me a headcount report that looked fine until the Legal director spotted the problem. Legal had three open roles and no employees, and the department was missing from the report entirely. A second department showed one extra person who didn&#8217;t exist. The query was short, and both mistakes came from ... <a title="SQL Query to Count Employees in Each Department" class="read-more" href="https://sqlserverguides.com/sql-query-to-count-employees-in-each-department/" aria-label="Read more about SQL Query to Count Employees in Each Department">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">An HR analyst once sent me a headcount report that looked fine until the Legal director spotted the problem. Legal had three open roles and no employees, and the department was missing from the report entirely. A second department showed one extra person who didn&#8217;t exist. The query was short, and both mistakes came from two small choices: an <code>INNER JOIN</code> and a <code>COUNT(*)</code>.</p>



<p class="wp-block-paragraph">That is why the <strong>SQL query to count employees in each department</strong> deserves more care than its size suggests. The basic version is three lines. A reliable version has to handle empty departments, unassigned employees, inactive staff, NULLs, and growing tables. This guide builds the query step by step in T-SQL, then covers performance, reusable objects, and secure access in Azure SQL Database or SQL Server, so you can ship a headcount report you can trust.</p>



<h2 class="wp-block-heading">Set Up the Employee and Department Tables</h2>



<p class="wp-block-paragraph">A <strong>schema</strong> is the structure of your tables and how they relate. Start with two tables: one row per department, and one row per employee.</p>



<pre class="wp-block-preformatted"><code>CREATE TABLE dbo.Departments (<br>    DepartmentID   INT IDENTITY(1,1) PRIMARY KEY,<br>    DepartmentName VARCHAR(100) NOT NULL UNIQUE<br>);<br><br>CREATE TABLE dbo.Employees (<br>    EmployeeID   INT IDENTITY(1,1) PRIMARY KEY,<br>    FullName     VARCHAR(150) NOT NULL,<br>    DepartmentID INT NULL<br>        REFERENCES dbo.Departments(DepartmentID),<br>    HireDate     DATE NOT NULL,<br>    IsActive     BIT NOT NULL DEFAULT 1<br>);</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="978" height="615" src="https://sqlserverguides.com/wp-content/uploads/2026/11/Query-to-Count-Employees-in-Each-Department-sql.jpg" alt="" class="wp-image-23993" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/Query-to-Count-Employees-in-Each-Department-sql.jpg 978w, https://sqlserverguides.com/wp-content/uploads/2026/11/Query-to-Count-Employees-in-Each-Department-sql-300x189.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/Query-to-Count-Employees-in-Each-Department-sql-766x482.jpg 766w" sizes="auto, (max-width: 978px) 100vw, 978px" /></figure>



<p class="wp-block-paragraph"><code>DepartmentID</code> in <code>Employees</code> is a <strong>foreign key</strong>, a rule that stops an employee from pointing to a department that does not exist. It allows NULL here on purpose, because new hires are sometimes unassigned. Review the basics in <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-create-table-with-primary-key/">creating a table with a primary key</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/foreign-key-in-sql-server/">foreign keys in SQL Server</a>.</p>



<p class="wp-block-paragraph">Now load sample data. Run this only on a test database, because it inserts rows.</p>



<pre class="wp-block-preformatted"><code>INSERT INTO dbo.Departments (DepartmentName)<br>VALUES ('Engineering'), ('Sales'), ('HR'), ('Legal');<br><br>INSERT INTO dbo.Employees (FullName, DepartmentID, HireDate, IsActive)<br>VALUES<br> ('Ava Chen',   1, '2022-03-01', 1),<br> ('Ben Ortiz',  1, '2023-06-15', 1),<br> ('Cara Singh', 1, '2021-01-10', 0),<br> ('Dan Weber',  2, '2024-02-20', 1),<br> ('Eli Brooks', 2, '2020-09-05', 1),<br> ('Fay Nolan',  3, '2023-11-30', 1),<br> ('Gus Reyes', NULL, '2026-10-01', 1);</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="539" src="https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-count-number-of-employees-in-each-department-1024x539.jpg" alt="sql query to count number of employees in each department" class="wp-image-23994" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-count-number-of-employees-in-each-department-1024x539.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-count-number-of-employees-in-each-department-300x158.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-count-number-of-employees-in-each-department-768x404.jpg 768w, https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-count-number-of-employees-in-each-department.jpg 1323w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">This gives you Engineering with three people (one inactive), Sales with two, HR with one, Legal with none, and one employee with no department. Those edge cases are deliberate. Each one breaks a careless query.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, a tiny seed dataset with an empty department, a NULL department, and an inactive employee finds more headcount bugs than a million random rows. I build one before writing any report query.</p>
</blockquote>



<h3 class="wp-block-heading">Basic SQL Query to Count Employees in Each Department</h3>



<p class="wp-block-paragraph">The core idea is <code>GROUP BY</code> plus <code>COUNT</code>. <code>GROUP BY</code> splits rows into buckets, and <code>COUNT</code> is an <strong>aggregate function</strong> that returns one number per bucket. For the fundamentals, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-group-by-clause-in-sql-server/">how to use GROUP BY in SQL Server</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-count-function-in-sql-server/">the COUNT function</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    DepartmentID,<br>    COUNT(*) AS EmployeeCount<br>FROM dbo.Employees<br>GROUP BY DepartmentID;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="638" height="388" src="https://sqlserverguides.com/wp-content/uploads/2026/11/SQL-Query-to-Count-Employees-in-Each-Department.jpg" alt="SQL Query to Count Employees in Each Department" class="wp-image-23995" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/SQL-Query-to-Count-Employees-in-Each-Department.jpg 638w, https://sqlserverguides.com/wp-content/uploads/2026/11/SQL-Query-to-Count-Employees-in-Each-Department-300x182.jpg 300w" sizes="auto, (max-width: 638px) 100vw, 638px" /></figure>
</div>


<p class="wp-block-paragraph">This groups employees by <code>DepartmentID</code> and counts the rows in each group. It works, but the output is just numbers. Also, the group with a NULL <code>DepartmentID</code> appears as its own row, which is correct but confusing without a label.</p>



<p class="wp-block-paragraph">To show department names, join to <code>Departments</code>. A <strong>join</strong> combines rows from two tables based on a matching column. See this <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-inner-join-tutorial/">inner join tutorial</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    d.DepartmentName,<br>    COUNT(e.EmployeeID) AS EmployeeCount<br>FROM dbo.Departments AS d<br>INNER JOIN dbo.Employees AS e<br>    ON e.DepartmentID = d.DepartmentID<br>GROUP BY d.DepartmentName<br>ORDER BY EmployeeCount DESC;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="832" height="457" src="https://sqlserverguides.com/wp-content/uploads/2026/11/write-a-sql-query-to-count-the-number-of-employees-in-each-department.jpg" alt="write a sql query to count the number of employees in each department" class="wp-image-23996" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/write-a-sql-query-to-count-the-number-of-employees-in-each-department.jpg 832w, https://sqlserverguides.com/wp-content/uploads/2026/11/write-a-sql-query-to-count-the-number-of-employees-in-each-department-766x421.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/11/write-a-sql-query-to-count-the-number-of-employees-in-each-department-300x165.jpg 300w" sizes="auto, (max-width: 832px) 100vw, 832px" /></figure>
</div>


<p class="wp-block-paragraph">The aliases <code>d</code> and <code>e</code> shorten the table names; read about <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-aliases/">SQL aliases</a> if they are new. <code>ORDER BY ... DESC</code> puts the largest department first; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/order-by-clause-in-sql-server/">ORDER BY in SQL Server</a>.</p>



<p class="wp-block-paragraph">This query is the one that hid the Legal department. An <code>INNER JOIN</code> returns only departments that have matching employees, so empty ones vanish.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I treat any headcount report built on an <code>INNER JOIN</code> as suspect until I have tested it with an empty department. That single test catches the most common version of this bug.</p>
</blockquote>



<h3 class="wp-block-heading">SQL Query to Count Employees in Each Department, Including Empty Ones</h3>



<p class="wp-block-paragraph">Use a <code>LEFT JOIN</code>, which keeps every row from the left table even when there is no match on the right. Read more in <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-left-join/">SQL LEFT JOIN</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    d.DepartmentName,<br>    COUNT(e.EmployeeID) AS EmployeeCount<br>FROM dbo.Departments AS d<br>LEFT JOIN dbo.Employees AS e<br>    ON e.DepartmentID = d.DepartmentID<br>GROUP BY d.DepartmentName<br>ORDER BY d.DepartmentName;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="837" height="484" src="https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-count-employees-department-wise.jpg" alt="sql query to count employees department wise" class="wp-image-23997" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-count-employees-department-wise.jpg 837w, https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-count-employees-department-wise-768x444.jpg 768w, https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-count-employees-department-wise-300x173.jpg 300w" sizes="auto, (max-width: 837px) 100vw, 837px" /></figure>
</div>


<p class="wp-block-paragraph">Legal now appears with a count of 0. The key detail is <code>COUNT(e.EmployeeID)</code>. When you count a specific column, SQL Server skips NULLs, and a department with no employees produces a single row where every <code>e.</code> column is NULL. If you wrote <code>COUNT(*)</code> instead, that one placeholder row would count as 1, and Legal would show one employee.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Expression</th><th>What it counts</th><th>Legal result</th></tr></thead><tbody><tr><td><code>COUNT(*)</code></td><td>Every row in the group</td><td>1 (wrong)</td></tr><tr><td><code>COUNT(e.EmployeeID)</code></td><td>Non-NULL employee IDs</td><td>0 (correct)</td></tr><tr><td><code>COUNT(DISTINCT e.EmployeeID)</code></td><td>Unique non-NULL IDs</td><td>0 (correct)</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">You rarely need <code>DISTINCT</code> here because <code>EmployeeID</code> is already unique. It matters when your join can repeat employees, for example when joining to a table of project assignments. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-count-distinct/">COUNT DISTINCT</a> for details, and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-null/">how SQL treats NULL</a> for why the counts behave this way.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I make <code>COUNT(column_from_the_right_table)</code> a habit in every left-join count. It reads as the intent, which is to count matches, and it prevents the phantom row problem.</p>
</blockquote>



<h3 class="wp-block-heading">Filter Active Employees Without Breaking the Join</h3>



<p class="wp-block-paragraph">Most reports should count only active staff. The trap is where you put the filter. Putting it in <code>WHERE</code> quietly turns the <code>LEFT JOIN</code> back into an inner join.</p>



<pre class="wp-block-preformatted"><code><em>-- Wrong: Legal disappears again</em><br>SELECT d.DepartmentName, COUNT(e.EmployeeID) AS EmployeeCount<br>FROM dbo.Departments AS d<br>LEFT JOIN dbo.Employees AS e ON e.DepartmentID = d.DepartmentID<br>WHERE e.IsActive = 1<br>GROUP BY d.DepartmentName;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="311" src="https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-get-count-of-employees-in-each-department-1024x311.jpg" alt="sql query to get count of employees in each department" class="wp-image-23998" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-get-count-of-employees-in-each-department-1024x311.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-get-count-of-employees-in-each-department-300x91.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-get-count-of-employees-in-each-department-766x233.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-get-count-of-employees-in-each-department.jpg 1220w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">Why does this fail? <code>WHERE</code> runs after the join. For Legal, <code>e.IsActive</code> is NULL, and <code>NULL = 1</code> is not true, so the row is removed. Move the condition into the <code>ON</code> clause so it applies before unmatched rows are added back.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    d.DepartmentName,<br>    COUNT(e.EmployeeID) AS ActiveEmployees<br>FROM dbo.Departments AS d<br>LEFT JOIN dbo.Employees AS e<br>    ON e.DepartmentID = d.DepartmentID<br>   AND e.IsActive = 1<br>GROUP BY d.DepartmentName<br>ORDER BY ActiveEmployees DESC;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="857" height="531" src="https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-find-count-of-employees-in-each-department.jpg" alt="sql query to find count of employees in each department" class="wp-image-23999" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-find-count-of-employees-in-each-department.jpg 857w, https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-find-count-of-employees-in-each-department-300x186.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-find-count-of-employees-in-each-department-767x475.jpg 767w" sizes="auto, (max-width: 857px) 100vw, 857px" /></figure>
</div>


<p class="wp-block-paragraph">With the sample data, the result is Engineering 2, Sales 2, HR 1, and Legal 0. Cara Singh is inactive, so she is excluded. For a refresher on filtering rules, read <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-join-vs-where/">JOIN vs. WHERE</a>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, &#8220;my left join lost rows&#8221; is almost always a right-table filter sitting in <code>WHERE</code>. When I review code, that is the first place I look.</p>
</blockquote>



<h3 class="wp-block-heading">Find Departments Above or Below a Size</h3>



<p class="wp-block-paragraph">To show only departments that meet a size rule, use <code>HAVING</code>. It filters groups after aggregation, while <code>WHERE</code> filters rows before it. The difference is explained in <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-having-vs-where/">HAVING vs. WHERE</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    d.DepartmentName,<br>    COUNT(e.EmployeeID) AS ActiveEmployees<br>FROM dbo.Departments AS d<br>LEFT JOIN dbo.Employees AS e<br>    ON e.DepartmentID = d.DepartmentID<br>   AND e.IsActive = 1<br>GROUP BY d.DepartmentName<br>HAVING COUNT(e.EmployeeID) >= 2;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="897" height="497" src="https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-find-the-count-of-employees-working-in-each-department.jpg" alt="sql query to find the count of employees working in each department" class="wp-image-24000" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-find-the-count-of-employees-working-in-each-department.jpg 897w, https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-find-the-count-of-employees-working-in-each-department-300x166.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/sql-query-to-find-the-count-of-employees-working-in-each-department-767x425.jpg 767w" sizes="auto, (max-width: 897px) 100vw, 897px" /></figure>
</div>


<p class="wp-block-paragraph">This returns only departments with at least two active employees. Change the condition to <code>= 0</code> to list empty departments, which is useful for cleaning up stale data. To get just the biggest department, combine the count with <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-top-clause/">the TOP clause</a>.</p>



<h3 class="wp-block-heading">Show Unassigned Employees and Percentages</h3>



<p class="wp-block-paragraph">Gus Reyes has no department, so he does not appear in any of the queries above. A report that silently drops people is a data quality problem. Handle it explicitly with <code>COALESCE</code>, which returns the first non-NULL value; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-coalesce-function/">the COALESCE function</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    COALESCE(d.DepartmentName, 'Unassigned') AS DepartmentName,<br>    COUNT(*) AS ActiveEmployees,<br>    CAST(100.0 * COUNT(*) / SUM(COUNT(*)) OVER () AS DECIMAL(5,1)) AS PercentOfTotal<br>FROM dbo.Employees AS e<br>LEFT JOIN dbo.Departments AS d<br>    ON d.DepartmentID = e.DepartmentID<br>WHERE e.IsActive = 1<br>GROUP BY COALESCE(d.DepartmentName, 'Unassigned');</code></pre>



<p class="wp-block-paragraph">Here the left table is <code>Employees</code>, so no employee can be lost. Using <code>COUNT(*)</code> is safe, because every row represents a real employee. The expression <code>SUM(COUNT(*)) OVER ()</code> is a <strong>window function</strong>, which calculates across rows without collapsing them. It sums all group counts to give the total headcount, so each department&#8217;s share is easy to compute. Learn more in <a rel="noopener" target="_blank" href="https://sqlserverguides.com/window-functions-in-sql-server/">window functions in SQL Server</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-over-clause/">the OVER clause</a>.</p>



<p class="wp-block-paragraph">The trade-off: this version will not show empty departments, because it starts from <code>Employees</code>. If you need both empty departments and unassigned employees, use a <code>FULL OUTER JOIN</code> or <code>UNION ALL</code> the two result sets. I usually run the unassigned check as a separate data-quality query instead.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I add an &#8220;Unassigned&#8221; row to headcount reports on purpose. Finance trusts a report more when its total matches the payroll count.</p>
</blockquote>



<h3 class="wp-block-heading">Make the Query Reusable and Fast</h3>



<p class="wp-block-paragraph">Once the logic is correct, save it. A <strong>view</strong> is a stored query you select from like a table. Read about <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-views/">SQL Server views</a>.</p>



<pre class="wp-block-preformatted"><code>CREATE OR ALTER VIEW dbo.vw_DepartmentHeadcount<br>AS<br>SELECT<br>    d.DepartmentID,<br>    d.DepartmentName,<br>    COUNT(e.EmployeeID) AS ActiveEmployees<br>FROM dbo.Departments AS d<br>LEFT JOIN dbo.Employees AS e<br>    ON e.DepartmentID = d.DepartmentID<br>   AND e.IsActive = 1<br>GROUP BY d.DepartmentID, d.DepartmentName;</code></pre>



<p class="wp-block-paragraph">Now every report reads from one definition, so &#8220;headcount&#8221; means the same thing everywhere. The view exposes only counts, not employee names, which also limits what a report user can see.</p>



<h4 class="wp-block-heading">Add an Index</h4>



<p class="wp-block-paragraph">An <strong>index</strong> is a sorted structure that helps SQL Server find rows without reading the whole table. For counting by department, an index on the foreign key column helps. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-create-index-in-sql-server/">how to create an index in SQL Server</a>.</p>



<pre class="wp-block-preformatted"><code>CREATE NONCLUSTERED INDEX IX_Employees_Department_Active<br>ON dbo.Employees (DepartmentID)<br>INCLUDE (IsActive);</code></pre>



<p class="wp-block-paragraph">The index lets SQL Server count rows from a narrow, sorted structure instead of scanning full employee rows. <code>INCLUDE (IsActive)</code> carries the filter column so the query does not need extra lookups. The trade-off is slightly slower inserts and updates, plus extra storage. On a table with a few hundred rows, you won&#8217;t notice a benefit. It starts to matter at hundreds of thousands of rows or more.</p>



<p class="wp-block-paragraph">Check the actual execution plan before and after, and keep statistics current. For more tuning ideas, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-query-optimization-tips/">SQL query optimization tips</a>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I enable Query Store in Azure SQL Database before tuning anything. It keeps runtime history, so I can show that an index helped instead of assuming it did.</p>
</blockquote>



<h3 class="wp-block-heading">Frequently Asked Questions</h3>



<h4 class="wp-block-heading">What is the SQL query to count employees in each department?</h4>



<p class="wp-block-paragraph">Join employees to departments, then use <code>GROUP BY</code> on the department name with <code>COUNT</code>. For example, <code>SELECT d.DepartmentName, COUNT(e.EmployeeID) FROM Departments d LEFT JOIN Employees e ON e.DepartmentID = d.DepartmentID GROUP BY d.DepartmentName</code>. The left join keeps empty departments.</p>



<h4 class="wp-block-heading">Why does my query show 1 for a department with no employees?</h4>



<p class="wp-block-paragraph">You are using <code>COUNT(*)</code> after a <code>LEFT JOIN</code>. The unmatched row still counts as a row. Use <code>COUNT(e.EmployeeID)</code> so NULLs are skipped.</p>



<h4 class="wp-block-heading">How do I count only active employees?</h4>



<p class="wp-block-paragraph">Put the active condition in the <code>ON</code> clause of the left join, such as <code>AND e.IsActive = 1</code>. If you put it in <code>WHERE</code>, departments with no active employees disappear.</p>



<h4 class="wp-block-heading">How do I list departments with more than five employees?</h4>



<p class="wp-block-paragraph">Add a <code>HAVING COUNT(e.EmployeeID) &gt; 5</code> clause after <code>GROUP BY</code>. <code>HAVING</code> filters after the counts are calculated, which <code>WHERE</code> cannot do.</p>



<h4 class="wp-block-heading">How can I make this query faster?</h4>



<p class="wp-block-paragraph">Index the <code>DepartmentID</code> column in <code>Employees</code>, keep statistics current, and read the actual execution plan to confirm the index is used. For repeated reports, store the logic in a view so every consumer uses the same tuned query.</p>



<p class="wp-block-paragraph">You now know how to write a SQL query to count employees in each department using <code>GROUP BY</code>, <code>LEFT JOIN</code>, <code>COUNT</code> on the right column, and <code>HAVING</code>, and how to avoid the empty-department and NULL traps. Test every headcount query with an empty department and an unassigned employee, and give report users read access to a view, not the employee table. I hope you found this article helpful.</p>



<p class="wp-block-paragraph">You May Also Like</p>



<ul class="wp-block-list">
<li><a href="https://sqlserverguides.com/sql-select-query-examples/" target="_blank" rel="noopener">SQL SELECT query examples</a></li>



<li><a href="https://sqlserverguides.com/how-to-use-sum-function-in-sql-server/" target="_blank" rel="noopener">How to use the SUM function in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/how-to-use-avg-function-in-sql-server/" target="_blank" rel="noopener">How to use the AVG function in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/how-to-use-max-function-in-sql-server/" target="_blank" rel="noopener">How to use the MAX function in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/sql-cheat-sheet/" target="_blank" rel="noopener">SQL cheat sheet</a></li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to Calculate Total Sales in SQL</title>
		<link>https://sqlserverguides.com/how-to-calculate-total-sales-in-sql/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Sun, 01 Nov 2026 07:50:01 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[How to Calculate Total Sales in SQL]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=23982</guid>

					<description><![CDATA[A finance manager once sent me two dashboard numbers for the same month. They were $38,000 apart, and both came from &#8220;total sales&#8221; queries. One query summed order headers, and the other joined to order lines and counted each header total once per line. Nobody had been careless. The query had simply never been checked ... <a title="How to Calculate Total Sales in SQL" class="read-more" href="https://sqlserverguides.com/how-to-calculate-total-sales-in-sql/" aria-label="Read more about How to Calculate Total Sales in SQL">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A finance manager once sent me two dashboard numbers for the same month. They were $38,000 apart, and both came from &#8220;total sales&#8221; queries. One query summed order headers, and the other joined to order lines and counted each header total once per line. Nobody had been careless. The query had simply never been checked against how the data was modeled.</p>



<p class="wp-block-paragraph">That is the real challenge behind <strong>how to calculate total sales in SQL</strong>. The <code>SUM</code> function is easy. Choosing the right rows, avoiding duplicate counting, handling NULLs, and keeping the query fast on millions of orders is the hard part. This guide walks through a complete example with T-SQL, then covers security, performance, and Azure SQL considerations, so you can build a total sales query you can trust.</p>



<h2 class="wp-block-heading">Set Up a Realistic Sales Schema</h2>



<p class="wp-block-paragraph">A <strong>schema</strong> here means the design of your tables and how they relate. Good totals start with good structure. Use a simple e-commerce model with orders and order lines.</p>



<pre class="wp-block-preformatted"><code>CREATE TABLE dbo.Orders (<br>    OrderID     INT IDENTITY(1,1) PRIMARY KEY,<br>    CustomerID  INT NOT NULL,<br>    OrderDate   DATETIME2(0) NOT NULL,<br>    Status      VARCHAR(20) NOT NULL   <em>-- Completed, Cancelled, Returned</em><br>);<br><br>CREATE TABLE dbo.OrderItems (<br>    OrderItemID INT IDENTITY(1,1) PRIMARY KEY,<br>    OrderID     INT NOT NULL<br>        REFERENCES dbo.Orders(OrderID),<br>    ProductID   INT NOT NULL,<br>    Quantity    INT NOT NULL,<br>    UnitPrice   DECIMAL(10,2) NOT NULL,<br>    Discount    DECIMAL(10,2) NULL      <em>-- amount off the line</em><br>);</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="590" src="https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-total-sales-in-sql-1024x590.jpg" alt="calculate total sales in sql" class="wp-image-23983" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-total-sales-in-sql-1024x590.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-total-sales-in-sql-767x442.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-total-sales-in-sql-300x173.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-total-sales-in-sql.jpg 1380w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">This creates two tables. <code>Orders</code> holds one row per order, and <code>OrderItems</code> holds one row per product on that order. The <code>REFERENCES</code> clause creates a <strong>foreign key</strong>, a rule that stops an item from pointing to an order that does not exist. If you want to review these basics, see how to <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-create-table-with-primary-key/">create a table with a primary key</a> and how a <a rel="noopener" target="_blank" href="https://sqlserverguides.com/foreign-key-in-sql-server/">foreign key works in SQL Server</a>.</p>



<p class="wp-block-paragraph">I use <code>DECIMAL(10,2)</code> for money, not <code>FLOAT</code>. Floating-point types store approximate values, so totals can drift by fractions of a cent. Decimal types store exact values, which matters when finance checks your numbers.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, the &#8220;sales total&#8221; bugs I see most often come from the data model, not the SQL. Before writing any query, decide which table is the source of truth for revenue. For most systems it is the line items, not a total column on the order header.</p>
</blockquote>



<p class="wp-block-paragraph">Below is the insert query:</p>



<pre class="wp-block-code"><code>SET XACT_ABORT ON;
BEGIN TRY
    BEGIN TRANSACTION;

    DECLARE @OrderID INT;

    -- Order 1: completed, one NULL discount
    INSERT INTO dbo.Orders (CustomerID, OrderDate, Status)
    VALUES (101, '2026-10-03 10:15:00', 'Completed');
    SET @OrderID = SCOPE_IDENTITY();

    INSERT INTO dbo.OrderItems (OrderID, ProductID, Quantity, UnitPrice, Discount)
    VALUES (@OrderID, 1, 2, 25.00, 5.00),
           (@OrderID, 2, 1, 40.00, NULL);

    -- Order 2: completed, one NULL and one zero discount
    INSERT INTO dbo.Orders (CustomerID, OrderDate, Status)
    VALUES (102, '2026-10-12 14:40:00', 'Completed');
    SET @OrderID = SCOPE_IDENTITY();

    INSERT INTO dbo.OrderItems (OrderID, ProductID, Quantity, UnitPrice, Discount)
    VALUES (@OrderID, 3, 3, 10.00, NULL),
           (@OrderID, 1, 1, 25.00, 0.00);

    -- Order 3: cancelled, should be excluded from sales
    INSERT INTO dbo.Orders (CustomerID, OrderDate, Status)
    VALUES (101, '2026-10-20 09:05:00', 'Cancelled');
    SET @OrderID = SCOPE_IDENTITY();

    INSERT INTO dbo.OrderItems (OrderID, ProductID, Quantity, UnitPrice, Discount)
    VALUES (@OrderID, 2, 2, 40.00, NULL);

    -- Order 4: completed, discount on the line
    INSERT INTO dbo.Orders (CustomerID, OrderDate, Status)
    VALUES (103, '2026-10-28 16:30:00', 'Completed');
    SET @OrderID = SCOPE_IDENTITY();

    INSERT INTO dbo.OrderItems (OrderID, ProductID, Quantity, UnitPrice, Discount)
    VALUES (@OrderID, 4, 4, 12.50, 2.50);

    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    IF @@TRANCOUNT &gt; 0 ROLLBACK TRANSACTION;
    THROW;
END CATCH;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="743" src="https://sqlserverguides.com/wp-content/uploads/2026/11/Calculate-Total-Sales-SQL-1024x743.jpg" alt="Calculate Total Sales SQL" class="wp-image-23984" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/Calculate-Total-Sales-SQL-1024x743.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/Calculate-Total-Sales-SQL-768x557.jpg 768w, https://sqlserverguides.com/wp-content/uploads/2026/11/Calculate-Total-Sales-SQL-300x218.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/Calculate-Total-Sales-SQL.jpg 1137w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<h3 class="wp-block-heading">The Basic Way to Calculate Total Sales in SQL</h3>



<p class="wp-block-paragraph">The simplest approach is to multiply quantity by price for each line and add everything up with <code>SUM</code>. <code>SUM</code> is an <strong>aggregate function</strong>, which means it collapses many rows into one value. For more on the function itself, read <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-sum-function-in-sql-server/">how to use the SUM function in SQL Server</a>.</p>



<pre class="wp-block-preformatted"><code>SELECT SUM(Quantity * UnitPrice) AS TotalSales<br>FROM dbo.OrderItems;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="940" height="216" src="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Calculate-Total-Sales-in-SQL.jpg" alt="How to Calculate Total Sales in SQL" class="wp-image-23985" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Calculate-Total-Sales-in-SQL.jpg 940w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Calculate-Total-Sales-in-SQL-300x69.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/How-to-Calculate-Total-Sales-in-SQL-766x176.jpg 766w" sizes="auto, (max-width: 940px) 100vw, 940px" /></figure>
</div>


<p class="wp-block-paragraph">This multiplies quantity by unit price on every row, then adds the results. It is correct as a starting point, but it is rarely the number the business wants. It includes cancelled orders, ignores discounts, and covers all time.</p>



<h4 class="wp-block-heading">Filter to the Right Orders</h4>



<p class="wp-block-paragraph">A total sales number should only include orders that count as revenue. Join to <code>Orders</code> and filter by status. An <strong>inner join</strong> returns only rows that match in both tables; see this <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-inner-join-tutorial/">inner join tutorial</a> for more detail.</p>



<pre class="wp-block-preformatted"><code>SELECT SUM(oi.Quantity * oi.UnitPrice) AS GrossSales<br>FROM dbo.Orders AS o<br>INNER JOIN dbo.OrderItems AS oi<br>    ON oi.OrderID = o.OrderID<br>WHERE o.Status = 'Completed';</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="299" src="https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-total-sales-in-sql-server-1024x299.jpg" alt="calculate total sales in sql server" class="wp-image-23986" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-total-sales-in-sql-server-1024x299.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-total-sales-in-sql-server-300x88.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-total-sales-in-sql-server-767x224.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/11/calculate-total-sales-in-sql-server.jpg 1030w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">The <code>WHERE</code> clause removes cancelled and returned orders before the sum happens. If you are unsure when to filter in <code>WHERE</code> and when to use <code>HAVING</code>, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-having-vs-where/">HAVING vs. WHERE</a>. In short, <code>WHERE</code> filters rows before grouping, and <code>HAVING</code> filters groups after.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I always run <code>SELECT COUNT(*)</code> and a quick sample of rows before trusting a new total. If the row count is surprising, the join or filter is wrong, and no amount of correct arithmetic will fix it.</p>
</blockquote>



<h3 class="wp-block-heading">Handle Discounts and NULL Values</h3>



<p class="wp-block-paragraph">A <strong>NULL</strong> is a missing or unknown value, not zero. In SQL Server, any arithmetic with NULL returns NULL. If <code>Discount</code> is NULL on some rows, <code>Quantity * UnitPrice - Discount</code> turns into NULL for those rows, and <code>SUM</code> silently ignores them. Your total drops with no error.</p>



<p class="wp-block-paragraph">Use <code>COALESCE</code>, which returns the first non-NULL value in a list. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-coalesce-function/">the COALESCE function</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-null/">how SQL handles NULL</a> for more detail.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    SUM(oi.Quantity * oi.UnitPrice) AS GrossSales,<br>    SUM(COALESCE(oi.Discount, 0))   AS TotalDiscounts,<br>    SUM(oi.Quantity * oi.UnitPrice - COALESCE(oi.Discount, 0)) AS NetSales<br>FROM dbo.Orders AS o<br>INNER JOIN dbo.OrderItems AS oi<br>    ON oi.OrderID = o.OrderID<br>WHERE o.Status = 'Completed';</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="306" src="https://sqlserverguides.com/wp-content/uploads/2026/11/total-sales-formula-in-sql-1024x306.jpg" alt="total sales formula in sql" class="wp-image-23987" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/total-sales-formula-in-sql-1024x306.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/total-sales-formula-in-sql-300x90.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/total-sales-formula-in-sql-768x229.jpg 768w, https://sqlserverguides.com/wp-content/uploads/2026/11/total-sales-formula-in-sql.jpg 1401w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">This returns three values. <strong>Gross sales</strong> is revenue before discounts. <strong>Net sales</strong> subtracts discounts. Showing both avoids arguments later, because different teams often mean different things by &#8220;total sales.&#8221;</p>



<p class="wp-block-paragraph">If your result looks rounded oddly, review <a rel="noopener" target="_blank" href="https://sqlserverguides.com/format-number-to-2-decimal-places-in-sql-server/">formatting numbers to two decimal places</a>. If you see a conversion error when your price column is stored as text, read <a rel="noopener" target="_blank" href="https://sqlserverguides.com/error-converting-data-type-varchar-to-numeric/">the varchar to numeric conversion error</a>. The better fix is to store numbers as numeric types in the first place.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I have seen a nullable discount column quietly cut a quarterly report by thousands of dollars. Where possible, make money columns <code>NOT NULL DEFAULT 0</code>, so this mistake cannot happen.</p>
</blockquote>



<h3 class="wp-block-heading">Calculate Total Sales by Date Range</h3>



<p class="wp-block-paragraph">Most real questions are &#8220;how much did we sell this month?&#8221; Use a half-open date range, meaning start date included and end date excluded.</p>



<pre class="wp-block-preformatted"><code>SELECT SUM(oi.Quantity * oi.UnitPrice - COALESCE(oi.Discount, 0)) AS NetSales<br>FROM dbo.Orders AS o<br>INNER JOIN dbo.OrderItems AS oi<br>    ON oi.OrderID = o.OrderID<br>WHERE o.Status = 'Completed'<br>  AND o.OrderDate &gt;= '2026-10-01'<br>  AND o.OrderDate &lt;  '2026-11-01';</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="265" src="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-total-sales-in-sql-1024x265.jpg" alt="how to find total sales in sql" class="wp-image-23988" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-total-sales-in-sql-1024x265.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-total-sales-in-sql-768x199.jpg 768w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-total-sales-in-sql-300x78.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/how-to-find-total-sales-in-sql.jpg 1470w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">I avoid <code>BETWEEN</code> for datetime columns. <code>BETWEEN '2026-10-01' AND '2026-10-31'</code> stops at midnight on October 31 and drops almost the entire last day. The half-open pattern includes every moment of October and nothing from November. For more, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-filter-by-date/">filtering by date</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-date-between/">using BETWEEN with dates</a>.</p>



<p class="wp-block-paragraph">Also keep the date column unwrapped. Writing <code>WHERE YEAR(o.OrderDate) = 2026</code> blocks index use, which forces SQL Server to scan more data. Comparing the raw column to a range lets the optimizer seek instead.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> In my experience, month-end reports break when someone uses <code>BETWEEN</code> with a time component. Make the half-open range your team&#8217;s standard and put it in your code review checklist.</p>
</blockquote>



<h3 class="wp-block-heading">How to Calculate Total Sales in SQL by Customer and Month</h3>



<p class="wp-block-paragraph">Business reports rarely need one number. They need totals per customer, product, or month. The <code>GROUP BY</code> clause splits rows into groups, and <code>SUM</code> runs once per group. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-group-by-clause-in-sql-server/">how to use GROUP BY in SQL Server</a> for the fundamentals.</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    o.CustomerID,<br>    DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1) AS SalesMonth,<br>    SUM(oi.Quantity * oi.UnitPrice - COALESCE(oi.Discount, 0)) AS NetSales<br>FROM dbo.Orders AS o<br>INNER JOIN dbo.OrderItems AS oi<br>    ON oi.OrderID = o.OrderID<br>WHERE o.Status = 'Completed'<br>GROUP BY<br>    o.CustomerID,<br>    DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1)<br>ORDER BY SalesMonth, NetSales DESC;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="439" src="https://sqlserverguides.com/wp-content/uploads/2026/11/total-sales-sql-query-1024x439.jpg" alt="total sales sql query" class="wp-image-23989" srcset="https://sqlserverguides.com/wp-content/uploads/2026/11/total-sales-sql-query-1024x439.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/11/total-sales-sql-query-300x129.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/11/total-sales-sql-query-767x329.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/11/total-sales-sql-query.jpg 1410w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph"><code>DATEFROMPARTS</code> builds the first day of each month, which gives clean, sortable month labels. Every non-aggregated column in <code>SELECT</code> must appear in <code>GROUP BY</code>. For other grouping patterns, see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-group-by-date/">grouping by date</a>.</p>



<h3 class="wp-block-heading">Avoid the Double-Counting Trap</h3>



<p class="wp-block-paragraph">This is the bug behind the $38,000 gap. Suppose <code>Orders</code> also had a <code>OrderTotal</code> column, and you wrote this:</p>



<pre class="wp-block-preformatted"><code><em>-- Wrong: OrderTotal repeats once per line item</em><br>SELECT SUM(o.OrderTotal)<br>FROM dbo.Orders AS o<br>INNER JOIN dbo.OrderItems AS oi ON oi.OrderID = o.OrderID;</code></pre>



<p class="wp-block-paragraph">An order with five lines would add its header total five times. This is called <strong>fan-out</strong>, where a join multiplies rows from the &#8220;one&#8221; side. Sum values from the same grain, meaning the same level of detail. Either sum the order lines, or sum the header without joining to the lines.</p>



<p class="wp-block-paragraph">If you ever need the number of orders, count distinct IDs rather than rows; see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-count-distinct/">COUNT DISTINCT</a>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I check every sales query by reconciling it against a simple independent total, such as a payment system export for one day. If the numbers differ, I look for fan-out first.</p>
</blockquote>



<h3 class="wp-block-heading">Add Running Totals and Rollups</h3>



<p class="wp-block-paragraph">Sometimes you need a cumulative total over time. <strong>Window functions</strong> perform calculations across related rows without collapsing them. The <code>OVER</code> clause defines the window; read more in <a rel="noopener" target="_blank" href="https://sqlserverguides.com/window-functions-in-sql-server/">window functions in SQL Server</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-over-clause/">the OVER clause</a>.</p>



<pre class="wp-block-preformatted"><code>WITH MonthlySales AS (<br>    SELECT<br>        DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1) AS SalesMonth,<br>        SUM(oi.Quantity * oi.UnitPrice - COALESCE(oi.Discount, 0)) AS NetSales<br>    FROM dbo.Orders AS o<br>    INNER JOIN dbo.OrderItems AS oi ON oi.OrderID = o.OrderID<br>    WHERE o.Status = 'Completed'<br>    GROUP BY DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1)<br>)<br>SELECT<br>    SalesMonth,<br>    NetSales,<br>    SUM(NetSales) OVER (ORDER BY SalesMonth<br>                        ROWS UNBOUNDED PRECEDING) AS RunningTotal<br>FROM MonthlySales<br>ORDER BY SalesMonth;</code></pre>



<p class="wp-block-paragraph">The <strong>CTE</strong> (common table expression) named <code>MonthlySales</code> is a temporary named result you can query right after it. The outer query then adds a running total across months. Specifying <code>ROWS UNBOUNDED PRECEDING</code> makes the frame explicit, and it is usually faster than the default frame.</p>



<p class="wp-block-paragraph">For a grand total row on the same report, add <code>GROUP BY ROLLUP</code>:</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    o.CustomerID,<br>    SUM(oi.Quantity * oi.UnitPrice - COALESCE(oi.Discount, 0)) AS NetSales<br>FROM dbo.Orders AS o<br>INNER JOIN dbo.OrderItems AS oi ON oi.OrderID = o.OrderID<br>WHERE o.Status = 'Completed'<br>GROUP BY ROLLUP (o.CustomerID);</code></pre>



<p class="wp-block-paragraph"><code>ROLLUP</code> adds a final row where <code>CustomerID</code> is NULL and the value is the overall total. That NULL is the subtotal marker, so label it in your application.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I use a CTE like this to keep the &#8220;calculate&#8221; step separate from the &#8220;present&#8221; step. It makes the query easier to test and easier for the next person to read.</p>
</blockquote>



<h3 class="wp-block-heading">Make the Calculation Reusable and Fast</h3>



<p class="wp-block-paragraph">Once the logic is right, package it. A <strong>view</strong> is a saved query you can select from like a table. Read about <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-views/">SQL Server views</a>. A <strong>stored procedure</strong> is a saved, parameterized block of T-SQL; see how to <a rel="noopener" target="_blank" href="https://sqlserverguides.com/create-stored-procedure-in-sql-server/">create a stored procedure</a>.</p>



<pre class="wp-block-preformatted"><code>CREATE OR ALTER PROCEDURE dbo.usp_GetNetSales<br>    @StartDate DATE,<br>    @EndDate   DATE   <em>-- exclusive</em><br>AS<br>BEGIN<br>    SET NOCOUNT ON;<br><br>    SELECT SUM(oi.Quantity * oi.UnitPrice - COALESCE(oi.Discount, 0)) AS NetSales<br>    FROM dbo.Orders AS o<br>    INNER JOIN dbo.OrderItems AS oi ON oi.OrderID = o.OrderID<br>    WHERE o.Status = 'Completed'<br>      AND o.OrderDate &gt;= @StartDate<br>      AND o.OrderDate &lt;  @EndDate;<br>END;</code></pre>



<p class="wp-block-paragraph">The procedure takes parameters, so the logic lives in one place. That prevents ten reports from computing &#8220;sales&#8221; ten different ways. <code>SET NOCOUNT ON</code> stops SQL Server from sending row-count messages, which reduces chatter.</p>



<h3 class="wp-block-heading">Index for the Query</h3>



<p class="wp-block-paragraph">An <strong>index</strong> is a sorted structure that helps SQL Server find rows without scanning the whole table. For this workload, a nonclustered index on order date and status helps, and an index on <code>OrderItems(OrderID)</code> supports the join. Learn the basics in <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-create-index-in-sql-server/">how to create an index in SQL Server</a>.</p>



<pre class="wp-block-preformatted"><code>CREATE NONCLUSTERED INDEX IX_Orders_Date_Status<br>ON dbo.Orders (OrderDate, Status)<br>INCLUDE (CustomerID);<br><br>CREATE NONCLUSTERED INDEX IX_OrderItems_OrderID<br>ON dbo.OrderItems (OrderID)<br>INCLUDE (Quantity, UnitPrice, Discount);</code></pre>



<p class="wp-block-paragraph"><code>INCLUDE</code> adds columns to the index leaf level so the query can be answered from the index alone. The trade-off is that every index slows inserts and updates and uses storage. Create them after checking the actual execution plan, not before. More tips are in <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-query-optimization-tips/">SQL query optimization tips</a>.</p>



<p class="wp-block-paragraph">For heavy analytics on very large tables, a <strong>columnstore index</strong> stores data by column and compresses well, which can speed up large aggregations. Test it against your workload, since it suits reporting better than frequent single-row updates.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> I turn on Query Store in Azure SQL Database or SQL Server before tuning anything. It records query runtime history, so I can prove an index helped instead of guessing.</p>
</blockquote>



<h3 class="wp-block-heading">Frequently Asked Questions</h3>



<h4 class="wp-block-heading">How do I calculate total sales in SQL Server?</h4>



<p class="wp-block-paragraph">Multiply quantity by unit price for each order line and wrap it in <code>SUM</code>, such as <code>SUM(Quantity * UnitPrice)</code>. Then filter to completed orders and the right date range. Subtract discounts for net sales.</p>



<h4 class="wp-block-heading">How do I calculate total sales by month in SQL?</h4>



<p class="wp-block-paragraph">Use <code>GROUP BY</code> on the year and month of the order date, for example with <code>DATEFROMPARTS</code>. Then apply <code>SUM</code> to the line amounts. Add a window function if you want a running total.</p>



<h4 class="wp-block-heading">Why is my SQL total sales number too high?</h4>



<p class="wp-block-paragraph">The usual cause is join fan-out, where an order-level value is repeated for each line item. Cancelled orders and duplicate rows can also inflate totals. Reconcile against a known number for one day to find it.</p>



<h4 class="wp-block-heading">How do I include discounts in total sales?</h4>



<p class="wp-block-paragraph">Subtract the discount from each line amount before summing, and wrap nullable discount columns with <code>COALESCE(Discount, 0)</code>. Report gross and net sales separately so teams agree on the definition.</p>



<h4 class="wp-block-heading">How can I make sales queries faster?</h4>



<p class="wp-block-paragraph">Compare your date column directly to a range, index the join and filter columns based on the execution plan, and keep statistics updated. For very large reporting tables, consider a columnstore index and test it first.</p>



<p class="wp-block-paragraph">You now know how to calculate total sales in SQL using <code>SUM</code>, <code>JOIN</code>, <code>GROUP BY</code>, <code>COALESCE</code>, and window functions, and how to avoid common mistakes like fan-out and NULL discounts. Always reconcile your result against an independent number, and give reporting users access to a specific view or procedure instead of the whole database. I hope you found this article helpful.</p>



<p class="wp-block-paragraph">You May Also Like</p>



<ul class="wp-block-list">
<li><a href="https://sqlserverguides.com/how-to-find-percentage-of-total-in-sql-server/" target="_blank" rel="noreferrer noopener">How to Find Percentage of Total in SQL</a></li>



<li><a href="https://sqlserverguides.com/how-to-swap-two-column-values-in-sql/" target="_blank" rel="noreferrer noopener">How to Swap Two Column Values in SQL</a></li>



<li><a href="https://sqlserverguides.com/how-to-use-avg-function-in-sql-server/" target="_blank" rel="noopener">How to use the AVG function in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/how-to-use-count-function-in-sql-server/" target="_blank" rel="noopener">How to use the COUNT function in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/how-to-use-max-function-in-sql-server/" target="_blank" rel="noopener">How to use the MAX function in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/how-to-use-min-function-in-sql-server/" target="_blank" rel="noopener">How to use the MIN function in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/sql-cheat-sheet/" target="_blank" rel="noopener">SQL cheat sheet</a></li>



<li><a href="https://sqlserverguides.com/how-to-find-even-and-odd-numbers-in-sql-server/" target="_blank" rel="noreferrer noopener">How to Find Even and Odd Numbers in SQL Server</a></li>
</ul>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: sqlserverguides.com @ 2026-11-04 12:29:17 by W3 Total Cache
-->