<?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>Fri, 16 Oct 2026 15:11:34 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.3</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 Find the Lowest Salary in SQL Server</title>
		<link>https://sqlserverguides.com/how-to-find-the-lowest-salary-in-sql-server/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Fri, 16 Oct 2026 13:52:50 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[How to Find the Lowest Salary in SQL Server]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=23882</guid>

					<description><![CDATA[A payroll dashboard shows the lowest salary as zero, but HR says no active employee earns that amount. The query runs successfully. The problem is that it mixes placeholder values, inactive employees, and legitimate salaries without defining which records belong in the report. Knowing how to find the lowest salary in SQL Server starts with ... <a title="How to Find the Lowest Salary in SQL Server" class="read-more" href="https://sqlserverguides.com/how-to-find-the-lowest-salary-in-sql-server/" aria-label="Read more about How to Find the Lowest Salary in SQL Server">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A payroll dashboard shows the lowest salary as zero, but HR says no active employee earns that amount. The query runs successfully. The problem is that it mixes placeholder values, inactive employees, and legitimate salaries without defining which records belong in the report.</p>



<p class="wp-block-paragraph">Knowing <strong>how to find the lowest salary in SQL Server</strong> starts with <code>MIN()</code>, but reliable reporting also requires clear rules for missing values, duplicate salaries, and employee eligibility.</p>



<p class="wp-block-paragraph">The following examples show how to retrieve the minimum salary, return matching employees, calculate department minimums, and test performance and access controls.</p>



<h2 class="wp-block-heading">How to Find the Lowest Salary in SQL Server With MIN()</h2>



<p class="wp-block-paragraph">Use this query when you need only the lowest salary amount:</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    MIN(Salary) AS LowestSalary<br>FROM dbo.Employees;</code></pre>



<p class="wp-block-paragraph"><code>MIN()</code> is an aggregate function: it calculates one summary value from a set of rows. Here, it returns the smallest non-null salary in <code>dbo.Employees</code>. It ignores <code>NULL</code>, which represents an unknown or missing value.<a rel="noopener" target="_blank" href="https://learn.microsoft.com/en-us/sql/t-sql/functions/min-transact-sql?view=sql-server-ver17"></a></p>



<p class="wp-block-paragraph">For example, salaries of <code>92000.00</code>, <code>68000.00</code>, <code>54000.00</code>, and <code>NULL</code> produce:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">LowestSalary</th></tr></thead><tbody><tr><td>54000.00</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Duplicates do not change the minimum. If five employees earn <code>54000.00</code>, this query still returns one row containing that amount. Adding <code>DISTINCT</code> is unnecessary.</p>



<p class="wp-block-paragraph">An ungrouped <code>MIN()</code> query also returns one row when no records qualify, but its value is <code>NULL</code>. That differs from a detail query, which can return no rows.</p>



<p class="wp-block-paragraph">The syntax works in SQL Server, Azure SQL Database, and Azure SQL Managed Instance. This task does not require a different deployment model, special service tier, or extra infrastructure.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Pro Tip: I separate the requirement for “one salary amount” from “the employees earning that amount.” Those are different result shapes, and applications need to handle them differently.</p>
</blockquote>



<h2 class="wp-block-heading">Create a Safe Sample Payroll Table</h2>



<p class="wp-block-paragraph">Use a development database for this setup. The script creates a table and inserts fictional records; it does not modify existing employee data.</p>



<p class="wp-block-paragraph">If <code>dbo.Employees</code> already exists, use a different demonstration name and adjust the queries. Do not remove an existing payroll table just to run a tutorial.</p>



<pre class="wp-block-preformatted"><code>CREATE TABLE dbo.Employees<br>(<br>    EmployeeID   int NOT NULL<br>        CONSTRAINT PK_Employees PRIMARY KEY,<br>    EmployeeName nvarchar(100) NOT NULL,<br>    DepartmentID int NOT NULL,<br>    Salary       decimal(12, 2) NULL,<br>    IsActive     bit NOT NULL,<br>    CONSTRAINT CK_Employees_Salary<br>        CHECK (Salary >= 0)<br>);<br><br>INSERT INTO dbo.Employees<br>    (EmployeeID, EmployeeName, DepartmentID, Salary, IsActive)<br>VALUES<br>    (1, N'Emma Carter',   10, 92000.00, 1),<br>    (2, N'Noah Williams', 10, 68000.00, 1),<br>    (3, N'Olivia Davis',  20, 54000.00, 1),<br>    (4, N'Liam Johnson',  20, 54000.00, 1),<br>    (5, N'Ava Martinez',  30, 61000.00, 1),<br>    (6, N'James Wilson',  30,     NULL, 1),<br>    (7, N'Sophia Brown',  10, 42000.00, 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-full"><img fetchpriority="high" decoding="async" width="890" height="687" src="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-the-Lowest-Salary-in-SQL.jpg" alt="How to Find the Lowest Salary in SQL" class="wp-image-23883" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-the-Lowest-Salary-in-SQL.jpg 890w, https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-the-Lowest-Salary-in-SQL-300x232.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-the-Lowest-Salary-in-SQL-767x592.jpg 767w" sizes="(max-width: 890px) 100vw, 890px" /></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><img decoding="async" width="1024" height="544" src="https://sqlserverguides.com/wp-content/uploads/2026/10/Find-the-Lowest-Salary-in-SQL-1024x544.jpg" alt="Find the Lowest Salary in SQL" class="wp-image-23884" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/Find-the-Lowest-Salary-in-SQL-1024x544.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/10/Find-the-Lowest-Salary-in-SQL-300x160.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/Find-the-Lowest-Salary-in-SQL-767x408.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/10/Find-the-Lowest-Salary-in-SQL.jpg 1198w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">A schema is a namespace for database objects; this example uses <code>dbo</code>. The primary key uniquely identifies each employee. In a complete model, <code>DepartmentID</code> would normally reference a department table through a foreign key.</p>



<p class="wp-block-paragraph"><code>decimal(12, 2)</code> stores exact amounts with two decimal places. Avoid approximate types such as <code>float</code> for compensation amounts. <code>IsActive</code> is a <code>bit</code> flag indicating whether an employee belongs in the active workforce.</p>



<p class="wp-block-paragraph">The check constraint rejects negative salaries but permits zero and <code>NULL</code>. That distinction is intentional: preventing negative values does not establish whether zero is valid.</p>



<p class="wp-block-paragraph">Running the first query returns <code>42000.00</code>, because it includes inactive Sophia. The lowest active salary is <code>54000.00</code>.</p>



<p class="wp-block-paragraph">Review <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-constraints/">SQL constraints</a> when translating payroll rules into database validation.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Pro Tip: I include an inactive employee, duplicate minimums, and a missing salary in sample data. Unique, perfectly clean values make weak queries look correct.</p>
</blockquote>



<h3 class="wp-block-heading">How to Find the Lowest Salary in SQL Server for Active Employees</h3>



<p class="wp-block-paragraph">Apply eligibility rules before calculating the minimum:</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    MIN(Salary) AS LowestActiveSalary<br>FROM dbo.Employees<br>WHERE IsActive = 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-full"><img decoding="async" width="785" height="296" src="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-the-Lowest-Salary-in-SQL-Server.jpg" alt="How to Find the Lowest Salary in SQL Server" class="wp-image-23885" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-the-Lowest-Salary-in-SQL-Server.jpg 785w, https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-the-Lowest-Salary-in-SQL-Server-300x113.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-the-Lowest-Salary-in-SQL-Server-766x289.jpg 766w" sizes="(max-width: 785px) 100vw, 785px" /></figure>
</div>


<p class="wp-block-paragraph"><code>WHERE</code> restricts the input rows, so the result is <code>54000.00</code>.</p>



<p class="wp-block-paragraph">If zero means “not loaded” in a legacy system, the reporting requirement might explicitly exclude it:</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    MIN(Salary) AS LowestActiveSalary<br>FROM dbo.Employees<br>WHERE IsActive = 1<br>  AND Salary > 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-full"><img loading="lazy" decoding="async" width="786" height="361" src="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-Lowest-Salary-in-SQL-Server.jpg" alt="How to Find Lowest Salary in SQL Server" class="wp-image-23886" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-Lowest-Salary-in-SQL-Server.jpg 786w, https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-Lowest-Salary-in-SQL-Server-766x352.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-Lowest-Salary-in-SQL-Server-300x138.jpg 300w" sizes="auto, (max-width: 786px) 100vw, 786px" /></figure>
</div>


<p class="wp-block-paragraph">This is a business decision, not a universal SQL Server best practice. Excluding zero can hide a data-quality problem, and some legitimate records may have zero base salary.</p>



<p class="wp-block-paragraph">Do not replace missing salaries with zero before calculating the minimum. <code>MIN(COALESCE(Salary, 0))</code> can make an unknown amount appear to be the lowest salary.</p>



<p class="wp-block-paragraph">Also define comparable units. Annual salaries, hourly rates, different currencies, and full-time-equivalent compensation cannot be ranked meaningfully without normalization. For historical reporting, select the applicable effective-date records first rather than mixing current and previous salaries.</p>



<p class="wp-block-paragraph">The <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-where-clause-tutorial/">SQL WHERE clause tutorial</a> explains how filters determine which rows reach an aggregate.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Pro Tip: I document the salary basis beside the report definition. A technically correct minimum still misleads people when the input mixes annual pay and hourly rates.</p>
</blockquote>



<h3 class="wp-block-heading">Return Every Employee Earning the Lowest Salary</h3>



<p class="wp-block-paragraph">Use a subquery when the report needs names and employee identifiers:</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    e.EmployeeID,<br>    e.EmployeeName,<br>    e.DepartmentID,<br>    e.Salary<br>FROM dbo.Employees AS e<br>WHERE e.IsActive = 1<br>  AND e.Salary =<br>  (<br>      SELECT MIN(e2.Salary)<br>      FROM dbo.Employees AS e2<br>      WHERE e2.IsActive = 1<br>  )<br>ORDER BY e.EmployeeID;</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="680" height="723" src="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-Lowest-Salary-in-SQL.jpg" alt="How to Find Lowest Salary in SQL" class="wp-image-23887" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-Lowest-Salary-in-SQL.jpg 680w, https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-Lowest-Salary-in-SQL-282x300.jpg 282w" sizes="auto, (max-width: 680px) 100vw, 680px" /></figure>
</div>


<p class="wp-block-paragraph">The subquery calculates the lowest active salary. The outer query returns every active employee earning it.</p>



<p class="wp-block-paragraph">The result contains Olivia and Liam, both at <code>54000.00</code>. Filtering both levels keeps the population consistent. Without the outer active filter, an inactive employee with the same salary could appear.</p>



<p class="wp-block-paragraph">A common mistake is selecting <code>EmployeeName</code> beside <code>MIN(Salary)</code> without a suitable grouping strategy. SQL Server cannot infer which employee name should accompany the aggregate, especially when several employees tie.</p>



<p class="wp-block-paragraph">For a compact alternative:</p>



<pre class="wp-block-preformatted"><code>SELECT TOP (1) WITH TIES<br>    EmployeeID,<br>    EmployeeName,<br>    DepartmentID,<br>    Salary<br>FROM dbo.Employees<br>WHERE IsActive = 1<br>  AND Salary IS NOT NULL<br>ORDER BY Salary ASC;</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="585" height="523" src="https://sqlserverguides.com/wp-content/uploads/2026/10/Find-Lowest-Salary-SQL-Server.jpg" alt="Find Lowest Salary SQL Server" class="wp-image-23888" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/Find-Lowest-Salary-SQL-Server.jpg 585w, https://sqlserverguides.com/wp-content/uploads/2026/10/Find-Lowest-Salary-SQL-Server-300x268.jpg 300w" sizes="auto, (max-width: 585px) 100vw, 585px" /></figure>
</div>


<p class="wp-block-paragraph"><code>WITH TIES</code> includes every row matching the first salary. Do not add <code>EmployeeID</code> to this ordering: it would make the ordering combinations unique and prevent salary ties from being retained.</p>



<p class="wp-block-paragraph">If the requirement is exactly one employee, remove <code>WITH TIES</code> and order by <code>Salary, EmployeeID</code>. That produces a repeatable selection, not every lowest-paid employee.</p>



<p class="wp-block-paragraph">See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-subquery/">SQL subqueries</a> for the relationship between inner calculations and outer results.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Pro Tip: I ask whether ties should produce multiple rows before connecting the query to an API. A response designed for one employee can break when payroll introduces a duplicate minimum.</p>
</blockquote>



<h3 class="wp-block-heading">Find the Lowest Salary in Each Department</h3>



<p class="wp-block-paragraph">Use <code>GROUP BY</code> to calculate a separate minimum for each department:</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    DepartmentID,<br>    MIN(Salary) AS LowestSalary,<br>    COUNT(*) AS ActiveEmployees,<br>    COUNT(Salary) AS EmployeesWithKnownSalary<br>FROM dbo.Employees<br>WHERE IsActive = 1<br>GROUP BY DepartmentID<br>ORDER 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="928" height="535" src="https://sqlserverguides.com/wp-content/uploads/2026/10/Find-Lowest-Salary-SQL.jpg" alt="Find Lowest Salary SQL" class="wp-image-23889" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/Find-Lowest-Salary-SQL.jpg 928w, https://sqlserverguides.com/wp-content/uploads/2026/10/Find-Lowest-Salary-SQL-300x173.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/Find-Lowest-Salary-SQL-767x442.jpg 767w" sizes="auto, (max-width: 928px) 100vw, 928px" /></figure>
</div>


<p class="wp-block-paragraph"><code>GROUP BY</code> forms one group per department. <code>COUNT(*)</code> counts active employee rows, while <code>COUNT(Salary)</code> counts only non-null salary values.</p>



<p class="wp-block-paragraph">The minimums are <code>68000.00</code> for department 10, <code>54000.00</code> for department 20, and <code>61000.00</code> for department 30. The two counts also reveal that department 30 has an employee with missing compensation.</p>



<p class="wp-block-paragraph">To return employees rather than department summaries, use a window aggregate:</p>



<pre class="wp-block-preformatted"><code>WITH DepartmentPay AS<br>(<br>    SELECT<br>        EmployeeID,<br>        EmployeeName,<br>        DepartmentID,<br>        Salary,<br>        MIN(Salary) OVER (<br>            PARTITION BY DepartmentID<br>        ) AS DepartmentMinimum<br>    FROM dbo.Employees<br>    WHERE IsActive = 1<br>)<br>SELECT<br>    EmployeeID,<br>    EmployeeName,<br>    DepartmentID,<br>    Salary<br>FROM DepartmentPay<br>WHERE Salary = DepartmentMinimum<br>ORDER BY DepartmentID, EmployeeID;</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="783" height="550" src="https://sqlserverguides.com/wp-content/uploads/2026/10/Lowest-Salary-in-SQL-Server.jpg" alt="Lowest Salary in SQL Server" class="wp-image-23890" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/Lowest-Salary-in-SQL-Server.jpg 783w, https://sqlserverguides.com/wp-content/uploads/2026/10/Lowest-Salary-in-SQL-Server-300x211.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/Lowest-Salary-in-SQL-Server-767x539.jpg 767w" sizes="auto, (max-width: 783px) 100vw, 783px" /></figure>
</div>


<p class="wp-block-paragraph">A common table expression, or CTE, names the intermediate query. <code>PARTITION BY</code> divides the calculation by department without collapsing individual employee rows.</p>



<p class="wp-block-paragraph">A department containing only unknown salaries has a null minimum and produces no matching employee rows in the second query. An empty department cannot appear unless the query starts from a department table.</p>



<p class="wp-block-paragraph">Learn more about <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-group-by-clause-in-sql-server/">GROUP BY in SQL Server</a> when building summary reports.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Pro Tip: I include known-salary counts in payroll validation reports. A minimum based on one known amount out of 200 employees deserves attention.</p>
</blockquote>



<h3 class="wp-block-heading">Test NULLs, Empty Results, and Common Query Mistakes</h3>



<p class="wp-block-paragraph">Test edge cases before publishing the report:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Test case</th><th class="has-text-align-left" data-align="left">Expected behavior</th></tr></thead><tbody><tr><td>Several employees share the minimum</td><td>Employee-detail query returns every tie</td></tr><tr><td>All qualifying salaries are <code>NULL</code></td><td>Ungrouped <code>MIN()</code> returns one <code>NULL</code></td></tr><tr><td>No employees qualify</td><td>Ungrouped <code>MIN()</code> returns one <code>NULL</code></td></tr><tr><td>Lowest employee is inactive</td><td>Active-only query excludes that employee</td></tr><tr><td>Zero salary exists</td><td>Included unless the business filter excludes it</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Two mistakes cause especially confusing results.</p>



<p class="wp-block-paragraph">First, ascending ordering places nulls before non-null salaries in SQL Server. A <code>TOP (1)</code> query therefore needs <code>Salary IS NOT NULL</code> when searching for a known minimum.</p>



<p class="wp-block-paragraph">Second, salary stored as text is compared as text. A <code>varchar</code> column can sort amounts differently from numeric order. Fix the data model and validate imported values rather than converting an entire text column during every report execution.</p>



<p class="wp-block-paragraph">Avoid <code>NOLOCK</code> for payroll results. It permits uncommitted reads, so a report can reflect salary changes that later roll back.</p>



<p class="wp-block-paragraph">If separate dashboard queries must agree, define their consistency requirement. An isolation level controls how transactions observe concurrent changes. Database-level row-versioning options can help some workloads, but require DBA review and capacity testing.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Pro Tip: I test the report during a simulated salary import, not just against idle data. Concurrency problems rarely appear in a quiet demonstration database.</p>
</blockquote>



<h3 class="wp-block-heading">Tune Minimum-Salary Queries Without Over-Indexing</h3>



<p class="wp-block-paragraph">Start with an actual execution plan, which shows the operations SQL Server used. Measure page reads and CPU time:</p>



<pre class="wp-block-preformatted"><code>SET STATISTICS IO ON;<br>SET STATISTICS TIME ON;<br><br>SELECT<br>    MIN(Salary) AS LowestActiveSalary<br>FROM dbo.Employees<br>WHERE IsActive = 1;<br><br>SET STATISTICS IO OFF;<br>SET STATISTICS TIME OFF;</code></pre>



<p class="wp-block-paragraph"><code>STATISTICS IO</code> reports logical and physical reads. <code>STATISTICS TIME</code> reports CPU and elapsed time. Small tables may scan efficiently, so a scan alone does not prove a problem.</p>



<p class="wp-block-paragraph">For a frequent active-salary lookup, test this candidate:</p>



<pre class="wp-block-preformatted"><code>CREATE NONCLUSTERED INDEX IX_Employees_ActiveSalary<br>ON dbo.Employees (Salary)<br>WHERE IsActive = 1<br>  AND Salary IS NOT NULL;</code></pre>



<p class="wp-block-paragraph">A nonclustered index is a separate ordered access structure. This filtered index stores known active salaries and can help SQL Server find the low end efficiently.</p>



<p class="wp-block-paragraph">Warning: Index creation consumes storage and resources and may block other activity. Review existing indexes and schedule production deployment appropriately.</p>



<p class="wp-block-paragraph">For department-focused reports, <code>(DepartmentID, Salary)</code> may be a better key order. Included employee columns can avoid extra table lookups, but wider indexes increase storage and write overhead.</p>



<p class="wp-block-paragraph">Use <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-query-optimization-tips/">SQL query optimization tips</a> alongside real plans. Statistics—metadata describing data distribution—help the optimizer estimate row counts and choose access paths.</p>



<p class="wp-block-paragraph">Query Store, when enabled, preserves query performance history and plans for regression analysis. Tune before increasing Azure SQL compute, but do not promise direct savings: lower resource use reduces the bill only when the purchasing model or provisioned capacity reflects it.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Pro Tip: I keep an index only after testing both the report and salary-write workload. Optimizing a monthly query should not penalize every payroll update.</p>
</blockquote>



<h3 class="wp-block-heading">Secure and Maintain the Payroll Reporting Path</h3>



<p class="wp-block-paragraph">Use least privilege: grant only the access needed for the report. A stored procedure, a named database routine, can expose an approved aggregate without granting broad payroll-table access.</p>



<p class="wp-block-paragraph">After creating the procedure and an authorized reporting role, grant execution:</p>



<pre class="wp-block-preformatted"><code>GRANT EXECUTE<br>ON OBJECT::dbo.GetLowestActiveSalary<br>TO PayrollReportingRole;</code></pre>



<p class="wp-block-paragraph">The procedure should contain the approved active-employee query. With an appropriate same-owner ownership chain, callers need not receive direct table permissions. Review <a rel="noopener" target="_blank" href="https://sqlserverguides.com/give-permission-to-execute-stored-procedure-in-sql-server/">stored procedure execution permissions</a> and test with a non-administrator identity.</p>



<p class="wp-block-paragraph">Prefer Microsoft Entra authentication and managed identities for supported Azure applications. Keep unavoidable secrets in Azure Key Vault, not source code, spreadsheets, shared files, or unprotected application settings.</p>



<p class="wp-block-paragraph">Require encrypted connections with server-certificate validation. An authorized administrator can check the session:</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    encrypt_option<br>FROM sys.dm_exec_connections<br>WHERE session_id = @@SPID;</code></pre>



<p class="wp-block-paragraph"><code>TRUE</code> indicates encryption; it does not prove the client validated the certificate. DMV permissions vary by platform and version, so do not grant broad monitoring rights merely for this check.</p>



<p class="wp-block-paragraph">Prefer private endpoints for Azure database access. Audit sensitive payroll access and alert on permission changes without writing salary results into general application logs.</p>



<p class="wp-block-paragraph">The read-only query needs no special backup. Deploy supporting objects through reviewed scripts, and keep the database’s existing backup and restore-testing requirements intact.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Pro Tip: I test the reporting identity independently from the deployment identity. Successful execution as an administrator says little about application permissions.</p>
</blockquote>



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



<h4 class="wp-block-heading">How do I find the lowest salary without NULL values?</h4>



<p class="wp-block-paragraph"><code>MIN(Salary)</code> already ignores null values. With <code>TOP (1) ... ORDER BY Salary ASC</code>, explicitly add <code>WHERE Salary IS NOT NULL</code> so an unknown salary does not appear first.</p>



<h4 class="wp-block-heading">How do I find the employee with the lowest salary?</h4>



<p class="wp-block-paragraph">Compare each employee’s salary with a subquery that calculates <code>MIN(Salary)</code>. This returns every matching employee; use deterministic <code>TOP (1)</code> ordering only when exactly one employee is required.</p>



<h4 class="wp-block-heading">Does MIN return zero when the table is empty?</h4>



<p class="wp-block-paragraph">No. An ungrouped <code>MIN()</code> returns one row containing <code>NULL</code> when no rows qualify. Convert that to a display message in the application rather than silently labeling it zero salary.</p>



<h4 class="wp-block-heading">How do I exclude zero salaries?</h4>



<p class="wp-block-paragraph">Add <code>Salary &gt; 0</code> to the qualifying filter. Do this only when the business definition excludes zero; otherwise, the report may conceal legitimate records or loading errors.</p>



<h4 class="wp-block-heading">Can I run these queries in Azure SQL Database?</h4>



<p class="wp-block-paragraph">Yes. The aggregate, subquery, grouped, and window-function examples work in Azure SQL Database and Azure SQL Managed Instance. Validate permissions and execution plans in the target database.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Pro Tip: I keep the empty-result behavior consistent across reports so applications do not interpret “unknown” as “zero.”</p>
</blockquote>



<p class="wp-block-paragraph">You can now calculate minimum salaries, return tied employees, and build department-level reports. Keep eligibility rules explicit, protect payroll access, and measure performance before adding indexes or compute. 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-null/" target="_blank" rel="noopener">Understand SQL NULL values</a></li>



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



<li><a href="https://sqlserverguides.com/window-functions-in-sql-server/" target="_blank" rel="noopener">Explore SQL window functions</a></li>



<li><a href="https://sqlserverguides.com/sql-indexes-best-practices/" target="_blank" rel="noopener">Review SQL Server indexing best practices</a></li>



<li><a href="https://sqlserverguides.com/how-to-test-stored-procedures-in-sql-server/" target="_blank" rel="noopener">Test SQL Server stored procedures</a></li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to Find 3rd Highest Salary in SQL Server</title>
		<link>https://sqlserverguides.com/how-to-find-3rd-highest-salary-in-sql-server/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Fri, 16 Oct 2026 12:33:12 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[How to Find 3rd Highest Salary in SQL Server]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=23871</guid>

					<description><![CDATA[A payroll report can return the wrong employee even when its SQL runs without errors. Two employees share the highest salary, and a query that simply selects the third row reports the second-highest salary instead of the third distinct amount. Understanding how to find 3rd highest salary in SQL Server starts with defining what “third ... <a title="How to Find 3rd Highest Salary in SQL Server" class="read-more" href="https://sqlserverguides.com/how-to-find-3rd-highest-salary-in-sql-server/" aria-label="Read more about How to Find 3rd Highest Salary in SQL Server">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A payroll report can return the wrong employee even when its SQL runs without errors. Two employees share the highest salary, and a query that simply selects the third row reports the second-highest salary instead of the third distinct amount.</p>



<p class="wp-block-paragraph">Understanding <strong>how to find 3rd highest salary in SQL Server</strong> starts with defining what “third highest” means. For compensation reporting, the usual requirement is the third distinct salary, including every employee who earns that amount.</p>



<p class="wp-block-paragraph">The following examples show how to build that query, handle ties and missing values, return department-level results, and check performance without exposing unnecessary payroll data.</p>



<h2 class="wp-block-heading">How to Find 3rd Highest Salary in SQL Server With DENSE_RANK</h2>



<p class="wp-block-paragraph">Use <code>DENSE_RANK()</code> to assign consecutive ranks to distinct salaries, then select rank 3:</p>



<pre class="wp-block-preformatted"><code>WITH RankedSalaries AS<br>(<br>    SELECT<br>        Salary,<br>        DENSE_RANK() OVER (<br>            ORDER BY Salary DESC<br>        ) AS SalaryRank<br>    FROM dbo.Employees<br>    WHERE Salary IS NOT NULL<br>)<br>SELECT DISTINCT<br>    Salary AS ThirdHighestSalary<br>FROM RankedSalaries<br>WHERE SalaryRank = 3;</code></pre>



<p class="wp-block-paragraph"><code>DENSE_RANK()</code> is a window function: it calculates a value across related rows without combining those rows into one summary record. Equal salaries receive equal ranks, and the next distinct salary receives the next consecutive rank.</p>



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



<ul class="wp-block-list">
<li><code>WHERE Salary IS NOT NULL</code> excludes unknown salaries.</li>



<li><code>ORDER BY Salary DESC</code> ranks higher salaries first.</li>



<li><code>DENSE_RANK()</code> preserves ties without leaving gaps.</li>



<li>The common table expression, or CTE, names the intermediate query <code>RankedSalaries</code>.</li>



<li>The outer filter selects rank 3.</li>



<li><code>DISTINCT</code> returns the salary once, even when several employees earn it.</li>
</ul>



<p class="wp-block-paragraph">The leading semicolon safely terminates any preceding statement. A CTE applies only to the statement immediately following it; it does not create a permanent table.</p>



<p class="wp-block-paragraph">These queries work in SQL Server, Azure SQL Database, and Azure SQL Managed Instance. No special Azure service tier is required for this ranking operation.</p>



<p class="wp-block-paragraph">For more background, review <a rel="noopener" target="_blank" href="https://sqlserverguides.com/window-functions-in-sql-server/">window functions in SQL Server</a>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Pro Tip: I confirm whether the report needs the third distinct salary or the third employee before writing the query. That single question prevents most ranking mistakes.</p>
</blockquote>



<h3 class="wp-block-heading">Create Sample Data and Define the Expected Result</h3>



<p class="wp-block-paragraph">Run the setup in a development database, not an existing payroll database. It creates a new table and inserts fictional employees; it does not delete or overwrite existing records.</p>



<p class="wp-block-paragraph">In SQL Server Management Studio, select your development database and open a query window. If needed, follow these steps to <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-run-query-in-ssms/">run a query in SSMS</a>.</p>



<pre class="wp-block-preformatted"><code>CREATE TABLE dbo.Employees<br>(<br>    EmployeeID   int NOT NULL<br>        CONSTRAINT PK_Employees PRIMARY KEY,<br>    EmployeeName nvarchar(100) NOT NULL,<br>    DepartmentID int NOT NULL,<br>    Salary       decimal(12, 2) NULL,<br>    CONSTRAINT CK_Employees_Salary<br>        CHECK (Salary >= 0)<br>);<br><br>INSERT INTO dbo.Employees<br>    (EmployeeID, EmployeeName, DepartmentID, Salary)<br>VALUES<br>    (1, N'Emma Carter',    10, 150000.00),<br>    (2, N'Noah Williams',  20, 150000.00),<br>    (3, N'Olivia Davis',   10, 130000.00),<br>    (4, N'Liam Johnson',  20, 120000.00),<br>    (5, N'Ava Martinez',  10, 120000.00),<br>    (6, N'James Wilson',  20, 110000.00),<br>    (7, N'Sophia Brown',  10,  95000.00),<br>    (8, N'Benjamin Lee',  20, 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="887" height="556" src="https://sqlserverguides.com/wp-content/uploads/2026/10/3rd-highest-salary-sql-server.jpg" alt="" class="wp-image-23872" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/3rd-highest-salary-sql-server.jpg 887w, https://sqlserverguides.com/wp-content/uploads/2026/10/3rd-highest-salary-sql-server-300x188.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/3rd-highest-salary-sql-server-767x481.jpg 767w" sizes="auto, (max-width: 887px) 100vw, 887px" /></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="595" src="https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-find-the-3rd-highest-salary-in-sql-server-1024x595.jpg" alt="how to find the 3rd highest salary in sql server" class="wp-image-23873" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-find-the-3rd-highest-salary-in-sql-server-1024x595.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-find-the-3rd-highest-salary-in-sql-server-300x174.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-find-the-3rd-highest-salary-in-sql-server-768x446.jpg 768w, https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-find-the-3rd-highest-salary-in-sql-server.jpg 1033w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">A schema is a namespace that groups database objects; this example uses <code>dbo</code>. The primary key uniquely identifies each employee. In a complete HR database, <code>DepartmentID</code> would normally reference a department table through a foreign key.</p>



<p class="wp-block-paragraph"><code>decimal(12, 2)</code> stores exact numeric values with two decimal places. That is more appropriate for salary amounts than approximate types such as <code>float</code>.</p>



<p class="wp-block-paragraph">The check constraint rejects negative salaries but permits <code>NULL</code>, meaning unknown or unavailable. It does not treat an unknown salary as zero.</p>



<p class="wp-block-paragraph">The expected ranking is:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Salary</th><th>Employees earning it</th><th>Dense rank</th></tr></thead><tbody><tr><td>150000.00</td><td>2</td><td>1</td></tr><tr><td>130000.00</td><td>1</td><td>2</td></tr><tr><td>120000.00</td><td>2</td><td>3</td></tr><tr><td>110000.00</td><td>1</td><td>4</td></tr><tr><td>95000.00</td><td>1</td><td>5</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The third-highest distinct salary is <code>120000.00</code>, earned by Liam and Ava.</p>



<p class="wp-block-paragraph">For a real business report, also define the pay basis. Do not rank annual salaries against hourly rates, or compare different currencies without an agreed conversion rule.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Pro Tip: I deliberately include duplicate salaries and a missing salary in test data. A sample with only unique values hides the bugs that usually appear later.</p>
</blockquote>



<h3 class="wp-block-heading">Understand DENSE_RANK, RANK, and ROW_NUMBER</h3>



<p class="wp-block-paragraph">These functions answer different questions. They are not interchangeable just because each produces a number beside a row.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Function</th><th>Treatment of ties</th><th>Appropriate requirement</th></tr></thead><tbody><tr><td><code>DENSE_RANK()</code></td><td>Same rank; no gaps afterward</td><td>Third distinct salary</td></tr><tr><td><code>RANK()</code></td><td>Same rank; gaps afterward</td><td>Competition-style placement</td></tr><tr><td><code>ROW_NUMBER()</code></td><td>Unique sequential number</td><td>Third employee in an ordered list</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">With salaries <code>150000, 150000, 130000, 120000</code>, <code>DENSE_RANK()</code> produces <code>1, 1, 2, 3</code>. <code>RANK()</code> produces <code>1, 1, 3, 4</code>, so filtering rank 3 returns <code>130000</code>, not the third distinct salary.</p>



<p class="wp-block-paragraph">To select the third employee instead, use a unique tie-breaker:</p>



<pre class="wp-block-preformatted"><code>WITH OrderedEmployees AS<br>(<br>    SELECT<br>        EmployeeID,<br>        EmployeeName,<br>        Salary,<br>        ROW_NUMBER() OVER (<br>            ORDER BY Salary DESC, EmployeeID ASC<br>        ) AS RowNum<br>    FROM dbo.Employees<br>    WHERE Salary IS NOT NULL<br>)<br>SELECT<br>    EmployeeID,<br>    EmployeeName,<br>    Salary<br>FROM OrderedEmployees<br>WHERE RowNum = 3;</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="976" height="853" src="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-3rd-Highest-Salary-in-SQL-Server.jpg" alt="How to Find 3rd Highest Salary in SQL Server" class="wp-image-23874" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-3rd-Highest-Salary-in-SQL-Server.jpg 976w, https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-3rd-Highest-Salary-in-SQL-Server-300x262.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-3rd-Highest-Salary-in-SQL-Server-768x671.jpg 768w" sizes="auto, (max-width: 976px) 100vw, 976px" /></figure>
</div>


<p class="wp-block-paragraph"><code>EmployeeID</code> makes the ordering repeatable when employees share a salary. This query returns Olivia at <code>130000.00</code>.</p>



<p class="wp-block-paragraph">Do not add <code>EmployeeID</code> inside the salary-only <code>DENSE_RANK()</code> ordering. Doing so ranks the combination of salary and employee ID, breaking salary ties into separate ranks.</p>



<p class="wp-block-paragraph">See the detailed comparison of <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-rank-vs-dense-rank/">RANK versus DENSE_RANK</a> when choosing ranking behavior.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Pro Tip: I keep tie-breakers in the final display order unless the business requirement explicitly ranks individual employees rather than salary amounts.</p>
</blockquote>



<h3 class="wp-block-heading">Return Employee Details and Department-Level Results</h3>



<p class="wp-block-paragraph">A payroll review often needs employee details rather than a single amount. Keep the employee columns in the CTE and remove the outer <code>DISTINCT</code>:</p>



<pre class="wp-block-preformatted"><code>WITH RankedEmployees AS<br>(<br>    SELECT<br>        EmployeeID,<br>        EmployeeName,<br>        DepartmentID,<br>        Salary,<br>        DENSE_RANK() OVER (<br>            ORDER BY Salary DESC<br>        ) AS SalaryRank<br>    FROM dbo.Employees<br>    WHERE Salary IS NOT NULL<br>)<br>SELECT<br>    EmployeeID,<br>    EmployeeName,<br>    DepartmentID,<br>    Salary<br>FROM RankedEmployees<br>WHERE SalaryRank = 3<br>ORDER BY EmployeeID;</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="677" height="640" src="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-3rd-Highest-Salary-in-SQL.jpg" alt="How to Find 3rd Highest Salary in SQL" class="wp-image-23875" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-3rd-Highest-Salary-in-SQL.jpg 677w, https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-3rd-Highest-Salary-in-SQL-300x284.jpg 300w" sizes="auto, (max-width: 677px) 100vw, 677px" /></figure>
</div>


<p class="wp-block-paragraph">This returns Liam and Ava. The final <code>ORDER BY</code> controls presentation; ordering inside a window function does not guarantee the final result order.</p>



<p class="wp-block-paragraph">For the third-highest salary within each department, add <code>PARTITION BY</code>. A partition is a group within which the window calculation restarts:</p>



<pre class="wp-block-preformatted"><code>WITH DepartmentRanks AS<br>(<br>    SELECT<br>        EmployeeID,<br>        EmployeeName,<br>        DepartmentID,<br>        Salary,<br>        DENSE_RANK() OVER (<br>            PARTITION BY DepartmentID<br>            ORDER BY Salary DESC<br>        ) AS SalaryRank<br>    FROM dbo.Employees<br>    WHERE Salary IS NOT NULL<br>)<br>SELECT<br>    DepartmentID,<br>    EmployeeID,<br>    EmployeeName,<br>    Salary<br>FROM DepartmentRanks<br>WHERE SalaryRank = 3<br>ORDER BY DepartmentID, EmployeeID;</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="792" height="682" src="https://sqlserverguides.com/wp-content/uploads/2026/10/Find-3rd-Highest-Salary-in-SQL.jpg" alt="Find 3rd Highest Salary in SQL" class="wp-image-23876" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/Find-3rd-Highest-Salary-in-SQL.jpg 792w, https://sqlserverguides.com/wp-content/uploads/2026/10/Find-3rd-Highest-Salary-in-SQL-300x258.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/Find-3rd-Highest-Salary-in-SQL-768x661.jpg 768w" sizes="auto, (max-width: 792px) 100vw, 792px" /></figure>
</div>


<p class="wp-block-paragraph">Department 10 returns Ava at <code>120000.00</code>. Department 20 returns James at <code>110000.00</code>.</p>



<p class="wp-block-paragraph">Departments with fewer than three distinct non-null salaries return no rows. Learn more about <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-partition-by/">SQL PARTITION BY</a> before extending this pattern to business units or locations.</p>



<p class="wp-block-paragraph">For a multi-tenant application, apply the authorized tenant filter before ranking. Partitioning by tenant organizes results; it does not enforce access control.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Pro Tip: I check where filters belong before reviewing performance. Filtering a department after company-wide ranking answers a different question from ranking within that department.</p>
</blockquote>



<h3 class="wp-block-heading">How to Find 3rd Highest Salary in SQL Server Without Ranking</h3>



<p class="wp-block-paragraph">When only the salary amount is needed, <code>OFFSET ... FETCH</code> provides a compact alternative:</p>



<pre class="wp-block-preformatted"><code>SELECT DISTINCT<br>    Salary AS ThirdHighestSalary<br>FROM dbo.Employees<br>WHERE Salary IS NOT NULL<br>ORDER BY Salary DESC<br>OFFSET 2 ROWS<br>FETCH NEXT 1 ROWS ONLY;</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="697" height="372" src="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-the-3rd-Highest-Salary-in-SQL.jpg" alt="" class="wp-image-23877" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-the-3rd-Highest-Salary-in-SQL.jpg 697w, https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-the-3rd-Highest-Salary-in-SQL-300x160.jpg 300w" sizes="auto, (max-width: 697px) 100vw, 697px" /></figure>
</div>


<p class="wp-block-paragraph"><code>DISTINCT</code> removes duplicate amounts, <code>OFFSET 2</code> skips the two highest distinct salaries, and <code>FETCH NEXT 1</code> returns the next amount.</p>



<p class="wp-block-paragraph">This syntax requires SQL Server 2012 or later and is available in Azure SQL Database and Azure SQL Managed Instance. It returns no rows when a third distinct salary does not exist.</p>



<p class="wp-block-paragraph">Another approach combines <code>TOP</code>, <code>MIN</code>, and a count check:</p>



<pre class="wp-block-preformatted"><code>SELECT<br>    CASE<br>        WHEN COUNT(*) = 3 THEN MIN(Salary)<br>        ELSE NULL<br>    END AS ThirdHighestSalary<br>FROM<br>(<br>    SELECT DISTINCT TOP (3)<br>        Salary<br>    FROM dbo.Employees<br>    WHERE Salary IS NOT NULL<br>    ORDER BY Salary DESC<br>) AS TopSalaries;</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="878" height="542" src="https://sqlserverguides.com/wp-content/uploads/2026/10/Find-the-3rd-Highest-Salary-in-SQL.jpg" alt="" class="wp-image-23878" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/Find-the-3rd-Highest-Salary-in-SQL.jpg 878w, https://sqlserverguides.com/wp-content/uploads/2026/10/Find-the-3rd-Highest-Salary-in-SQL-300x185.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/Find-the-3rd-Highest-Salary-in-SQL-768x474.jpg 768w" sizes="auto, (max-width: 878px) 100vw, 878px" /></figure>
</div>


<p class="wp-block-paragraph">The inner query selects at most three distinct salaries. <code>MIN</code> then returns the lowest of those three.</p>



<p class="wp-block-paragraph">The count check matters. Without it, a table containing only two distinct salaries would incorrectly return the second-highest salary.</p>



<p class="wp-block-paragraph">Choose the result contract deliberately:</p>



<ul class="wp-block-list">
<li><code>DENSE_RANK()</code> and <code>OFFSET ... FETCH</code> naturally return no rows when rank 3 is absent.</li>



<li>The guarded aggregate returns one row containing <code>NULL</code>.</li>
</ul>



<p class="wp-block-paragraph">Neither alternative is automatically faster. Compare execution plans against representative data rather than choosing by query length.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Pro Tip: I use the guarded aggregate when an application expects exactly one result row, even when the requested salary does not exist.</p>
</blockquote>



<h3 class="wp-block-heading">Tune the Query Before Increasing Database Compute</h3>



<p class="wp-block-paragraph">Small HR tables may not need another index. Larger tables or frequently executed reports deserve measurement first.</p>



<p class="wp-block-paragraph">An execution plan shows how SQL Server accesses and processes data. Enable the actual execution plan in SSMS, then measure a representative query:</p>



<pre class="wp-block-preformatted"><code>SET STATISTICS IO ON;<br>SET STATISTICS TIME ON;<br><br>SELECT DISTINCT<br>    Salary<br>FROM dbo.Employees<br>WHERE Salary IS NOT NULL<br>ORDER BY Salary DESC<br>OFFSET 2 ROWS<br>FETCH NEXT 1 ROWS ONLY;<br><br>SET STATISTICS IO OFF;<br>SET STATISTICS TIME OFF;</code></pre>



<p class="wp-block-paragraph"><code>STATISTICS IO</code> reports page reads. <code>STATISTICS TIME</code> reports CPU and elapsed time. Compare repeated executions under similar conditions rather than treating one unusually quick run as proof.</p>



<p class="wp-block-paragraph">For frequent salary-only lookups, test a nonclustered index, which is a separate ordered access structure:</p>



<pre class="wp-block-preformatted"><code>CREATE NONCLUSTERED INDEX IX_Employees_Salary<br>ON dbo.Employees (Salary DESC)<br>WHERE Salary IS NOT NULL;</code></pre>



<p class="wp-block-paragraph">This filtered index excludes unknown salaries. Its ordering may help SQL Server avoid a separate sort, but an existing ascending index can also support a backward scan. Check existing indexes before adding another.</p>



<p class="wp-block-paragraph">Warning: Creating an index consumes storage and resources and can block other work, depending on the operation and environment. Schedule production changes through your deployment process.</p>



<p class="wp-block-paragraph">For department-level reports, an index beginning with <code>(DepartmentID, Salary DESC)</code> may fit better. Included columns can cover employee details, meaning the query can retrieve them from the index without additional table lookups. However, wider indexes require more storage and more work during writes.</p>



<p class="wp-block-paragraph">Use <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-indexes-best-practices/">SQL Server indexing best practices</a> to evaluate that trade-off. Check statistics—metadata describing data distribution—when row estimates are inaccurate.</p>



<p class="wp-block-paragraph">Query Store, when enabled, records query performance history and plans. Use it to detect regressions after deployments. On Azure SQL, tuning can reduce resource pressure, but it does not guarantee a lower bill unless the billing model or provisioned size also changes.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Pro Tip: I compare logical reads and write overhead before keeping a new index. A faster report is not a win if it significantly slows payroll imports.</p>
</blockquote>



<h2 class="wp-block-heading">Protect Salary Data and Test the Reporting Contract</h2>



<p class="wp-block-paragraph">Salary information is sensitive. An application that needs one ranked result should not automatically receive permission to browse the entire payroll table.</p>



<p class="wp-block-paragraph">A stored procedure—a named database routine—can expose only the required output. Grant an existing reporting role permission to execute that procedure:</p>



<pre class="wp-block-preformatted"><code>GRANT EXECUTE<br>ON OBJECT::dbo.GetThirdHighestSalary<br>TO PayrollReportingRole;</code></pre>



<p class="wp-block-paragraph">Run this only after creating the procedure and role. Review <a rel="noopener" target="_blank" href="https://sqlserverguides.com/give-permission-to-execute-stored-procedure-in-sql-server/">granting stored procedure execution permission</a> for the setup.</p>



<p class="wp-block-paragraph">With a conventional same-owner ownership chain, callers can execute an approved procedure without direct table access. Dynamic SQL and cross-database access need separate permission review.</p>



<p class="wp-block-paragraph">Prefer Microsoft Entra authentication and managed identities for supported Azure applications. Where passwords remain necessary, keep them in Azure Key Vault rather than source code or shared files.</p>



<p class="wp-block-paragraph">Require encrypted connections with certificate validation. Check the connection’s encryption status through approved administration tools; encryption alone does not prove that the client validated the server certificate. Prefer private endpoints for Azure access rather than opening broad public firewall ranges.</p>



<p class="wp-block-paragraph">Test as a non-administrator, including unauthorized access attempts. Audit sensitive payroll access without copying salary results into general application logs.</p>



<p class="wp-block-paragraph">Finally, test empty data, fewer than three salaries, ties at rank 3, and concurrent salary changes. Avoid <code>NOLOCK</code>: reading uncommitted data can produce an unreliable payroll result.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Pro Tip: I test permissions using the application identity, not my DBA account. Administrative access can hide missing grants and overly broad permissions alike.</p>
</blockquote>



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



<h4 class="wp-block-heading">How do I find the third-highest salary without duplicates?</h4>



<p class="wp-block-paragraph">Use <code>DENSE_RANK()</code> ordered by salary descending and filter for rank 3. Add <code>DISTINCT</code> to the outer salary-only result so multiple employees earning that amount produce one row.</p>



<h4 class="wp-block-heading">Why does ROW_NUMBER return the wrong third-highest salary?</h4>



<p class="wp-block-paragraph"><code>ROW_NUMBER()</code> counts employees, not distinct salary amounts. If two employees share the highest salary, the third employee can have the second-highest distinct salary.</p>



<h4 class="wp-block-heading">What happens if there are fewer than three distinct salaries?</h4>



<p class="wp-block-paragraph">The ranking and <code>OFFSET ... FETCH</code> examples return no rows. Use the guarded <code>TOP</code> aggregate when your application requires one row containing <code>NULL</code> instead.</p>



<h4 class="wp-block-heading">Can I find the nth-highest salary with the same query?</h4>



<p class="wp-block-paragraph">Yes. Replace <code>SalaryRank = 3</code> with a validated positive integer parameter such as <code>@N</code>. Keep the salary-only ordering so equal salaries retain the same rank.</p>



<h4 class="wp-block-heading">Does this query work in Azure SQL Database?</h4>



<p class="wp-block-paragraph">Yes, the ranking pattern works in Azure SQL Database and Azure SQL Managed Instance. Use the same correctness tests, then evaluate permissions and execution plans in the target database.</p>



<p class="wp-block-paragraph">You can now retrieve the third-highest distinct salary, return tied employees, and rank salaries within departments. Keep the ranking definition explicit, protect payroll access, and measure performance before adding indexes or compute. 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-over-clause/" target="_blank" rel="noopener">Understand the SQL OVER clause</a></li>



<li><a href="https://sqlserverguides.com/row_number-function-in-sql-server/" target="_blank" rel="noopener">Use the ROW_NUMBER function in SQL Server</a></li>



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



<li><a href="https://sqlserverguides.com/sql-server-cte-vs-temp-table/" target="_blank" rel="noopener">Compare SQL Server CTEs and temporary tables</a></li>



<li><a href="https://sqlserverguides.com/how-to-test-stored-procedures-in-sql-server/" target="_blank" rel="noopener">Test stored procedures in SQL Server</a></li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to Find Nth Highest Salary in SQL Server</title>
		<link>https://sqlserverguides.com/how-to-find-nth-highest-salary-in-sql-server/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Tue, 13 Oct 2026 06:45:31 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[How to Find Nth Highest Salary in SQL Server]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=23860</guid>

					<description><![CDATA[A payroll analyst at a mid-size staffing company once sent me a screenshot of a report. It showed the &#8220;third highest salary&#8221; in a department, and the number was wrong. Two people shared the top salary, and the query counted them as separate ranks. The report fed a compensation review, so a small logic bug ... <a title="How to Find Nth Highest Salary in SQL Server" class="read-more" href="https://sqlserverguides.com/how-to-find-nth-highest-salary-in-sql-server/" aria-label="Read more about How to Find Nth Highest Salary in SQL Server">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A payroll analyst at a mid-size staffing company once sent me a screenshot of a report. It showed the &#8220;third highest salary&#8221; in a department, and the number was wrong. Two people shared the top salary, and the query counted them as separate ranks. The report fed a compensation review, so a small logic bug could have changed real paychecks.</p>



<p class="wp-block-paragraph">That is the catch with a question that looks simple. If you want to <strong>find nth highest salary in SQL Server</strong>, the right query depends on how you treat ties, NULL values, and large tables. A wrong choice also slows the report as data grows.</p>



<p class="wp-block-paragraph">I will walk through four working T-SQL methods and show when each one fits. You will also learn how to wrap the logic in a secure stored procedure, speed it up with an index, and run it safely on Azure SQL Database.</p>



<h2 class="wp-block-heading">How to Find Nth Highest Salary in SQL Server</h2>



<h3 class="wp-block-heading">Set Up a Safe Test Table</h3>



<p class="wp-block-paragraph">Start with a small table so you can check every result by hand. A <strong>schema</strong> is a named container for database objects, and <code>dbo</code> is the default one. A <strong>primary key</strong> is a column that uniquely identifies each row. This guide on <a href="https://sqlserverguides.com/sql-server-create-table-with-primary-key/" target="_blank" rel="noopener">creating a table with a primary key</a> covers the basics.</p>



<pre class="wp-block-preformatted"><code>CREATE TABLE dbo.Employees<br>(<br>    EmployeeId  INT           NOT NULL CONSTRAINT PK_Employees PRIMARY KEY,<br>    FullName    NVARCHAR(100) NOT NULL,<br>    Department  NVARCHAR(50)  NOT NULL,<br>    Salary      DECIMAL(12,2) NULL<br>);<br><br>INSERT INTO dbo.Employees (EmployeeId, FullName, Department, Salary)<br>VALUES<br>(1, N'Ava Carter',  N'Engineering', 120000),<br>(2, N'Ben Ortiz',   N'Engineering', 110000),<br>(3, N'Cara Nguyen', N'Engineering', 110000),<br>(4, N'Dan Brooks',  N'Sales',        95000),<br>(5, N'Eli Foster',  N'Sales',        90000),<br>(6, N'Fay Morgan',  N'HR',           90000),<br>(7, N'Gus Patel',   N'HR',           70000),<br>(8, N'Hana Lee',    N'HR',           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="542" src="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-Nth-Highest-Salary-in-SQL-Server-1024x542.jpg" alt="How to Find Nth Highest Salary in SQL Server" class="wp-image-23861" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-Nth-Highest-Salary-in-SQL-Server-1024x542.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-Nth-Highest-Salary-in-SQL-Server-300x159.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-Nth-Highest-Salary-in-SQL-Server-767x406.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-Nth-Highest-Salary-in-SQL-Server.jpg 1422w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">This creates an <code>Employees</code> table with a primary key and a nullable <code>Salary</code> column. I use <code>DECIMAL(12,2)</code> for money, because floating-point types can introduce rounding errors. The data holds two deliberate traps: Ben and Cara tie at 110,000, and Hana has no salary. To see why that matters, read how <a href="https://sqlserverguides.com/sql-null/" target="_blank" rel="noopener">SQL NULL values</a> behave in comparisons and sorting.</p>



<p class="wp-block-paragraph">The distinct salaries in descending order are 120,000, 110,000, 95,000, 90,000, and 70,000. So the 2nd highest is 110,000, the 3rd is 95,000, and the 4th is 90,000. Keep these answers handy to verify each method.</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 always put deliberate ties and NULLs in test data. Most &#8220;nth highest&#8221; bugs I have fixed passed testing only because the sample data had unique values.</p>
</blockquote>



<h3 class="wp-block-heading">Decide What &#8220;Nth Highest&#8221; Means</h3>



<p class="wp-block-paragraph">Before writing code, settle one business rule. There are two valid meanings.</p>



<ul class="wp-block-list">
<li><strong>Distinct salary rank:</strong> If two people earn 110,000, that value is rank 2, and 95,000 is rank 3. This is what most people mean.</li>



<li><strong>Row position:</strong> Each employee takes one position, so the second tied person is rank 3.</li>
</ul>



<p class="wp-block-paragraph">For a salary report, I almost always use distinct rank. Ask the stakeholder before you code. It takes one minute and prevents a bad report.</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 tie rule in a code comment and in the ticket. Six months later, someone always asks why rank 3 shows a lower number than they expected.</p>
</blockquote>



<h3 class="wp-block-heading">Method 1: DENSE_RANK Window Function (Recommended)</h3>



<p class="wp-block-paragraph">A <strong>window function</strong> calculates a value across a set of related rows without collapsing them. If they are new to you, start with <a href="https://sqlserverguides.com/window-functions-in-sql-server/" target="_blank" rel="noopener">window functions in SQL Server</a>. <strong>DENSE_RANK</strong> gives tied rows the same rank and does not skip numbers afterward.</p>



<pre class="wp-block-preformatted"><code>DECLARE @N INT = 3;<br><br>WITH RankedSalaries AS<br>(<br>    SELECT<br>        Salary,<br>        DENSE_RANK() OVER (ORDER BY Salary DESC) AS SalaryRank<br>    FROM dbo.Employees<br>    WHERE Salary IS NOT NULL<br>)<br>SELECT DISTINCT Salary<br>FROM RankedSalaries<br>WHERE SalaryRank = @N;</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="553" src="https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-find-the-nth-highest-salary-in-sql-server-1024x553.jpg" alt="how to find the nth highest salary in sql server" class="wp-image-23862" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-find-the-nth-highest-salary-in-sql-server-1024x553.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-find-the-nth-highest-salary-in-sql-server-300x162.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-find-the-nth-highest-salary-in-sql-server-767x414.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-find-the-nth-highest-salary-in-sql-server.jpg 1208w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">The <strong>CTE</strong> (common table expression) named <code>RankedSalaries</code> is a temporary named result set. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-cte-vs-temp-table/">CTE vs temp table</a> for when each fits. <code>DENSE_RANK() OVER (ORDER BY Salary DESC)</code> ranks salaries from highest to lowest. The <code>WHERE Salary IS NOT NULL</code> filter keeps NULLs out of the ranking. <code>DISTINCT</code> returns one row, even when several employees share the rank.</p>



<p class="wp-block-paragraph">With <code>@N = 3</code>, this returns 95,000. It returns an empty result if <code>@N</code> is larger than the number of distinct salaries. That is useful, because &#8220;no such rank&#8221; is a real answer.</p>



<p class="wp-block-paragraph">Why not <code>ROW_NUMBER()</code>? It gives every row a unique number, even for ties. With our data, rank 3 would return 110,000, which is wrong. The <a rel="noopener" target="_blank" href="https://sqlserverguides.com/row_number-function-in-sql-server/">ROW_NUMBER function</a> is great for deduplication and paging, but not for tied ranks. The article on <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-rank-vs-dense-rank/">RANK vs DENSE_RANK</a> explains why <code>RANK()</code> also fails here: it leaves gaps after ties.</p>



<p class="wp-block-paragraph">This method also returns full employee rows easily. Select <code>FullName</code> and <code>Department</code> instead of only <code>Salary</code>, and you get everyone who earns the nth highest value.</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 default to DENSE_RANK for reports. It is readable, handles ties correctly, and the next developer understands it without a comment.</p>
</blockquote>



<h3 class="wp-block-heading">Method 2: OFFSET FETCH for Modern SQL Server</h3>



<p class="wp-block-paragraph"><strong>OFFSET FETCH</strong> skips a number of rows and then returns the next batch. It works in SQL Server 2012 and later, and in Azure SQL Database. It builds on the <a href="https://sqlserverguides.com/order-by-clause-in-sql-server/" target="_blank" rel="noopener">ORDER BY clause</a>, which it requires.</p>



<pre class="wp-block-preformatted"><code>DECLARE @N INT = 3;<br><br>SELECT DISTINCT Salary<br>FROM dbo.Employees<br>WHERE Salary IS NOT NULL<br>ORDER BY Salary DESC<br>OFFSET @N - 1 ROWS<br>FETCH NEXT 1 ROWS ONLY;</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="568" height="437" src="https://sqlserverguides.com/wp-content/uploads/2026/10/find-nth-highest-salary-in-sql-server.jpg" alt="find nth highest salary in sql server" class="wp-image-23863" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/find-nth-highest-salary-in-sql-server.jpg 568w, https://sqlserverguides.com/wp-content/uploads/2026/10/find-nth-highest-salary-in-sql-server-300x231.jpg 300w" sizes="auto, (max-width: 568px) 100vw, 568px" /></figure>
</div>


<p class="wp-block-paragraph"><code>DISTINCT</code> first collapses duplicates, so each remaining row is one unique salary. <code>ORDER BY Salary DESC</code> puts the highest first. <code>OFFSET @N - 1 ROWS</code> skips the top ranks, and <code>FETCH NEXT 1 ROWS ONLY</code> returns the next one. For <code>@N = 3</code>, it skips two salaries and returns 95,000.</p>



<p class="wp-block-paragraph">I like this method for APIs and paging screens. It is short, and its intent is clear. The trade-off is that it returns only the salary value. If you need employee details, join the result back to the table or use DENSE_RANK.</p>



<p class="wp-block-paragraph">The <code>ORDER BY</code> clause is required with OFFSET. Without it, SQL Server raises a syntax error.</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 OFFSET FETCH when a developer wants a single value for an API. It is hard to misread, and it avoids a nested subquery.</p>
</blockquote>



<h3 class="wp-block-heading">Method 3: TOP With a Nested Query</h3>



<p class="wp-block-paragraph">This is the classic approach and the only one that works on very old versions. The <a href="https://sqlserverguides.com/sql-server-top-clause/" target="_blank" rel="noopener">TOP clause</a> limits how many rows a query returns.</p>



<pre class="wp-block-preformatted"><code>DECLARE @N INT = 3;<br><br>SELECT TOP (1) Salary<br>FROM<br>(<br>    SELECT DISTINCT TOP (@N) Salary<br>    FROM dbo.Employees<br>    WHERE Salary IS NOT NULL<br>    ORDER BY Salary DESC<br>) AS TopSalaries<br>ORDER BY Salary ASC;</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="763" height="567" src="https://sqlserverguides.com/wp-content/uploads/2026/10/find-the-nth-highest-salary-in-sql-server.jpg" alt="find the nth highest salary in sql server" class="wp-image-23864" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/find-the-nth-highest-salary-in-sql-server.jpg 763w, https://sqlserverguides.com/wp-content/uploads/2026/10/find-the-nth-highest-salary-in-sql-server-300x223.jpg 300w" sizes="auto, (max-width: 763px) 100vw, 763px" /></figure>
</div>


<p class="wp-block-paragraph">The inner query takes the top <code>@N</code> distinct salaries, which are 120,000, 110,000, and 95,000 for <code>@N = 3</code>. The outer query then sorts that small set ascending and returns the first row, 95,000. The lowest of the top three is the third highest.</p>



<p class="wp-block-paragraph">Use this method when you must support legacy servers. One caution: if <code>@N</code> exceeds the number of distinct salaries, this query returns the lowest salary instead of nothing. That is a silent wrong answer, so add a check if you cannot guarantee valid input.</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 inherited TOP-based code on SQL Server 2008 systems more than once. When the server is upgraded, I replace it with DENSE_RANK and add a test for the out-of-range case.</p>
</blockquote>



<h3 class="wp-block-heading">Method 4: Correlated Subquery (Learn It, Avoid It)</h3>



<p class="wp-block-paragraph">A <strong>correlated subquery</strong> runs once per outer row. It is a good teaching tool, but a poor choice for large tables. If you need a refresher, read about the <a href="https://sqlserverguides.com/sql-subquery/" target="_blank" rel="noopener">SQL subquery</a> first.</p>



<pre class="wp-block-preformatted"><code>DECLARE @N INT = 3;<br><br>SELECT DISTINCT e1.Salary<br>FROM dbo.Employees AS e1<br>WHERE @N - 1 =<br>(<br>    SELECT COUNT(DISTINCT e2.Salary)<br>    FROM dbo.Employees AS e2<br>    WHERE e2.Salary > e1.Salary<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-full"><img loading="lazy" decoding="async" width="777" height="523" src="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-Nth-Highest-Salary-in-SQL.jpg" alt="How to Find Nth Highest Salary in SQL" class="wp-image-23865" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-Nth-Highest-Salary-in-SQL.jpg 777w, https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-Nth-Highest-Salary-in-SQL-300x202.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/How-to-Find-Nth-Highest-Salary-in-SQL-767x516.jpg 767w" sizes="auto, (max-width: 777px) 100vw, 777px" /></figure>
</div>


<p class="wp-block-paragraph">For each salary, the inner query counts how many distinct salaries are higher. If exactly <code>@N - 1</code> are higher, that salary is the nth highest. The logic is correct, and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-count-distinct/">COUNT DISTINCT</a> handles ties.</p>



<p class="wp-block-paragraph">The cost is the problem. The engine may scan the table repeatedly. On a few hundred rows you will not notice, but on millions of rows it can run far slower than the window function. I show this in training so people recognize it in old code, then I rewrite it.</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 ties</th><th class="has-text-align-left" data-align="left">Min version</th><th class="has-text-align-left" data-align="left">Best for</th><th class="has-text-align-left" data-align="left">Watch out for</th></tr></thead><tbody><tr><td>DENSE_RANK</td><td>Yes</td><td>2005</td><td>Reports, full rows</td><td>Needs a window-capable version</td></tr><tr><td>OFFSET FETCH</td><td>Yes (with DISTINCT)</td><td>2012</td><td>APIs, single value</td><td>Returns salary only</td></tr><tr><td>TOP nested</td><td>Yes (with DISTINCT)</td><td>Any</td><td>Legacy servers</td><td>Wrong result if N is too large</td></tr><tr><td>Correlated subquery</td><td>Yes</td><td>Any</td><td>Learning</td><td>Slow on large tables</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 never ship the correlated version. The first time I tested it on a 5-million-row table, the query ran many times longer than DENSE_RANK on the same data.</p>
</blockquote>



<h3 class="wp-block-heading">Find the Nth Highest Salary Per Department</h3>



<p class="wp-block-paragraph">Real requests are usually grouped. A manager wants the second highest salary in each department. Add <a href="https://sqlserverguides.com/sql-partition-by/" target="_blank" rel="noopener">PARTITION BY</a>, which restarts the ranking for each group.</p>



<pre class="wp-block-preformatted"><code>DECLARE @N INT = 2;<br><br>WITH DeptRanks AS<br>(<br>    SELECT<br>        Department,<br>        FullName,<br>        Salary,<br>        DENSE_RANK() OVER<br>            (PARTITION BY Department ORDER BY Salary DESC) AS SalaryRank<br>    FROM dbo.Employees<br>    WHERE Salary IS NOT NULL<br>)<br>SELECT Department, FullName, Salary<br>FROM DeptRanks<br>WHERE SalaryRank = @N<br>ORDER BY Department;</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="468" src="https://sqlserverguides.com/wp-content/uploads/2026/10/find-nth-highest-salary-in-sql-server-using-row_number-1024x468.jpg" alt="find nth highest salary in sql server using row_number" class="wp-image-23866" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/find-nth-highest-salary-in-sql-server-using-row_number-1024x468.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/10/find-nth-highest-salary-in-sql-server-using-row_number-300x137.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/find-nth-highest-salary-in-sql-server-using-row_number-767x351.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/10/find-nth-highest-salary-in-sql-server-using-row_number.jpg 1371w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph"><code>PARTITION BY Department</code> ranks employees only within their own department. This returns Ben and Cara for Engineering at 110,000, and Eli for Sales at 90,000. HR returns Gus at 70,000, because Hana&#8217;s NULL is excluded and Fay&#8217;s 90,000 is rank 1. Notice that departments with fewer than N distinct salaries return no row, which is correct.</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 gave a finance team a per-department report that silently skipped small departments. Now I add a note to the output explaining that missing rows mean &#8220;fewer than N distinct salaries.&#8221;</p>
</blockquote>



<h3 class="wp-block-heading">Wrap the Logic in a Secure Stored Procedure</h3>



<p class="wp-block-paragraph">A <strong>stored procedure</strong> is a saved, named block of T-SQL. It keeps the logic in one place, so every app gets the same answer. It also lets you grant access to the procedure instead of the whole table. Salary data is sensitive, so this matters. For syntax details, see how to <a href="https://sqlserverguides.com/create-stored-procedure-in-sql-server/" target="_blank" rel="noopener">create a stored procedure in SQL Server</a>.</p>



<pre class="wp-block-preformatted"><code>CREATE OR ALTER PROCEDURE dbo.usp_GetNthHighestSalary<br>    @N INT<br>AS<br>BEGIN<br>    SET NOCOUNT ON;<br><br>    IF @N IS NULL OR @N &lt; 1<br>        THROW 50001, 'Parameter @N must be 1 or greater.', 1;<br><br>    SELECT<br>    (<br>        SELECT DISTINCT Salary<br>        FROM dbo.Employees<br>        WHERE Salary IS NOT NULL<br>        ORDER BY Salary DESC<br>        OFFSET @N - 1 ROWS<br>        FETCH NEXT 1 ROWS ONLY<br>    ) AS NthHighestSalary;<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="520" src="https://sqlserverguides.com/wp-content/uploads/2026/10/nth-highest-salary-in-sql-1024x520.jpg" alt="nth highest salary in sql" class="wp-image-23867" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/nth-highest-salary-in-sql-1024x520.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/10/nth-highest-salary-in-sql-300x152.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/nth-highest-salary-in-sql-767x389.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/10/nth-highest-salary-in-sql.jpg 1153w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph"><code>SET NOCOUNT ON</code> stops extra &#8220;rows affected&#8221; messages. The <code>THROW</code> statement rejects invalid input early. Wrapping the query in a scalar subquery means the result is NULL, not an empty set, when N is too large. Many API callers prefer one predictable row.</p>



<p class="wp-block-paragraph">Now give a reporting identity the least access it needs. In Azure SQL Database, a <strong>contained database user</strong> exists inside the database and does not need a server login. Learn the basics in this guide to <a rel="noopener" target="_blank" href="https://azurelessons.com/create-user-azure-sql-database">creating users in Azure SQL Database</a>.</p>



<pre class="wp-block-preformatted"><code>CREATE USER [&lt;entra-user-or-group-name>] FROM EXTERNAL PROVIDER;<br><br>CREATE ROLE role_salary_report;<br>GRANT EXECUTE ON OBJECT::dbo.usp_GetNthHighestSalary TO role_salary_report;<br>ALTER ROLE role_salary_report ADD MEMBER [&lt;entra-user-or-group-name>];</code></pre>



<p class="wp-block-paragraph"><code>FROM EXTERNAL PROVIDER</code> creates a user backed by <strong>Microsoft Entra ID</strong>, so there is no SQL password to store. The custom role receives only EXECUTE on this one procedure. Because the procedure and table share the same owner, <strong>ownership chaining</strong> lets the procedure read the table without giving the caller direct SELECT rights. For more on this pattern, read how to <a href="https://sqlserverguides.com/give-permission-to-execute-stored-procedure-in-sql-server/" target="_blank" rel="noopener">give permission to execute a stored procedure</a> and the overview of <a href="https://sqlserverguides.com/sql-server-permissions/" target="_blank" rel="noopener">SQL Server permissions</a>.</p>



<p class="wp-block-paragraph">Test with that non-administrator identity. Confirm it can run the procedure and gets a permission error on <code>SELECT * FROM dbo.Employees</code>. Never use <code>sa</code> or a sysadmin account for an 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> In my experience, granting EXECUTE on one procedure catches mistakes early. I once found an analyst with db_owner &#8220;just for a report,&#8221; and a procedure grant fixed that in ten minutes.</p>
</blockquote>



<h3 class="wp-block-heading">Speed It Up With the Right Index</h3>



<p class="wp-block-paragraph">Correct results come first, then speed. Every method above must sort salaries. On a large table, a <strong>nonclustered index</strong> can supply the salaries already sorted. It is a separate structure that stores chosen columns in sorted order with pointers to the rows. Learn the syntax in <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>.</p>



<pre class="wp-block-preformatted"><code>CREATE NONCLUSTERED INDEX IX_Employees_Salary<br>ON dbo.Employees (Salary DESC)<br>INCLUDE (Department, FullName);</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="912" height="333" src="https://sqlserverguides.com/wp-content/uploads/2026/10/nth-highest-salary-in-sql-using-dense_rank.jpg" alt="nth highest salary in sql using dense_rank" class="wp-image-23868" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/nth-highest-salary-in-sql-using-dense_rank.jpg 912w, https://sqlserverguides.com/wp-content/uploads/2026/10/nth-highest-salary-in-sql-using-dense_rank-767x280.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/10/nth-highest-salary-in-sql-using-dense_rank-300x110.jpg 300w" sizes="auto, (max-width: 912px) 100vw, 912px" /></figure>
</div>


<p class="wp-block-paragraph">The key column <code>Salary DESC</code> matches the sort order, so the engine can read from the top and stop early. <code>INCLUDE</code> adds extra columns to the index, which lets queries return names and departments without extra lookups. <strong>Creating an index uses CPU, I/O, and storage, and can block writes on some editions</strong>, so build it during a quiet window.</p>



<p class="wp-block-paragraph">There is a trade-off. Every index makes inserts and updates a bit slower, because SQL Server maintains it. For a payroll table that is read often and changed rarely, that cost is fine. For a heavy write table, measure first.</p>



<p class="wp-block-paragraph">Check the real <strong>execution plan</strong>, which shows how SQL Server ran your query. Look for a sort operator and a scan. After adding the index, you should see an ordered scan or seek and a cheaper sort. Turn on <strong>Query Store</strong>, the built-in feature that tracks query performance over time, to compare before and after. Also keep statistics fresh, as shown in <a href="https://sqlserverguides.com/update-stats-on-table-sql-server/" target="_blank" rel="noopener">updating stats on a table</a>, so the optimizer picks good plans.</p>



<p class="wp-block-paragraph">If the query is still slow after tuning, scaling up compute is the last option, not the first. Bigger compute hides a bad query but does not fix it, and you pay for it every hour.</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 team double their Azure SQL tier to fix a slow ranking report. One index cut the runtime and let them stay on the smaller tier.</p>
</blockquote>



<h3 class="wp-block-heading">Run It Safely on Azure SQL Database</h3>



<p class="wp-block-paragraph">Many readers practice on Azure SQL Database. Here is a secure, low-cost dev setup.</p>



<pre class="wp-block-preformatted"><code>az group create \<br>  --name rg-salary-dev \<br>  --location eastus<br><br>az sql server create \<br>  --name &lt;sql-server-name> \<br>  --resource-group rg-salary-dev \<br>  --location eastus \<br>  --enable-ad-only-auth \<br>  --external-admin-principal-type Group \<br>  --external-admin-name &lt;entra-admin-group-name> \<br>  --external-admin-sid &lt;entra-admin-group-object-id></code></pre>



<p class="wp-block-paragraph">The first command creates a <strong>resource group</strong>, a container for related resources, in the East US <strong>region</strong>. The second creates a logical SQL server. <code>--enable-ad-only-auth</code> turns off SQL passwords so only Entra ID sign-ins work. The admin is an Entra group, which is easier to manage than one person.</p>



<pre class="wp-block-preformatted"><code>az sql db create \<br>  --resource-group rg-salary-dev \<br>  --server &lt;sql-server-name> \<br>  --name sqldb-salary-dev \<br>  --edition GeneralPurpose \<br>  --compute-model Serverless \<br>  --family Gen5 \<br>  --capacity 1 \<br>  --min-capacity 0.5 \<br>  --auto-pause-delay 60 \<br>  --backup-storage-redundancy Local</code></pre>



<p class="wp-block-paragraph">This creates a <strong>serverless</strong> database, which bills compute per second while active and can auto-pause. <code>--auto-pause-delay 60</code> pauses after 60 idle minutes, which suits dev work. <code>--backup-storage-redundancy Local</code> keeps backups cheaper in dev. Production usually needs zone or geo redundancy instead. Storage still costs money while the database is paused.</p>



<p class="wp-block-paragraph">Now lock down the network. For real environments, disable public access and use a <strong>private endpoint</strong>, which gives private network access to a supported service. <strong>Never add a firewall rule that opens the whole internet.</strong> For temporary laptop access, allow only your own IP and remove it afterward.</p>



<p class="wp-block-paragraph">Connect with a client tool such as SSMS. This guide to <a rel="noopener" target="_blank" href="https://azurelessons.com/connect-to-azure-sql-from-ssms">connecting to Azure SQL from SSMS</a> covers it. Require encryption by keeping Encrypt set to true in your connection settings. Verify it with this query:</p>



<pre class="wp-block-preformatted"><code>SELECT session_id, encrypt_option, auth_scheme<br>FROM sys.dm_exec_connections<br>WHERE session_id = @@SPID;</code></pre>



<p class="wp-block-paragraph">This <strong>dynamic management view</strong> (DMV) shows details for your current connection. <code>encrypt_option</code> should read TRUE. If it does not, fix the client setting before you query real data.</p>



<p class="wp-block-paragraph">Because salaries are sensitive, add controls on top. <strong>Dynamic data masking</strong> hides column values from low-privilege users. <strong>Auditing</strong> records who ran what, and alerts help you catch unusual access.</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 set a budget on every dev resource group on day one. A forgotten database once ran for three weeks, and a <a rel="noopener" target="_blank" href="https://azurelessons.com/azure-budget-alerts">budget alert</a> would have caught it on day two.</p>
</blockquote>



<h3 class="wp-block-heading">Common Errors and Troubleshooting</h3>



<p class="wp-block-paragraph"><strong>You get no rows.</strong> N is larger than the count of distinct salaries, or every salary in the group is NULL. This is normal behavior, so decide how your app should handle it.</p>



<p class="wp-block-paragraph"><strong>Rank 3 returns a wrong number.</strong> You probably used <code>ROW_NUMBER()</code> or <code>RANK()</code>. <code>RANK()</code> leaves gaps after ties, so rank 3 can disappear. Use <code>DENSE_RANK()</code> for distinct rank.</p>



<p class="wp-block-paragraph"><strong>&#8220;Invalid usage of the option NEXT in the FETCH statement.&#8221;</strong> OFFSET FETCH needs an <code>ORDER BY</code> clause, and it is not available before SQL Server 2012.</p>



<p class="wp-block-paragraph"><strong>The query is slow.</strong> Check the execution plan for a full scan and a sort. Add the index above, update statistics if they are stale, and compare runs in Query Store.</p>



<p class="wp-block-paragraph"><strong>You cannot sign in.</strong> Check the firewall, private endpoint DNS, and the identity you used. For SQL password logins, <a href="https://sqlserverguides.com/sql-server-error-18456/" target="_blank" rel="noopener">SQL Server error 18456</a> explains the common causes.</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 result looks wrong, I run the ranking CTE alone with every column visible. Seeing ranks next to salaries explains most bugs in a minute.</p>
</blockquote>



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



<h4 class="wp-block-heading">What is the easiest way to find the nth highest salary in SQL Server?</h4>



<p class="wp-block-paragraph">Use <code>DENSE_RANK() OVER (ORDER BY Salary DESC)</code> in a CTE and filter for your rank. It handles ties correctly and is easy to read. It works in SQL Server 2005 and later, and in Azure SQL Database.</p>



<h4 class="wp-block-heading">What is the difference between RANK, DENSE_RANK, and ROW_NUMBER?</h4>



<p class="wp-block-paragraph"><code>ROW_NUMBER</code> gives each row a unique number. <code>RANK</code> gives ties the same number but skips the next numbers. <code>DENSE_RANK</code> gives ties the same number and skips nothing, which makes it the right choice for nth highest salary.</p>



<h4 class="wp-block-heading">How do I find the second highest salary without a window function?</h4>



<p class="wp-block-paragraph">Use <code>SELECT MAX(Salary) FROM dbo.Employees WHERE Salary &lt; (SELECT MAX(Salary) FROM dbo.Employees)</code>. This works on any version. For other ranks, use the nested TOP or OFFSET FETCH method.</p>



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



<p class="wp-block-paragraph">Add <code>WHERE Salary IS NOT NULL</code> to every method. NULLs can sort into the result and cause wrong answers, especially when N is larger than the data. Decide with the business whether missing salaries should count at all.</p>



<h4 class="wp-block-heading">How do I make the nth highest salary query faster?</h4>



<p class="wp-block-paragraph">Create a nonclustered index on <code>Salary DESC</code> with included columns, then review the execution plan. Use Query Store to confirm the improvement. Add compute only after the query itself is tuned.</p>



<p class="wp-block-paragraph">You learned four ways to find the nth highest salary in SQL Server, why DENSE_RANK is the safest default, and how to secure and tune the query on Azure SQL Database. Always decide your tie and NULL rules first, and give applications access only through least-privilege procedures. I hope you found this article helpful.</p>



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



<ul class="wp-block-list">
<li>Read how to <a href="https://sqlserverguides.com/how-to-use-rank-function-in-sql-server/" target="_blank" rel="noopener">use the RANK function in SQL Server</a> for leaderboard-style queries.</li>



<li>Compare a <a href="https://sqlserverguides.com/sql-subquery-vs-join/" target="_blank" rel="noopener">SQL subquery vs. JOIN</a> to pick the right pattern.</li>



<li>Learn <a href="https://sqlserverguides.com/how-to-use-group-by-clause-in-sql-server/" target="_blank" rel="noopener">how to use GROUP BY in SQL Server</a> for salary summaries by department.</li>



<li>Follow these <a href="https://sqlserverguides.com/sql-query-optimization-tips/" target="_blank" rel="noopener">SQL query optimization tips</a> to speed up reports.</li>



<li>Review <a href="https://sqlserverguides.com/sql-indexes-best-practices/" target="_blank" rel="noopener">SQL index best practices</a> before adding more indexes.</li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to Find Second Highest Salary in SQL Server</title>
		<link>https://sqlserverguides.com/how-to-find-second-highest-salary-in-sql-server/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Thu, 08 Oct 2026 06:50:54 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[How to Find Second Highest Salary in SQL Server]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=23847</guid>

					<description><![CDATA[A payroll analyst at a mid-sized staffing company once sent me a spreadsheet and a worried email. Their HR dashboard showed the &#8220;second highest salary&#8221; per department, but two departments returned the same number as the top salary. The query had used TOP 1 with a sort, and it counted tied employees as separate ranks. ... <a title="How to Find Second Highest Salary in SQL Server" class="read-more" href="https://sqlserverguides.com/how-to-find-second-highest-salary-in-sql-server/" aria-label="Read more about How to Find Second Highest Salary in SQL Server">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A payroll analyst at a mid-sized staffing company once sent me a spreadsheet and a worried email. Their HR dashboard showed the &#8220;second highest salary&#8221; per department, but two departments returned the same number as the top salary. The query had used <code>TOP 1</code> with a sort, and it counted tied employees as separate ranks. Nobody caught it until a compensation review put the wrong figure in front of leadership.</p>



<p class="wp-block-paragraph">This is why <strong>how to find second highest salary in SQL Server</strong> is more than an interview question. The right answer depends on how you treat ties, empty results, and NULL values. It also depends on table size, because some patterns scan the whole table twice. I have fixed this exact bug in HR, sales-commission, and billing reports.</p>



<p class="wp-block-paragraph">By the end, you will know five reliable T-SQL methods, when to use each one, how to extend them to the Nth highest salary and to per-department results, and how to secure and tune the query in Azure SQL Database.</p>



<h2 class="wp-block-heading">How to Find Second Highest Salary in SQL Server</h2>



<h3 class="wp-block-heading">Set Up a Realistic Salary Table</h3>



<p class="wp-block-paragraph">Start with a clean test table. A <strong>schema</strong> is a named container that groups database objects, and it helps you control permissions. I keep HR data in its own <code>hr</code> schema so I can lock it down separately from application tables.</p>



<pre class="wp-block-preformatted"><code>CREATE SCHEMA hr;<br>GO<br><br>CREATE TABLE hr.Employees<br>(<br>    EmployeeId   INT IDENTITY(1,1) PRIMARY KEY,<br>    FullName     NVARCHAR(100) NOT NULL,<br>    Department   NVARCHAR(50)  NOT NULL,<br>    Salary       DECIMAL(12,2) NULL<br>);<br>GO<br><br>INSERT INTO hr.Employees (FullName, Department, Salary)<br>VALUES<br>('Ava Collins',  'Engineering', 145000),<br>('Noah Rivera',  'Engineering', 145000),<br>('Liam Patel',   'Engineering', 128000),<br>('Mia Johnson',  'Sales',       118000),<br>('Ethan Brooks', 'Sales',        99000),<br>('Zoe Carter',   'Sales',        99000),<br>('Owen Reed',    'Support',      72000),<br>('Ivy Morgan',   'Support',      NULL);<br>GO</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="713" src="https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-get-second-highest-salary-in-sql-server-1024x713.jpg" alt="how to get second highest salary in sql server" class="wp-image-23848" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-get-second-highest-salary-in-sql-server-1024x713.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-get-second-highest-salary-in-sql-server-300x209.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-get-second-highest-salary-in-sql-server-767x534.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-get-second-highest-salary-in-sql-server.jpg 1091w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">This creates the <code>hr</code> schema and an <code>Employees</code> table. <code>EmployeeId</code> is a <strong>primary key</strong>, which uniquely identifies each row and creates a clustered index by default. The <code>Salary</code> column allows NULL on purpose, because real HR data often has missing values. The sample rows include ties in Engineering and Sales, which is where most queries fail.</p>



<p class="wp-block-paragraph">Before you run anything, decide what &#8220;second highest&#8221; means. If two people earn 145,000, is the second highest 145,000 or 128,000? In almost every business report, you want the second highest <strong>distinct</strong> salary, which is 128,000. Write that rule down in the ticket.</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 ask the report owner one question before writing any query: &#8220;If two people tie for first, who is second?&#8221; That answer decides which method I use.</p>
</blockquote>



<h3 class="wp-block-heading">Method Overview: Which Query Fits</h3>



<p class="wp-block-paragraph">Several patterns solve this problem. They differ in readability, tie handling, and performance. Here is how I compare them.</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 ties as distinct values</th><th class="has-text-align-left" data-align="left">Returns NULL if none exists</th><th class="has-text-align-left" data-align="left">Easy to extend to Nth</th></tr></thead><tbody><tr><td>Subquery with MAX</td><td>Yes</td><td>Yes</td><td>No</td></tr><tr><td>TOP with ORDER BY twice</td><td>Needs DISTINCT</td><td>No</td><td>Moderate</td></tr><tr><td>OFFSET FETCH</td><td>Needs DISTINCT</td><td>No</td><td>Yes</td></tr><tr><td>DENSE_RANK window function</td><td>Yes</td><td>No</td><td>Yes</td></tr><tr><td>Correlated subquery count</td><td>Yes</td><td>No</td><td>Yes</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Use the first method for a fast one-off check. Use the window function method for production reports, since it groups by department cleanly.</p>



<h3 class="wp-block-heading">Find the Second Highest Salary With a Subquery</h3>



<p class="wp-block-paragraph">A <strong>subquery</strong> is a query nested inside another query. This approach finds the maximum salary that is lower than the overall maximum. See this overview of <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-subquery/">SQL subqueries</a> if you need a refresher.</p>



<pre class="wp-block-preformatted"><code>SELECT MAX(Salary) AS SecondHighestSalary<br>FROM   hr.Employees<br>WHERE  Salary &lt; (SELECT MAX(Salary) FROM hr.Employees);</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output as the second-highest salary 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="280" src="https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-find-second-highest-salary-in-sql-server-1024x280.jpg" alt="How to Find Second Highest Salary in SQL Server" class="wp-image-23849" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-find-second-highest-salary-in-sql-server-1024x280.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-find-second-highest-salary-in-sql-server-300x82.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-find-second-highest-salary-in-sql-server-767x210.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-find-second-highest-salary-in-sql-server.jpg 1077w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">The inner query returns the top salary, 145,000. The outer query then takes the <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-max-function-in-sql-server/">MAX value</a> from every salary below it, which is 128,000. Because <code>&lt;</code> skips all rows equal to the top salary, ties do not cause trouble.</p>



<p class="wp-block-paragraph">This method has a useful side effect. If the table has only one distinct salary, <code>MAX</code> over zero rows returns NULL instead of an error or an empty set. That is exactly what a dashboard needs, because the application gets a clear &#8220;no second value&#8221; signal. NULL salaries are ignored by <code>MAX</code>, so the NULL for Ivy Morgan does not break anything.</p>



<p class="wp-block-paragraph">The downside is that the query reads the table twice. On a small HR table, nobody notices. On a table with 50 million billing rows, the second pass matters, and you need an index on the column. I cover that 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 use this subquery for quick data checks during incident calls. It is short, safe with ties, and nobody has to remember window function syntax at 2 a.m.</p>
</blockquote>



<h3 class="wp-block-heading">Find the Second Highest Salary With TOP and ORDER BY</h3>



<p class="wp-block-paragraph">The classic pattern uses <code>TOP</code> twice with opposite sorts. The <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-top-clause/">TOP clause</a> limits the number of rows returned, and the <a rel="noopener" target="_blank" href="https://sqlserverguides.com/order-by-clause-in-sql-server/">ORDER BY clause</a> controls their order.</p>



<pre class="wp-block-preformatted"><code>SELECT TOP 1 Salary AS SecondHighestSalary<br>FROM (<br>    SELECT DISTINCT TOP 2 Salary<br>    FROM   hr.Employees<br>    WHERE  Salary IS NOT NULL<br>    ORDER BY Salary DESC<br>) AS t<br>ORDER BY Salary ASC;</code></pre>



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


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="872" height="446" src="https://sqlserverguides.com/wp-content/uploads/2026/10/find-second-highest-salary-in-sql-server.jpg" alt="find second highest salary in sql server" class="wp-image-23850" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/find-second-highest-salary-in-sql-server.jpg 872w, https://sqlserverguides.com/wp-content/uploads/2026/10/find-second-highest-salary-in-sql-server-766x392.jpg 766w, https://sqlserverguides.com/wp-content/uploads/2026/10/find-second-highest-salary-in-sql-server-300x153.jpg 300w" sizes="auto, (max-width: 872px) 100vw, 872px" /></figure>
</div>


<p class="wp-block-paragraph">The inner query uses <code>DISTINCT</code> to remove duplicate salaries, takes the two highest values, and sorts them from high to low. The outer query then sorts those two values from low to high and returns the first one, which is the second highest. The <code>IS NOT NULL</code> filter keeps NULLs out. See <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-distinct/">SQL DISTINCT</a> for how duplicates are removed.</p>



<p class="wp-block-paragraph">Without <code>DISTINCT</code>, this query returns 145,000, the same bug that hit my client. This is the most common mistake I see in code reviews.</p>



<p class="wp-block-paragraph">There is one more catch. If the table holds a single distinct salary, the outer query returns that one salary, not NULL, because the inner query returns only one row. The result is wrong, and it looks valid. Always test with a one-row table.</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 keep a tiny test table with three cases: ties at the top, one distinct value, and all NULLs. If a query passes all three, I trust it.</p>
</blockquote>



<h3 class="wp-block-heading">Find the Second Highest Salary With OFFSET FETCH</h3>



<p class="wp-block-paragraph"><code>OFFSET ... FETCH</code> skips a number of rows and returns the next group. It is available in SQL Server 2012 and later, and in Azure SQL Database. It is part of the <code>ORDER BY</code> clause, so it requires one.</p>



<pre class="wp-block-preformatted"><code>SELECT DISTINCT Salary AS SecondHighestSalary<br>FROM   hr.Employees<br>WHERE  Salary IS NOT NULL<br>ORDER BY Salary DESC<br>OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY;</code></pre>



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


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="920" height="335" src="https://sqlserverguides.com/wp-content/uploads/2026/10/second-highest-salary-in-sql-server-with-example.jpg" alt="second highest salary in sql server with example" class="wp-image-23851" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/second-highest-salary-in-sql-server-with-example.jpg 920w, https://sqlserverguides.com/wp-content/uploads/2026/10/second-highest-salary-in-sql-server-with-example-300x109.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/second-highest-salary-in-sql-server-with-example-766x279.jpg 766w" sizes="auto, (max-width: 920px) 100vw, 920px" /></figure>
</div>


<p class="wp-block-paragraph">Here, <code>DISTINCT</code> collapses the salaries to unique values. <code>ORDER BY Salary DESC</code> puts the largest first. <code>OFFSET 1 ROWS</code> skips the first value, and <code>FETCH NEXT 1 ROWS ONLY</code> returns the next one. The pattern is easy to read, and changing the offset gives you the third or fourth highest.</p>



<p class="wp-block-paragraph">The trade-off is that an empty result returns zero rows, not NULL. If your application expects a single value, wrap the query in a subquery with <code>SELECT (...)</code>, which converts an empty result to NULL. Handle that case in code, because a missing row can crash a report that expects one.</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 payroll API throw a null reference error because the query returned no row for a new department. I now wrap Nth-value queries so the app always gets exactly one row.</p>
</blockquote>



<h3 class="wp-block-heading">Find the Second Highest Salary With DENSE_RANK</h3>



<p class="wp-block-paragraph">A <strong>window function</strong> performs a calculation across a set of rows related to the current row, without collapsing them. This is the method I recommend for production reports. Read more on <a rel="noopener" target="_blank" href="https://sqlserverguides.com/window-functions-in-sql-server/">window functions in SQL Server</a> before you start.</p>



<pre class="wp-block-preformatted"><code>WITH RankedSalaries AS<br>(<br>    SELECT Salary,<br>           DENSE_RANK() OVER (ORDER BY Salary DESC) AS SalaryRank<br>    FROM   hr.Employees<br>    WHERE  Salary IS NOT NULL<br>)<br>SELECT DISTINCT Salary AS SecondHighestSalary<br>FROM   RankedSalaries<br>WHERE  SalaryRank = 2;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output as the second-highest salary 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="415" src="https://sqlserverguides.com/wp-content/uploads/2026/10/second-highest-salary-in-sql-server-1024x415.jpg" alt="second highest salary in sql server" class="wp-image-23852" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/second-highest-salary-in-sql-server-1024x415.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/10/second-highest-salary-in-sql-server-300x121.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/second-highest-salary-in-sql-server-767x311.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/10/second-highest-salary-in-sql-server.jpg 1250w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">The <strong>CTE</strong> (common table expression) named <code>RankedSalaries</code> is a temporary named result set that exists for one statement. For when to pick a CTE over a temp table, see this comparison of <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-cte-vs-temp-table/">CTEs vs. temp tables</a>. <code>DENSE_RANK()</code> assigns rank 1 to 145,000, rank 2 to 128,000, and so on, with no gaps after ties.</p>



<h4 class="wp-block-heading">DENSE_RANK vs. RANK vs. ROW_NUMBER</h4>



<p class="wp-block-paragraph">These three ranking functions give different answers on tied data, and picking the wrong one is the root of many bugs.</p>



<ul class="wp-block-list">
<li><strong>ROW_NUMBER</strong> gives every row a unique number, so tied salaries get different numbers. Read about <a href="https://sqlserverguides.com/row_number-function-in-sql-server/" target="_blank" rel="noopener">ROW_NUMBER in SQL Server</a>.</li>



<li><strong>RANK</strong> gives tied rows the same rank but skips the next number. Two people at rank 1 means the next rank is 3, so rank 2 never appears.</li>



<li><strong>DENSE_RANK</strong> gives tied rows the same rank and does not skip numbers.</li>
</ul>



<p class="wp-block-paragraph">The full difference is explained in <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-rank-vs-dense-rank/">RANK vs. DENSE_RANK</a>, and you can learn the basics of the <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-rank-function-in-sql-server/">RANK function</a>. For &#8220;second highest distinct salary,&#8221; use <code>DENSE_RANK</code>. If you used <code>RANK</code> on my sample data, rank 2 would not exist for Engineering, and the query would return nothing.</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 use RANK for &#8220;Nth highest&#8221; reports. A gap in rank numbers causes empty results that look like missing data, and users file bugs against the wrong team.</p>
</blockquote>



<h3 class="wp-block-heading">Second Highest Salary Per Department</h3>



<p class="wp-block-paragraph">Most real requests ask for the second highest salary in each department, not just one overall number. Add <code>PARTITION BY</code>, which splits the ranking into separate groups.</p>



<pre class="wp-block-preformatted"><code>WITH DeptRanks AS<br>(<br>    SELECT Department,<br>           Salary,<br>           DENSE_RANK() OVER (PARTITION BY Department<br>                              ORDER BY Salary DESC) AS SalaryRank<br>    FROM   hr.Employees<br>    WHERE  Salary IS NOT NULL<br>)<br>SELECT Department, Salary AS SecondHighestSalary<br>FROM   DeptRanks<br>WHERE  SalaryRank = 2<br>GROUP BY Department, Salary;</code></pre>



<p class="wp-block-paragraph">After running the query above, I got the expected output as the second-highest salary 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="423" src="https://sqlserverguides.com/wp-content/uploads/2026/10/get-second-highest-salary-in-sql-server-1024x423.jpg" alt="" class="wp-image-23853" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/get-second-highest-salary-in-sql-server-1024x423.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/10/get-second-highest-salary-in-sql-server-767x317.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/10/get-second-highest-salary-in-sql-server-300x124.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/get-second-highest-salary-in-sql-server.jpg 1251w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph"><code>PARTITION BY Department</code> restarts the ranking for each department. Engineering returns 128,000, Sales returns 99,000, and Support returns nothing, because Support has only one non-NULL salary. The final <code>GROUP BY</code> removes repeated rows when several employees share the same second-place salary. If you need a refresher, read how to <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-use-group-by-clause-in-sql-server/">use GROUP BY in SQL Server</a>.</p>



<p class="wp-block-paragraph">To include departments with no second value, add a <code>LEFT JOIN</code> from a department list. This is a business decision, so confirm with the report owner whether blank rows should appear.</p>



<h3 class="wp-block-heading">Generalize to the Nth Highest Salary</h3>



<p class="wp-block-paragraph">Hardcoding rank 2 works once. A reusable version takes the rank as a parameter. A <strong>stored procedure</strong> is a saved, parameterized T-SQL routine that applications can call.</p>



<pre class="wp-block-preformatted"><code>CREATE OR ALTER PROCEDURE hr.GetNthHighestSalary<br>    @N INT<br>AS<br>BEGIN<br>    SET NOCOUNT ON;<br><br>    IF @N &lt; 1<br>    BEGIN<br>        THROW 50001, 'N must be 1 or greater.', 1;<br>    END;<br><br>    WITH Ranked AS<br>    (<br>        SELECT Salary,<br>               DENSE_RANK() OVER (ORDER BY Salary DESC) AS SalaryRank<br>        FROM   hr.Employees<br>        WHERE  Salary IS NOT NULL<br>    )<br>    SELECT (SELECT TOP 1 Salary FROM Ranked WHERE SalaryRank = @N) AS NthHighestSalary;<br>END;<br>GO</code></pre>



<p class="wp-block-paragraph">After running 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="495" src="https://sqlserverguides.com/wp-content/uploads/2026/10/select-second-highest-salary-in-sql-server-1024x495.jpg" alt="select second highest salary in sql server" class="wp-image-23854" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/select-second-highest-salary-in-sql-server-1024x495.jpg 1024w, https://sqlserverguides.com/wp-content/uploads/2026/10/select-second-highest-salary-in-sql-server-300x145.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/select-second-highest-salary-in-sql-server-767x371.jpg 767w, https://sqlserverguides.com/wp-content/uploads/2026/10/select-second-highest-salary-in-sql-server.jpg 1121w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph"><code>SET NOCOUNT ON</code> stops SQL Server from sending row-count messages, which trims network chatter. The <code>IF</code> block validates the input and throws an error for bad values. The final <code>SELECT (subquery)</code> guarantees exactly one row, with NULL when the Nth value does not exist.</p>



<p class="wp-block-paragraph">Test it with <code>EXEC hr.GetNthHighestSalary @N = 2;</code>. Use parameters, never string concatenation, so the query stays safe from SQL injection. For more on structure, see how to <a rel="noopener" target="_blank" href="https://sqlserverguides.com/give-permission-to-execute-stored-procedure-in-sql-server/">give permission to execute a stored procedure</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 validate parameters inside the procedure even when the app validates them too. Another team will call your procedure someday, and they will not read your front-end code.</p>
</blockquote>



<h3 class="wp-block-heading">Secure Salary Data</h3>



<p class="wp-block-paragraph">Salary is sensitive data. A report query is only as safe as the access around it. Apply <strong>least privilege</strong>, which means each account gets only the permissions it needs. Do not let an app run as <code>sa</code>, <code>sysadmin</code>, or <code>db_owner</code>.</p>



<p class="wp-block-paragraph">In <strong>Azure SQL Database</strong>, prefer <strong>Microsoft Entra ID</strong> authentication and a <strong>managed identity</strong> over SQL passwords. A managed identity is an Azure-managed account that lets an app authenticate without a stored secret. This reduces the risk of a leaked connection string.</p>



<pre class="wp-block-preformatted"><code>CREATE USER [app-hr-reports] FROM EXTERNAL PROVIDER;<br><br>GRANT EXECUTE ON OBJECT::hr.GetNthHighestSalary TO [app-hr-reports];<br>DENY  SELECT  ON SCHEMA::hr TO [app-hr-reports];</code></pre>



<p class="wp-block-paragraph"><code>CREATE USER ... FROM EXTERNAL PROVIDER</code> maps an Entra ID identity to a database user. <code>GRANT EXECUTE</code> lets it run only the procedure. <code>DENY SELECT</code> blocks direct reads of the <code>hr</code> tables, so the report account sees the answer but never the whole payroll. To audit these rights later, use <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-permissions/">SQL Server permissions</a> and check <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-check-user-permissions-on-table/">user permissions on a table</a>.</p>



<p class="wp-block-paragraph">Next, think about the network and the data. Enforce TLS encrypted connections, which is the default on Azure SQL Database, and verify it by querying <code>sys.dm_exec_connections</code> and checking that <code>encrypt_option</code> is <code>TRUE</code>. Use a private endpoint instead of a public firewall rule, and never open 0.0.0.0 to 255.255.255.255. </p>



<p class="wp-block-paragraph">Consider <strong>dynamic data masking</strong> on the <code>Salary</code> column for support staff, and turn on auditing plus Microsoft Defender for SQL to flag unusual reads of HR tables. Test everything by logging in as the non-administrator report user.</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 in as the report account before every release and try a direct <code>SELECT * FROM hr.Employees</code>. If that works, my permissions are wrong, and I want to find out before an auditor does.</p>
</blockquote>



<h3 class="wp-block-heading">Speed Up the Query With Indexing</h3>



<p class="wp-block-paragraph">On a small table, any method works. On a large table, indexing decides everything. An <strong>index</strong> is a sorted structure that lets SQL Server find rows without scanning the whole table. The basic steps are covered 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_Employees_Department_Salary<br>ON hr.Employees (Department, Salary DESC);</code></pre>



<p class="wp-block-paragraph">This <strong>nonclustered index</strong> stores department and salary in sorted order, with salary descending. A ranking query partitioned by department can then read rows in the required order instead of sorting them in memory. Without it, the plan includes an expensive Sort operator.</p>



<p class="wp-block-paragraph">There is a trade-off. Each index adds work to every INSERT and UPDATE, plus storage. Salary tables change rarely, so the cost is low. A high-write table, like order lines, needs more caution. Follow these <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-indexes-best-practices/">SQL index best practices</a> and confirm the benefit in the <strong>actual execution plan</strong> before you keep an index.</p>



<p class="wp-block-paragraph">Then compare methods. Run each query with <code>SET STATISTICS IO ON</code> and <code>SET STATISTICS TIME ON</code>, and read logical reads and CPU time. In Azure SQL Database, <strong>Query Store</strong> records plans and runtime stats over time, so you can prove a change helped. Wasted CPU can push you to a bigger vCore or DTU tier, which raises cost, so fixing one slow report is cheaper than scaling the database.</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 compare at least two methods on production-sized data. The &#8220;clever&#8221; query is not always the fastest, and the plan tells me the truth in about 30 seconds.</p>
</blockquote>



<h3 class="wp-block-heading">Common Errors and Troubleshooting</h3>



<p class="wp-block-paragraph">These are the problems I see most.</p>



<p class="wp-block-paragraph"><strong>The query returns the highest salary instead of the second.</strong> You skipped <code>DISTINCT</code>, or you used <code>ROW_NUMBER</code>. Switch to <code>DENSE_RANK</code> or add <code>DISTINCT</code> before ranking.</p>



<p class="wp-block-paragraph"><strong>The query returns no rows.</strong> You used <code>RANK</code>, or there is no second distinct value. Return NULL with a wrapped subquery so the caller always gets one row.</p>



<p class="wp-block-paragraph"><strong>NULL salaries change the result.</strong> In sort order, NULL values appear first in ascending order and last in descending order, and they may confuse ranking. Add <code>WHERE Salary IS NOT NULL</code>, and read about handling <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-null/">NULL values</a> for details.</p>



<p class="wp-block-paragraph"><strong>&#8220;Incorrect syntax near OFFSET.&#8221;</strong> <code>OFFSET</code> needs <code>ORDER BY</code>, and it needs SQL Server 2012 or later. Check your version and your database compatibility level.</p>



<p class="wp-block-paragraph"><strong>Slow performance on large tables.</strong> Check the plan for scans and Sort operators. Add a supporting index, and update statistics if the estimates look wrong.</p>



<h3 class="wp-block-heading">Production Readiness Checklist</h3>



<ul class="wp-block-list">
<li><strong>Define tie rules:</strong> Document whether &#8220;second highest&#8221; means distinct salary or second row, and add test cases for ties, single values, and NULLs.</li>



<li><strong>Use least privilege:</strong> Give the report account EXECUTE on one procedure through a Microsoft Entra ID identity, not direct table access or sysadmin rights.</li>



<li><strong>Require encryption:</strong> Keep TLS on, use a private endpoint, and verify encrypted sessions with a DMV check.</li>



<li><strong>Test recovery:</strong> Confirm PITR or backup restores work before you ship schema or index changes.</li>



<li><strong>Monitor cost and speed:</strong> Track CPU in Query Store, set Azure budgets and alerts, and tag databases by environment.</li>



<li><strong>Right-size by environment:</strong> Use smaller, pausable compute in development and zone-redundant settings only where production needs them.</li>
</ul>



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



<h4 class="wp-block-heading">What is the easiest way to find the second highest salary in SQL Server?</h4>



<p class="wp-block-paragraph">The subquery with MAX is the shortest: select the maximum salary that is less than the overall maximum. It handles ties and returns NULL when no second value exists. For per-department reports, use DENSE_RANK instead.</p>



<h4 class="wp-block-heading">What is the difference between RANK and DENSE_RANK for salary queries?</h4>



<p class="wp-block-paragraph">RANK leaves gaps after ties, so a rank of 2 may not exist. DENSE_RANK has no gaps, so rank 2 always means the second distinct salary. Use DENSE_RANK for Nth highest queries.</p>



<h4 class="wp-block-heading">How do I find the Nth highest salary in SQL Server?</h4>



<p class="wp-block-paragraph">Use DENSE_RANK in a CTE and filter on the rank you want, or use OFFSET with FETCH on distinct salaries. Wrap it in a stored procedure with a parameter and validate that the value is at least 1.</p>



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



<p class="wp-block-paragraph">Filter them out with <code>WHERE Salary IS NOT NULL</code> before ranking or sorting. MAX ignores NULLs on its own, but ORDER BY does not.</p>



<h4 class="wp-block-heading">How do I speed up slow SQL Server queries like this one?</h4>



<p class="wp-block-paragraph">Read the actual execution plan, add an index that matches your partition and sort columns, and check Query Store for regressions. Measure logical reads before and after each change.</p>



<p class="wp-block-paragraph">You learned five ways to find the second highest salary in SQL Server, why DENSE_RANK is the safest choice for ties, and how to secure and index the query. Always define tie and NULL rules first, then test with a restricted account on production-sized data before you trust the result. 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-nth-highest-salary-in-sql-server/" target="_blank" rel="noreferrer noopener">How to Find Nth Highest Salary in SQL Server</a></li>



<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-subquery-vs-join/" target="_blank" rel="noopener">SQL subquery vs. join</a></li>



<li><a href="https://sqlserverguides.com/sql-min-max/" target="_blank" rel="noopener">SQL MIN and MAX functions</a></li>



<li><a href="https://sqlserverguides.com/sql-where-clause-tutorial/" target="_blank" rel="noopener">SQL WHERE clause tutorial</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/how-to-find-the-lowest-salary-in-sql-server/" target="_blank" rel="noreferrer noopener">How to Find the Lowest Salary in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/how-to-find-3rd-highest-salary-in-sql-server/" target="_blank" rel="noreferrer noopener">How to Find 3rd Highest Salary in SQL Server</a></li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Scalar Function vs Table Valued Function SQL Server</title>
		<link>https://sqlserverguides.com/scalar-function-vs-table-valued-function-sql-server/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Thu, 08 Oct 2026 05:45:17 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Scalar Function vs Table Valued Function SQL Server]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=23843</guid>

					<description><![CDATA[A retail client once called me because their order report had gone from 8 seconds to 14 minutes. Nothing had changed except one thing. A developer had wrapped a tax calculation in a user-defined function and called it for every row in a 3-million-row table. The query looked clean, but it ran one function call ... <a title="Scalar Function vs Table Valued Function SQL Server" class="read-more" href="https://sqlserverguides.com/scalar-function-vs-table-valued-function-sql-server/" aria-label="Read more about Scalar Function vs Table Valued Function SQL Server">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A retail client once called me because their order report had gone from 8 seconds to 14 minutes. Nothing had changed except one thing. A developer had wrapped a tax calculation in a user-defined function and called it for every row in a 3-million-row table. The query looked clean, but it ran one function call per row, and the optimizer could not see inside it.</p>



<p class="wp-block-paragraph">This is the core of the <strong>scalar function vs table valued function SQL Server</strong> question. Both are reusable T-SQL objects, but they behave very differently inside an execution plan. Picking the wrong one can quietly cost you CPU, and on Azure SQL Database that means a bigger compute tier and a bigger bill. I have fixed this pattern at several companies.</p>



<p class="wp-block-paragraph">By the end, you will know how each function type works, how to write and test both, how to convert a slow scalar function into a fast one, and how to secure and monitor them.</p>



<h2 class="wp-block-heading">Scalar Function vs Table Valued Function SQL Server</h2>



<h3 class="wp-block-heading">What Is a User-Defined Function?</h3>



<p class="wp-block-paragraph">A <strong>user-defined function (UDF)</strong> is a saved T-SQL routine that accepts parameters and returns a result. Unlike a stored procedure, you call a function inside a query, such as in a SELECT list, a WHERE clause, or a JOIN. If you want the broader comparison first, read this guide on <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-functions-vs-stored-procedures/">SQL Server functions vs. stored procedures</a>.</p>



<p class="wp-block-paragraph">SQL Server supports three main kinds:</p>



<ul class="wp-block-list">
<li><strong>Scalar functions</strong> return a single value, such as a number, date, or string.</li>



<li><strong>Inline table-valued functions (iTVF)</strong> return a table from a single SELECT statement.</li>



<li><strong>Multi-statement table-valued functions (MSTVF)</strong> return a table built by several statements inside a function body.</li>
</ul>



<p class="wp-block-paragraph">Functions have rules. They cannot change data in permanent tables, and they cannot use TRY&#8230;CATCH or dynamic SQL. These limits keep them predictable inside queries, but they also limit what you can do.</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 decide the function type by asking one question first: does the caller need one value or a set of rows? That answer removes half of the confusion before I write any code.</p>
</blockquote>



<h3 class="wp-block-heading">Scalar Functions Explained</h3>



<p class="wp-block-paragraph">A <strong>scalar function</strong> takes input parameters and returns exactly one value. It is the right shape for small, repeatable calculations like formatting a phone number or computing a discount. If you need a refresher on syntax, this page on <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-create-function-in-sql-server/">how to create a function in SQL Server</a> covers the basics, and the <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-scalar-functions/">SQL scalar functions tutorial</a> goes deeper.</p>



<h4 class="wp-block-heading">Creating a scalar function</h4>



<p class="wp-block-paragraph">Here is a realistic example from an e-commerce database. It calculates sales tax for an order line.</p>



<pre class="wp-block-preformatted"><code>CREATE SCHEMA fn;<br>GO<br><br>CREATE FUNCTION fn.CalcLineTax<br>(<br>    @LineTotal DECIMAL(18,2),<br>    @TaxRate   DECIMAL(5,4)<br>)<br>RETURNS DECIMAL(18,2)<br>AS<br>BEGIN<br>    RETURN ROUND(@LineTotal * @TaxRate, 2);<br>END;<br>GO</code></pre>



<p class="wp-block-paragraph">After executing the above query, the scalar function has been created successfully, as shown in the screenshot below.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="922" height="653" src="https://sqlserverguides.com/wp-content/uploads/2026/10/sql-table-valued-function-vs-scalar.jpg" alt="sql table valued function vs scalar" class="wp-image-23844" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/sql-table-valued-function-vs-scalar.jpg 922w, https://sqlserverguides.com/wp-content/uploads/2026/10/sql-table-valued-function-vs-scalar-300x212.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/sql-table-valued-function-vs-scalar-767x543.jpg 767w" sizes="auto, (max-width: 922px) 100vw, 922px" /></figure>
</div>


<p class="wp-block-paragraph">The <code>CREATE SCHEMA</code> statement makes a <strong>schema</strong>, which is a named container that groups database objects and helps with permissions. Putting functions in a separate <code>fn</code> schema lets you grant access to them as a group. The function takes two parameters, multiplies them, rounds the result, and returns a single <code>DECIMAL</code>.</p>



<p class="wp-block-paragraph">Call it inside a query like this:</p>



<pre class="wp-block-preformatted"><code>SELECT OrderLineId,<br>       LineTotal,<br>       fn.CalcLineTax(LineTotal, 0.0825) AS TaxAmount<br>FROM   sales.OrderLines<br>WHERE  OrderId = 10542;</code></pre>



<p class="wp-block-paragraph">This returns one tax value per order line. For a small filtered set, it performs fine. For more on calling patterns, see how to <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-execute-function-in-sql-server-with-parameters/">execute a function in SQL Server with parameters</a>.</p>



<h4 class="wp-block-heading">Why scalar functions can be slow</h4>



<p class="wp-block-paragraph">Historically, SQL Server executed a scalar function once per row, like a hidden loop. The optimizer treated the function as a black box, so it could not estimate its cost, and it often forced the whole query to run on a single CPU thread (serial execution). On large tables, that is painful.</p>



<p class="wp-block-paragraph">SQL Server 2019 introduced <strong>scalar UDF inlining</strong>, which rewrites eligible functions into the main query as an expression. This helps a lot, but it has conditions. The function must avoid features such as certain time-dependent functions, table variables, and some side effects. Inlining also depends on the database compatibility level (150 or higher), and Azure SQL Database supports it. Check eligibility on your version rather than assuming.</p>



<pre class="wp-block-preformatted"><code>SELECT name, is_inlineable<br>FROM   sys.sql_modules AS m<br>JOIN   sys.objects AS o ON o.object_id = m.object_id<br>WHERE  o.type = 'FN';</code></pre>



<p class="wp-block-paragraph">This query reads the <code>is_inlineable</code> column from the catalog view <code>sys.sql_modules</code>. A value of 1 means SQL Server can inline that function. A value of 0 means it will still run row by row.</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 found a scalar function marked as not inlineable only because it called GETDATE(). Passing the date in as a parameter fixed it and cut the query from minutes to seconds.</p>
</blockquote>



<h3 class="wp-block-heading">Table-Valued Functions Explained</h3>



<p class="wp-block-paragraph">A <strong>table-valued function (TVF)</strong> returns a result set that you query like a table. You can join it, filter it, and aggregate it. Read the <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-table-valued-function/">SQL Server table-valued function guide</a> for the full syntax.</p>



<h4 class="wp-block-heading">Inline table-valued functions</h4>



<p class="wp-block-paragraph">An inline TVF contains one SELECT statement. SQL Server expands it into the calling query, much like a view that accepts parameters. This gives the optimizer full visibility, so it can use indexes, estimate row counts, and run in parallel.</p>



<pre class="wp-block-preformatted"><code>CREATE FUNCTION sales.GetCustomerOrders<br>(<br>    @CustomerId INT,<br>    @FromDate   DATE<br>)<br>RETURNS TABLE<br>AS<br>RETURN<br>(<br>    SELECT o.OrderId,<br>           o.OrderDate,<br>           o.TotalAmount,<br>           o.Status<br>    FROM   sales.Orders AS o<br>    WHERE  o.CustomerId = @CustomerId<br>      AND  o.OrderDate >= @FromDate<br>);<br>GO</code></pre>



<p class="wp-block-paragraph">This function returns all orders for one customer since a given date. Notice there is no BEGIN&#8230;END block and no table definition, because the structure comes from the SELECT itself. It works like a <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-views/">view</a> with parameters, which a normal view cannot offer.</p>



<h4 class="wp-block-heading">Multi-statement table-valued functions</h4>



<p class="wp-block-paragraph">A multi-statement TVF declares a return table, fills it with several statements, and returns it. It is flexible, but it has a cost. The optimizer historically assumed a fixed low row estimate for the returned table, which led to poor join choices. Newer versions improved this with interleaved execution, but it is not a cure-all. Review the <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-multi-statement-table-function/">multi-statement table function guide</a> before you use one.</p>



<pre class="wp-block-preformatted"><code>CREATE FUNCTION sales.GetOrderSummary (@CustomerId INT)<br>RETURNS @Result TABLE<br>(<br>    OrderId INT,<br>    TotalAmount DECIMAL(18,2),<br>    OrderRank INT<br>)<br>AS<br>BEGIN<br>    INSERT INTO @Result (OrderId, TotalAmount)<br>    SELECT OrderId, TotalAmount<br>    FROM   sales.Orders<br>    WHERE  CustomerId = @CustomerId;<br><br>    UPDATE r<br>    SET    OrderRank = x.rnk<br>    FROM   @Result AS r<br>    JOIN   (SELECT OrderId,<br>                   RANK() OVER (ORDER BY TotalAmount DESC) AS rnk<br>            FROM   @Result) AS x<br>      ON   x.OrderId = r.OrderId;<br><br>    RETURN;<br>END;<br>GO</code></pre>



<p class="wp-block-paragraph">This function builds a table variable named <code>@Result</code>, loads it, then updates it with a rank. A <strong>table variable</strong> is a temporary in-memory-style structure scoped to the batch or function. Here, the same result could be written as one SELECT with a <a rel="noopener" target="_blank" href="https://sqlserverguides.com/window-functions-in-sql-server/">window function</a>, which would convert it to an inline TVF and make it faster.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> Whenever I see an MSTVF, I try to rewrite it as an inline TVF first. About 80 percent of the time, the logic fits in one SELECT, and the execution plan improves immediately.</p>
</blockquote>



<h3 class="wp-block-heading">Scalar Function vs Table-Valued Function: Key Differences</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Feature</th><th>Scalar function</th><th>Inline TVF</th><th>Multi-statement TVF</th></tr></thead><tbody><tr><td>Returns</td><td>One value</td><td>A table</td><td>A table</td></tr><tr><td>Used in</td><td>SELECT list, WHERE, computed columns</td><td>FROM clause, JOIN, APPLY</td><td>FROM clause, JOIN, APPLY</td></tr><tr><td>Optimizer visibility</td><td>Limited unless inlined</td><td>Full, expanded into the query</td><td>Limited, estimated rows</td></tr><tr><td>Typical performance on big sets</td><td>Risky per-row calls</td><td>Strong</td><td>Moderate to weak</td></tr><tr><td>Best use</td><td>Small formatting or math helpers</td><td>Parameterized reusable queries</td><td>Complex logic that cannot be one SELECT</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The main trade-off is flexibility versus optimizer visibility. A scalar function is simple to read, but it hides cost. An inline TVF is almost as readable and exposes everything to the optimizer. A multi-statement TVF sits in the middle with extra power and extra risk.</p>



<h3 class="wp-block-heading">How to Choose the Right Function Type</h3>



<p class="wp-block-paragraph">Use a simple decision path in code reviews. Start with what the caller needs, then check performance.</p>



<ol class="wp-block-list">
<li>If you need one value from simple math or string logic on a small row set, use a scalar function, and confirm it is inlineable.</li>



<li>If you need rows, use an inline TVF first.</li>



<li>If the logic needs multiple steps, try a <a href="https://sqlserverguides.com/sql-server-cte-vs-temp-table/" target="_blank" rel="noopener">CTE</a> or window functions before accepting an MSTVF.</li>



<li>If the logic changes data, use a stored procedure instead. You can compare that choice in this article on <a href="https://sqlserverguides.com/stored-procedure-vs-view/" target="_blank" rel="noopener">stored procedures vs. views</a>.</li>
</ol>



<p class="wp-block-paragraph">Also consider the data size. A function that is fine on 5,000 rows can crush a reporting query on 50 million rows. Always test at production-like volume, not on a developer&#8217;s tiny copy.</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 keep a staging database restored from last night&#8217;s backup so I can test functions against real row counts. Small test data hides almost every function performance bug.</p>
</blockquote>



<h3 class="wp-block-heading">Converting a Slow Scalar Function to an Inline TVF</h3>



<p class="wp-block-paragraph">This is the fix I use most often. Suppose a scalar function returns the latest order date for a customer.</p>



<pre class="wp-block-preformatted"><code>CREATE FUNCTION fn.LatestOrderDate (@CustomerId INT)<br>RETURNS DATE<br>AS<br>BEGIN<br>    DECLARE @d DATE;<br>    SELECT @d = MAX(OrderDate)<br>    FROM   sales.Orders<br>    WHERE  CustomerId = @CustomerId;<br>    RETURN @d;<br>END;<br>GO<br><br>SELECT c.CustomerId, fn.LatestOrderDate(c.CustomerId) AS LastOrder<br>FROM   sales.Customers AS c;</code></pre>



<p class="wp-block-paragraph">This runs one lookup per customer, which becomes a hidden loop. Here is the inline TVF version, called with <strong>CROSS APPLY</strong>:</p>



<pre class="wp-block-preformatted"><code>CREATE FUNCTION sales.LatestOrderDateTvf (@CustomerId INT)<br>RETURNS TABLE<br>AS<br>RETURN<br>(<br>    SELECT MAX(OrderDate) AS LastOrder<br>    FROM   sales.Orders<br>    WHERE  CustomerId = @CustomerId<br>);<br>GO<br><br>SELECT c.CustomerId, x.LastOrder<br>FROM   sales.Customers AS c<br>OUTER APPLY sales.LatestOrderDateTvf(c.CustomerId) AS x;</code></pre>



<p class="wp-block-paragraph"><code>APPLY</code> runs the function against each row from the left side, but the optimizer expands the logic into the main plan, so it can pick a join strategy. Use <code>OUTER APPLY</code> to keep customers with no orders, similar to a left join. Understanding <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-inner-join-tutorial/">join types</a> helps you choose between APPLY and a plain join.</p>



<p class="wp-block-paragraph">For this pattern, a plain <code>GROUP BY</code> join may be even faster. Compare plans. The best result depends on your data, so measure both.</p>



<h3 class="wp-block-heading">Check the execution plan and indexes</h3>



<p class="wp-block-paragraph">Turn on the actual execution plan in SSMS and look for these signs of trouble:</p>



<ul class="wp-block-list">
<li>A Compute Scalar operator calling a function with a very high estimated subtree cost.</li>



<li>A serial plan where you expected parallelism.</li>



<li>Scans on large tables inside the function.</li>
</ul>



<p class="wp-block-paragraph">Then support the function with the right index. For the example above, an index on <code>CustomerId</code> that includes <code>OrderDate</code> removes the scan. Follow these <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-indexes-best-practices/">SQL index best practices</a> and learn <a rel="noopener" target="_blank" href="https://sqlserverguides.com/how-to-create-index-in-sql-server/">how to create an index</a> properly.</p>



<pre class="wp-block-preformatted"><code>CREATE NONCLUSTERED INDEX IX_Orders_CustomerId_OrderDate<br>ON sales.Orders (CustomerId, OrderDate)<br>INCLUDE (TotalAmount, Status);</code></pre>



<p class="wp-block-paragraph">This <strong>nonclustered index</strong> stores a sorted copy of selected columns, so lookups by customer skip a full table scan. Remember the trade-off. Every added index slows INSERT and UPDATE operations a bit, so add only what the plans justify.</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 add an index just because a function is slow. I check the plan first, because sometimes the real fix is passing a better parameter or removing the function entirely.</p>
</blockquote>



<h3 class="wp-block-heading">Security, Permissions, and Governance</h3>



<p class="wp-block-paragraph">Functions are code, and code needs access control. Apply <strong>least privilege</strong>, which means giving each account only the rights it needs. Application logins should not be <code>sysadmin</code> or <code>db_owner</code>. A secure setup uses an Entra ID identity or a contained user, and the permissions below.</p>



<pre class="wp-block-preformatted"><code>CREATE USER app_orders_api FROM EXTERNAL PROVIDER;<br><br>GRANT EXECUTE ON SCHEMA::fn TO app_orders_api;<br>GRANT SELECT  ON OBJECT::sales.GetCustomerOrders TO app_orders_api;</code></pre>



<p class="wp-block-paragraph"><code>CREATE USER ... FROM EXTERNAL PROVIDER</code> maps a <strong>Microsoft Entra ID</strong> identity, such as a managed identity, to a database user in Azure SQL Database. This avoids storing passwords in a config file. <code>GRANT EXECUTE</code> lets the user call scalar functions in the <code>fn</code> schema, and <code>GRANT SELECT</code> lets the user query the inline TVF, since table-valued functions use SELECT permission. To audit access later, review <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-permissions/">SQL Server permissions</a> and <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-check-user-permissions-on-table/">how to check user permissions on a table</a>.</p>



<p class="wp-block-paragraph">Test with a non-administrator account. Connect as the app user and confirm that the function works and that unrelated tables stay blocked. For stored procedures, the same approach applies, as shown in this guide on <a rel="noopener" target="_blank" href="https://sqlserverguides.com/give-permission-to-execute-stored-procedure-in-sql-server/">giving permission to execute a stored procedure</a>.</p>



<p class="wp-block-paragraph">For Azure SQL, also require TLS encrypted connections, prefer private endpoints over public firewall rules, and never open a firewall range from 0.0.0.0 to 255.255.255.255. Enable auditing and Microsoft Defender for SQL so that unusual access to sensitive data raises an alert.</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 found a scalar function reading a salary table that its callers had no direct rights to, because ownership chaining allowed it. I now review function bodies during every access audit.</p>
</blockquote>



<h3 class="wp-block-heading">Monitoring Function Performance and Cost</h3>



<p class="wp-block-paragraph">You cannot tune what you do not measure. Use these tools:</p>



<ul class="wp-block-list">
<li><strong>Query Store</strong>, a built-in feature that records query plans and runtime stats over time. It lets you compare a query before and after a change.</li>



<li><strong>Query Performance Insight</strong> in Azure SQL Database, which shows top CPU consumers.</li>



<li><strong>Dynamic management views (DMVs)</strong>, such as <code>sys.dm_exec_function_stats</code>, which report execution counts and CPU per function.</li>
</ul>



<pre class="wp-block-preformatted"><code>SELECT OBJECT_NAME(object_id) AS FunctionName,<br>       execution_count,<br>       total_worker_time / 1000 AS TotalCpuMs,<br>       total_elapsed_time / 1000 AS TotalElapsedMs<br>FROM   sys.dm_exec_function_stats<br>ORDER BY total_worker_time DESC;</code></pre>



<p class="wp-block-paragraph">This lists functions ranked by total CPU time. Note that DMV data resets after a restart or failover, so use Query Store for long-term trends. Function stats collection can also depend on your version and settings, so confirm availability in your environment.</p>



<p class="wp-block-paragraph">Tie this to cost. In Azure SQL Database, high CPU pushes you toward a larger vCore or DTU tier. A <strong>vCore</strong> model sells compute by virtual cores, and <strong>DTU</strong> bundles CPU, memory, and I/O into one unit. Fixing one hot function can let you scale down a tier. Set budgets and cost alerts in Azure Cost Management, tag your databases by environment, and review Azure Advisor recommendations monthly.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong> After tuning one function, I screenshot nothing and instead record the Query Store CPU numbers before and after in the change ticket. Finance loves that proof of savings.</p>
</blockquote>



<h3 class="wp-block-heading">Safe Deployment and Rollback</h3>



<p class="wp-block-paragraph">Never edit functions directly in production. Keep them in source control and deploy through a pipeline. Always take a backup or confirm point-in-time restore is available before releasing schema changes. Dropping or altering a function can break views, computed columns, and stored procedures that depend on it.</p>



<p class="wp-block-paragraph">Use <code>CREATE OR ALTER FUNCTION</code> for repeatable deployments, which creates the function if missing or updates it in place. This keeps the permissions you granted earlier. Warn your team that <code>DROP FUNCTION</code> followed by <code>CREATE</code> removes those grants.</p>



<p class="wp-block-paragraph">Also watch for functions used in computed columns or CHECK constraints. A slow function there runs on every insert and update, which hurts write throughput. Test deployments in a staging copy first, then in production during a low-traffic window.</p>



<h3 class="wp-block-heading">Performance and Production Readiness Checklist</h3>



<ul class="wp-block-list">
<li><strong>Prefer inline logic:</strong> Choose inline TVFs over multi-statement versions, and check <code>is_inlineable</code> for scalar functions before going live.</li>



<li><strong>Test at real volume:</strong> Run functions against production-sized data and read the actual execution plan, not only the estimated one.</li>



<li><strong>Use least-privilege access:</strong> Grant EXECUTE or SELECT on a schema to a Microsoft Entra ID user instead of using <code>db_owner</code> or SQL passwords.</li>



<li><strong>Avoid functions in WHERE clauses:</strong> A function wrapped around an indexed column stops index seeks and forces scans.</li>



<li><strong>Plan recovery:</strong> Confirm backups and point-in-time restore before deployment, and test a restore, not just the schedule.</li>



<li><strong>Watch cost:</strong> Track CPU in Query Store, set budgets, and tag resources so tuning wins show up in Azure cost reports.</li>
</ul>



<h3 class="wp-block-heading">Common Errors and Troubleshooting</h3>



<p class="wp-block-paragraph">Here are problems I see most often.</p>



<p class="wp-block-paragraph"><strong>Slow query after adding a function.</strong> Open the actual plan and look for a serial plan. Replace the scalar function with an inline TVF or an inline expression.</p>



<p class="wp-block-paragraph"><strong>&#8220;Invalid use of a side-effecting operator.&#8221;</strong> Functions cannot modify permanent tables or use certain statements. Move that logic to a stored procedure, and follow these <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-stored-procedure-best-practices/">stored procedure best practices</a>.</p>



<p class="wp-block-paragraph"><strong>Wrong row estimates from an MSTVF.</strong> Rewrite it as an inline TVF, or load the data into a temp table first. This article on <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-server-temp-table-vs-table-variable/">temp tables vs. table variables</a> explains why the estimates differ.</p>



<p class="wp-block-paragraph"><strong>Row-by-row logic inside a function.</strong> Avoid WHILE loops in functions. Replace them with set-based queries, and see <a rel="noopener" target="_blank" href="https://sqlserverguides.com/while-loop-in-sql-server-stored-procedure/">WHILE loops in stored procedures</a> only for cases that truly need iteration.</p>



<p class="wp-block-paragraph"><strong>Blocking from long-running functions in transactions.</strong> A slow function inside a transaction holds locks longer. Keep transactions short and review <a rel="noopener" target="_blank" href="https://sqlserverguides.com/sql-deadlock-victim/">deadlock victims</a> when you see error 1205.</p>



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



<h4 class="wp-block-heading">What is the difference between a scalar function and a table-valued function?</h4>



<p class="wp-block-paragraph">A scalar function returns one value, while a table-valued function returns a table you can query. Scalar functions fit small calculations. Table-valued functions fit reusable, parameterized queries.</p>



<h4 class="wp-block-heading">Which is faster, a scalar function or a table-valued function?</h4>



<p class="wp-block-paragraph">An inline table-valued function is usually faster on large data sets because the optimizer expands it into the main query. Scalar functions can run once per row unless SQL Server 2019 or later inlines them. Always compare actual execution plans.</p>



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



<p class="wp-block-paragraph">Yes, but it can hurt performance. Wrapping an indexed column in a function usually prevents an index seek and causes a scan. Apply the function to the parameter instead of the column when you can.</p>



<h4 class="wp-block-heading">Can a function insert or update data?</h4>



<p class="wp-block-paragraph">No. Functions cannot change data in permanent tables. Use a stored procedure for INSERT, UPDATE, or DELETE work.</p>



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



<p class="wp-block-paragraph">Use Query Store to compare query runtime and CPU over time. You can also query <code>sys.dm_exec_function_stats</code> to rank functions by CPU. In Azure SQL Database, Query Performance Insight helps too.</p>



<p class="wp-block-paragraph">You learned how scalar, inline table-valued, and multi-statement table-valued functions differ, and how to convert slow row-by-row logic into set-based code. Measure every function in an actual execution plan at production-sized volume, because that one habit prevents most function-related slowdowns and cloud cost surprises. 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/temp-table-in-sql-server/" target="_blank" rel="noopener">SQL Server temp tables explained</a></li>



<li><a href="https://sqlserverguides.com/how-to-create-clustered-index-in-sql-server/" target="_blank" rel="noopener">How to create a clustered index in SQL Server</a></li>



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



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



<li><a href="https://sqlserverguides.com/trigger-sql-server/" target="_blank" rel="noopener">SQL Server triggers explained</a></li>
</ul>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to Reset SQL Server sa Password Without Login</title>
		<link>https://sqlserverguides.com/how-to-reset-sql-server-sa-password-without-login/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Tue, 06 Oct 2026 05:45:19 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[How to Reset SQL Server sa Password Without Login]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=23840</guid>

					<description><![CDATA[A manufacturing client called me on a Friday afternoon. Their only database administrator had left the company two weeks earlier, and nobody knew the&#160;sa&#160;password. The ERP system still ran, but a vendor needed to patch it that night. Every attempt to sign in with the old credentials ended in the same login failure. The good ... <a title="How to Reset SQL Server sa Password Without Login" class="read-more" href="https://sqlserverguides.com/how-to-reset-sql-server-sa-password-without-login/" aria-label="Read more about How to Reset SQL Server sa Password Without Login">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A manufacturing client called me on a Friday afternoon. Their only database administrator had left the company two weeks earlier, and nobody knew the&nbsp;<code>sa</code>&nbsp;password. The ERP system still ran, but a vendor needed to patch it that night. Every attempt to sign in with the old credentials ended in the same login failure.</p>



<p class="wp-block-paragraph">The good news was that the company owned the server. This is the situation where you need to know&nbsp;<strong>how to reset SQL Server sa password without login</strong>. If you have administrator rights on the Windows or Linux host, SQL Server gives you a supported recovery path. You do not need to reinstall anything or touch the data files.</p>



<p class="wp-block-paragraph">This guide shows you the exact recovery steps for SQL Server on Windows and Linux, plus what to do on Azure VMs, Azure SQL Database, and Managed Instance. You will also learn how to store the new password safely, lock down the&nbsp;<code>sa</code>&nbsp;account, and keep this from happening again.</p>



<h2 class="wp-block-heading">Understand What&nbsp;<code>sa</code>&nbsp;Is and Why You Are Locked Out</h2>



<p class="wp-block-paragraph">The <code>sa</code> login is the built-in system administrator account in SQL Server. It is a member of the <strong>sysadmin</strong> fixed server role, which means it can do anything on the instance.</p>



<p class="wp-block-paragraph">It only works if the server runs in <strong>mixed mode authentication</strong>, which allows both Windows and SQL logins. In Windows-only mode, <code>sa</code> is disabled by default even when the password is correct.</p>



<p class="wp-block-paragraph">You can be locked out for several reasons:</p>



<ul class="wp-block-list">
<li>Nobody recorded the password at install time.</li>



<li>The account was disabled, and no other sysadmin exists.</li>



<li>The password was changed, and the person who changed it left.</li>



<li>Mixed mode is off, so SQL logins are rejected.</li>



<li>A password policy lockout is blocking the account after repeated failures.</li>
</ul>



<p class="wp-block-paragraph">Check the error before you change anything. A login failure with state details usually names the cause. See the guide on&nbsp;<a rel="noopener" target="_blank" href="https://www.sqlserverguides.com/sql-server-error-18456/">SQL Server error 18456</a>&nbsp;to read the state codes. If you cannot even connect, review why&nbsp;<a rel="noopener" target="_blank" href="https://www.sqlserverguides.com/ssms-cannot-connect-to-server/">SSMS cannot connect to the server</a>&nbsp;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>&nbsp;In my experience, about half of &#8220;forgotten sa password&#8221; calls turn out to be a Windows account that is already a sysadmin. Before you restart anything, try signing in with every Windows admin account that was used to install or manage the server.</p>
</blockquote>



<h3 class="wp-block-heading">Authorization and Security First</h3>



<p class="wp-block-paragraph">This procedure gives whoever runs it full control of the instance. Only use it on servers you own or are authorized to administer. Before you start:</p>



<ul class="wp-block-list">
<li>Get written approval from the data owner or system owner.</li>



<li>Note the date, time, and reason in your change record.</li>



<li>Confirm that you have local administrator rights on the host, because the recovery method depends on them.</li>



<li>Schedule a maintenance window. The method restarts the SQL Server service, so applications will lose their connections.</li>
</ul>



<p class="wp-block-paragraph">Also take a backup of your system databases if you can. See the overview of&nbsp;<a rel="noopener" target="_blank" href="https://databasefaqs.com/types-of-backup-in-sql-server/">types of backup in SQL Server</a>&nbsp;to choose the right kind. If SQL Server is hosted on an Azure VM, consider a VM-level backup or snapshot first. The guide to&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/azure-vm-backup-restore/">Azure VM backup and restore</a>&nbsp;explains how.</p>



<p class="wp-block-paragraph">If someone else controls the host and you are not an administrator, you cannot use this method. Ask the infrastructure owner. That limit is a security feature, not a bug.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong>&nbsp;I always write the approval and the exact time window into the ticket before I touch the service. If an auditor asks six months later who regained sysadmin access and why, the answer is one click away.</p>
</blockquote>



<h3 class="wp-block-heading">Method 1: Reset&nbsp;<code>sa</code>&nbsp;in Single-User Mode (Windows)</h3>



<p class="wp-block-paragraph">When SQL Server starts in&nbsp;<strong>single-user mode</strong>, only one connection is allowed, and members of the local Windows Administrators group can connect with full sysadmin rights. This is the standard supported recovery path.</p>



<h4 class="wp-block-heading">Step 1: Find the Instance Name</h4>



<p class="wp-block-paragraph">You need the service name. For a default instance, it is&nbsp;<code>MSSQLSERVER</code>. For a named instance, it is&nbsp;<code>MSSQL$&lt;InstanceName&gt;</code>. If you are unsure, see how to&nbsp;<a rel="noopener" target="_blank" href="https://www.sqlserverguides.com/how-to-find-sql-server-instance-name-in-ssms/">find the SQL Server instance name</a>, or list the services on the host:</p>



<pre class="wp-block-preformatted"><code>Get-Service | Where-Object { $_.Name -like 'MSSQL*' } | Select-Object Name, Status</code></pre>



<p class="wp-block-paragraph">This lists SQL Server services and their status. The one named&nbsp;<code>MSSQLSERVER</code>&nbsp;or&nbsp;<code>MSSQL$&lt;InstanceName&gt;</code>&nbsp;is the database engine. You can also confirm it is running with this guide to&nbsp;<a rel="noopener" target="_blank" href="https://databasefaqs.com/how-to-check-if-sql-server-is-running/">check if SQL Server is running</a>.</p>



<h4 class="wp-block-heading">Step 2: Stop the Service and Dependent Services</h4>



<p class="wp-block-paragraph">Open&nbsp;<strong>PowerShell as Administrator</strong>. Stop the SQL Server Agent first, because it will otherwise grab the one allowed connection:</p>



<pre class="wp-block-preformatted"><code>Stop-Service -Name 'SQLSERVERAGENT' -Force<br>Stop-Service -Name 'MSSQLSERVER' -Force</code></pre>



<p class="wp-block-paragraph">Replace the names with&nbsp;<code>SQLAgent$&lt;InstanceName&gt;</code>&nbsp;and&nbsp;<code>MSSQL$&lt;InstanceName&gt;</code>&nbsp;for a named instance.&nbsp;<strong>Warning:</strong>&nbsp;stopping the service disconnects every user and application, so run this only in your approved window.</p>



<h4 class="wp-block-heading">Step 3: Start SQL Server in Single-User Mode</h4>



<pre class="wp-block-preformatted"><code>net start MSSQLSERVER /mSQLCMD</code></pre>



<p class="wp-block-paragraph">The&nbsp;<code>/mSQLCMD</code>&nbsp;startup option starts the instance in single-user mode and limits the connection to the&nbsp;<code>sqlcmd</code>&nbsp;utility. This keeps other programs, such as the Agent or a monitoring tool, from taking the only connection. For a named instance, use&nbsp;<code>net start MSSQL$&lt;InstanceName&gt; /mSQLCMD</code>.</p>



<p class="wp-block-paragraph">If the service fails to start, check the error log and see&nbsp;<a rel="noopener" target="_blank" href="https://www.sqlserverguides.com/sql-server-service-not-starting/">SQL Server service not starting</a>&nbsp;and the&nbsp;<a rel="noopener" target="_blank" href="https://www.sqlserverguides.com/sql-error-1067/">error 1067 guide</a>.</p>



<h4 class="wp-block-heading">Step 4: Connect with sqlcmd Using Windows Authentication</h4>



<p class="wp-block-paragraph">Open a second&nbsp;<strong>Command Prompt or PowerShell as Administrator</strong>, and run this:</p>



<pre class="wp-block-preformatted"><code>sqlcmd -S . -E</code></pre>



<p class="wp-block-paragraph">The&nbsp;<code>-S .</code>&nbsp;flag means the local default instance. For a named instance, use&nbsp;<code>-S .\&lt;InstanceName&gt;</code>. The&nbsp;<code>-E</code>&nbsp;flag uses your Windows credentials. Because you are a local administrator and the instance is in single-user mode, you connect as a sysadmin. If you see a &#8220;login failed&#8221; message, another process has taken the single connection, so stop it and try again.</p>



<h4 class="wp-block-heading">Step 5: Reset the Password and Re-enable&nbsp;<code>sa</code></h4>



<p class="wp-block-paragraph">Generate a strong password first. Do not invent a simple one under pressure. Use PowerShell to create a random value:</p>



<pre class="wp-block-preformatted"><code>$NewPassword = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 28 | ForEach-Object { [char]$_ })</code></pre>



<p class="wp-block-paragraph">This builds a 28-character password from letters and digits. Letters and digits avoid quoting problems in T-SQL. Now pass it to sqlcmd as a variable so it never appears in a script file:</p>



<pre class="wp-block-preformatted"><code>sqlcmd -S . -E -v NewPwd="$NewPassword" -Q 'ALTER LOGIN sa ENABLE; ALTER LOGIN sa WITH PASSWORD = N''$(NewPwd)'', CHECK_POLICY = ON; SELECT name, is_disabled FROM sys.server_principals WHERE name = N''sa'';'</code></pre>



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



<ul class="wp-block-list">
<li><code>-v NewPwd="$NewPassword"</code> defines a scripting variable that sqlcmd substitutes into the query.</li>



<li><code>ALTER LOGIN sa ENABLE</code> turns the account on if it was disabled.</li>



<li><code>ALTER LOGIN sa WITH PASSWORD = ...</code> sets the new password.</li>



<li><code>CHECK_POLICY = ON</code> enforces Windows password complexity and lockout rules.</li>



<li>The final <code>SELECT</code> confirms that <code>sa</code> exists and shows <code>is_disabled = 0</code>.</li>
</ul>



<p class="wp-block-paragraph">You can also run the same T-SQL inside an interactive sqlcmd session. Type the statements, then&nbsp;<code>GO</code>:</p>



<pre class="wp-block-preformatted"><code>ALTER LOGIN sa ENABLE;<br>GO<br>ALTER LOGIN sa WITH PASSWORD = N'&lt;new-strong-password>' , CHECK_POLICY = ON;<br>GO</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="925" height="342" src="https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-reset-sql-server-sa-password-without-login.jpg" alt="how to reset sql server sa password without login" class="wp-image-23841" srcset="https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-reset-sql-server-sa-password-without-login.jpg 925w, https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-reset-sql-server-sa-password-without-login-300x111.jpg 300w, https://sqlserverguides.com/wp-content/uploads/2026/10/how-to-reset-sql-server-sa-password-without-login-765x283.jpg 765w" sizes="auto, (max-width: 925px) 100vw, 925px" /></figure>
</div>


<p class="wp-block-paragraph">Use the interactive form only if you can avoid leaving the password in your terminal history. Clear the history afterward.</p>



<h4 class="wp-block-heading">Step 6: Add a Named Admin (Better Than Using&nbsp;<code>sa</code>)</h4>



<p class="wp-block-paragraph">While you have sysadmin rights, create or fix a proper administrator account so nobody depends on&nbsp;<code>sa</code>&nbsp;again.</p>



<pre class="wp-block-preformatted"><code>CREATE LOGIN [DOMAIN\sql-admins] FROM WINDOWS;<br>GO<br>ALTER SERVER ROLE sysadmin ADD MEMBER [DOMAIN\sql-admins];<br>GO</code></pre>



<p class="wp-block-paragraph">This creates a login for an Active Directory group and adds it to the sysadmin role. Use a group so access follows team membership. For more on creating logins, see the guide on&nbsp;<a rel="noopener" target="_blank" href="https://www.sqlserverguides.com/sql-create-login/">creating a SQL login</a>&nbsp;and how&nbsp;<a rel="noopener" target="_blank" href="https://www.sqlserverguides.com/sql-server-permissions/">SQL Server permissions</a>&nbsp;work.</p>



<h4 class="wp-block-heading">Step 7: Return to Normal Mode</h4>



<p class="wp-block-paragraph">Close sqlcmd, then restart the service normally:</p>



<pre class="wp-block-preformatted"><code>Stop-Service -Name 'MSSQLSERVER' -Force<br>Start-Service -Name 'MSSQLSERVER'<br>Start-Service -Name 'SQLSERVERAGENT'</code></pre>



<p class="wp-block-paragraph">This stops the single-user instance and starts SQL Server in normal multi-user mode. Then restart the Agent. If Agent jobs do not start, check&nbsp;<a rel="noopener" target="_blank" href="https://databasefaqs.com/sql-server-agent-wont-start/">SQL Server Agent will not start</a>.</p>



<h4 class="wp-block-heading">Step 8: Check Mixed Mode if&nbsp;<code>sa</code>&nbsp;Still Fails</h4>



<p class="wp-block-paragraph">If&nbsp;<code>sa</code>&nbsp;still cannot connect, the instance may be in Windows-only mode. Check from a sysadmin session:</p>



<pre class="wp-block-preformatted"><code>SELECT SERVERPROPERTY('IsIntegratedSecurityOnly') AS WindowsOnlyMode;</code></pre>



<p class="wp-block-paragraph">A value of&nbsp;<code>1</code>&nbsp;means Windows authentication only. In SSMS, right-click the server, choose&nbsp;<strong>Properties &gt; Security</strong>, and select&nbsp;<strong>SQL Server and Windows Authentication mode</strong>. Restart the service for the change to take effect.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong>&nbsp;I stop every other service that connects to SQL Server before starting single-user mode, including monitoring agents and backup tools. A forgotten agent grabbing the one connection is the number one reason this method seems to fail.</p>
</blockquote>



<h3 class="wp-block-heading">Method 2: Reset&nbsp;<code>sa</code>&nbsp;on SQL Server for Linux</h3>



<p class="wp-block-paragraph">On Linux, the&nbsp;<code>mssql-conf</code>&nbsp;utility handles this directly, and you do not need single-user mode.</p>



<pre class="wp-block-preformatted"><code>sudo systemctl stop mssql-server<br>sudo /opt/mssql/bin/mssql-conf set-sa-password<br>sudo systemctl start mssql-server</code></pre>



<p class="wp-block-paragraph">The first command stops the service. The second prompts you for a new&nbsp;<code>sa</code>&nbsp;password twice, so the password never appears on the command line. The third starts SQL Server again. You need&nbsp;<code>sudo</code>&nbsp;rights on the host. Check status afterward with&nbsp;<code>systemctl status mssql-server</code>.</p>



<p class="wp-block-paragraph">If you cannot sign in with&nbsp;<code>sa</code>&nbsp;afterward, SQL authentication may have been turned off, or the account may be locked by policy. Review the error log as covered below.</p>



<h3 class="wp-block-heading">Method 3: SQL Server on an Azure VM</h3>



<p class="wp-block-paragraph">An Azure VM running SQL Server behaves like any Windows or Linux host, with one extra problem: you may not know the VM&#8217;s own administrator password either.</p>



<ol class="wp-block-list">
<li>Make sure you can reach the VM. See how to <a href="https://azurelessons.com/how-to-access-azure-vm/" target="_blank" rel="noopener">access an Azure VM</a>.</li>



<li>If you have lost the VM admin password, follow how to <a href="https://azurelessons.com/how-to-reset-password-in-azure-virtual-machine/" target="_blank" rel="noopener">reset the password in an Azure virtual machine</a>.</li>



<li>Sign in with Remote Desktop (or SSH on Linux) and use Method 1 or Method 2.</li>
</ol>



<p class="wp-block-paragraph">Keep the network locked down while you work. A&nbsp;<strong>network security group (NSG)</strong>&nbsp;holds the rules that allow or deny traffic to the VM. Read&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/what-is-nsg-in-azure/">what an NSG in Azure is</a>, and make sure TCP 1433 is open only to your application subnet, not the internet. If you give people access to the VM, use&nbsp;<strong>Azure RBAC</strong>, which grants permissions through roles. Learn the basics in&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/what-is-rbac/">what Azure RBAC is</a>.</p>



<h3 class="wp-block-heading">Method 4: Azure SQL Database and Managed Instance</h3>



<p class="wp-block-paragraph">There is no&nbsp;<code>sa</code>&nbsp;login in Azure SQL Database. Instead, you have a&nbsp;<strong>server admin login</strong>&nbsp;that you chose at creation, and optionally a&nbsp;<strong>Microsoft Entra admin</strong>. If you lose the password, you reset it at the Azure level. You do not need any SQL login.</p>



<p class="wp-block-paragraph">For Azure SQL Database, run:</p>



<pre class="wp-block-preformatted"><code>az sql server update \<br>  --resource-group &lt;resource-group-name> \<br>  --name &lt;sql-server-name> \<br>  --admin-password "&lt;new-strong-password>"</code></pre>



<p class="wp-block-paragraph">This sets a new password for the server admin login. For Managed Instance, use <code>az sql mi update</code> with the same <code>--admin-password</code> option. You need an Azure role that can modify the server, such as SQL Server Contributor. </p>



<p class="wp-block-paragraph">Read the related guides on how to <a href="https://databasefaqs.com/reset-password-azure-sql-database/" target="_blank" rel="noopener">reset a password in Azure SQL Database</a> and <a href="https://azurelessons.com/change-password-azure-sql-server-database/" target="_blank" rel="noopener">change a password for an Azure SQL server database</a>. If the new value is rejected, see <a href="https://databasefaqs.com/azure-sql-password-validation-failed/" target="_blank" rel="noopener">Azure SQL password validation failed</a>.</p>



<p class="wp-block-paragraph">The better fix is to stop using the password altogether. Set a&nbsp;<strong>Microsoft Entra ID admin</strong>&nbsp;on the server, and sign in with your Entra identity. Start with the&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/microsoft-entra-id-tutorial-for-beginners/">Microsoft Entra ID tutorial for beginners</a>. Applications can then use a&nbsp;<strong>managed identity</strong>, which lets an Azure service authenticate without a stored secret. Read&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/what-is-a-managed-identity-in-azure/">what a managed identity in Azure is</a>.</p>



<p class="wp-block-paragraph">Avoid typing the new password in plain text on a shared screen. Use a variable or prompt, and clear your shell history afterward.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong>&nbsp;On Azure SQL, I reset the admin password once, immediately set an Entra admin group, and then stop using the SQL admin login. It turns a recurring emergency into a one-time cleanup.</p>
</blockquote>



<h3 class="wp-block-heading">Store and Protect the New Password</h3>



<p class="wp-block-paragraph">A reset only helps if the new password does not get lost again. Treat it as a break-glass credential.</p>



<p class="wp-block-paragraph"><strong>Use Azure Key Vault.</strong>&nbsp;<strong>Azure Key Vault</strong>&nbsp;is a service for storing secrets, keys, and certificates. Read&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/how-does-azure-key-vault-work/">how Azure Key Vault works</a>&nbsp;and follow how to&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/create-a-secret-in-azure-key-vault/">create a secret in Azure Key Vault</a>.</p>



<pre class="wp-block-preformatted"><code>$SecretValue = ConvertTo-SecureString $NewPassword -AsPlainText -Force<br><br>Set-AzKeyVaultSecret `<br>  -VaultName "&lt;key-vault-name>" `<br>  -Name "sql-sa-breakglass" `<br>  -SecretValue $SecretValue</code></pre>



<p class="wp-block-paragraph"><code>ConvertTo-SecureString</code> wraps the password so the cmdlet can accept it, and <code>Set-AzKeyVaultSecret</code> stores it as a secret. Giving the secret a clear name and an owner makes it easy to find during the next emergency. </p>



<p class="wp-block-paragraph">Then clear the variable from your session with <code>Remove-Variable NewPassword, SecretValue</code>. Never save the password in a spreadsheet, a shared document, a chat message, or a script.</p>



<p class="wp-block-paragraph"><strong>Limit who can read it.</strong>&nbsp;Give a small group the Key Vault Secrets User role, and alert when anyone reads this secret.</p>



<h3 class="wp-block-heading">Lock Down&nbsp;<code>sa</code>&nbsp;After Recovery</h3>



<p class="wp-block-paragraph">Once you have a named administrator group, reduce the risk that&nbsp;<code>sa</code>&nbsp;carries.</p>



<ul class="wp-block-list">
<li><strong>Disable it for daily use.</strong> Run <code>ALTER LOGIN sa DISABLE;</code> after you confirm the Windows admin group works.</li>



<li><strong>Never use it in applications.</strong> Applications should connect with their own least-privilege users, not <code>sa</code> or any sysadmin account. Use a login with only the roles it needs.</li>



<li><strong>Prefer Windows or Entra authentication.</strong> Passwords can be shared, guessed, or leaked. Identity-based sign-in leaves a clearer trail.</li>



<li><strong>Rename it if your policy allows.</strong> Renaming <code>sa</code> removes an easy target for password-guessing attacks, though it does not replace strong passwords.</li>



<li><strong>Keep the connection encrypted.</strong> Require TLS, and install a certificate from a trusted authority.</li>
</ul>



<p class="wp-block-paragraph">Review who has sysadmin rights, and what each person can do, with these checks:</p>



<pre class="wp-block-preformatted"><code>SELECT p.name, p.type_desc, p.is_disabled<br>FROM sys.server_role_members AS m<br>JOIN sys.server_principals AS r ON r.principal_id = m.role_principal_id<br>JOIN sys.server_principals AS p ON p.principal_id = m.member_principal_id<br>WHERE r.name = N'sysadmin';</code></pre>



<p class="wp-block-paragraph">This lists every member of the sysadmin role. Anyone who should not be on the list is a finding. For deeper checks, see how to&nbsp;<a rel="noopener" target="_blank" href="https://www.sqlserverguides.com/how-to-check-database-role-permissions-in-sql-server/">check database role permissions in SQL Server</a>&nbsp;and when a login was last used with&nbsp;<a rel="noopener" target="_blank" href="https://www.sqlserverguides.com/sql-server-last-login-date-for-user/">last login date for a user</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>&nbsp;I leave one disabled&nbsp;<code>sa</code>&nbsp;and two named admin groups in place, and I test the break-glass path every quarter. A recovery process nobody has practiced tends to fail on the one night you need it.</p>
</blockquote>



<h3 class="wp-block-heading">Audit, Monitor, and Alert</h3>



<p class="wp-block-paragraph">Regaining sysadmin access is a sensitive event. Make sure you can prove what happened.</p>



<ul class="wp-block-list">
<li><strong>Review the error log.</strong> Failed and successful logins, and the service restart, appear in the log. See how to <a href="https://databasefaqs.com/view-sql-server-error-logs/" target="_blank" rel="noopener">view SQL Server error logs</a>.</li>



<li><strong>Turn on auditing.</strong> Track logins, role membership changes, and failed logins. For Azure SQL, see how to <a href="https://azurelessons.com/configure-azure-sql-database-auditing/" target="_blank" rel="noopener">configure Azure SQL Database auditing</a>.</li>



<li><strong>Alert on <code>sa</code> use.</strong> Any successful <code>sa</code> login after recovery should raise an alert, since you disabled it for daily use.</li>



<li><strong>Send logs to Azure Monitor.</strong> <strong>Azure Monitor</strong> collects metrics and logs and triggers alerts. Learn <a href="https://azurelessons.com/what-does-azure-monitor-do/" target="_blank" rel="noopener">what Azure Monitor does</a>.</li>
</ul>



<p class="wp-block-paragraph">Write down what you did: the time of the restart, the commands you ran, who approved the change, and where the new secret lives.</p>



<h3 class="wp-block-heading">Test Normal, Failure, and Unauthorized Scenarios</h3>



<p class="wp-block-paragraph">Before you close the ticket, test the result.</p>



<ul class="wp-block-list">
<li><strong>Normal:</strong> Sign in with the named admin group and confirm full access. Confirm that applications reconnect and read data.</li>



<li><strong>Failure:</strong> Try an intentionally wrong password on a test login and confirm the lockout and alert behave as expected.</li>



<li><strong>Unauthorized:</strong> Sign in as a normal application user and confirm it cannot create logins, change roles, or read data outside its scope.</li>



<li><strong>Recovery:</strong> Store a copy of the break-glass process, and try reading the secret from Key Vault as an authorized person.</li>
</ul>



<h3 class="wp-block-heading">Common Errors and Fixes</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Problem</th><th class="has-text-align-left" data-align="left">Likely cause</th><th class="has-text-align-left" data-align="left">Fix</th></tr></thead><tbody><tr><td><code>Login failed for user 'sa'</code>&nbsp;after reset</td><td>Mixed mode is off, or the account is disabled</td><td>Enable mixed mode, run&nbsp;<code>ALTER LOGIN sa ENABLE</code>, restart</td></tr><tr><td>Cannot connect in single-user mode</td><td>Another process took the only connection</td><td>Stop the Agent and monitoring tools, then retry</td></tr><tr><td>Service will not start with&nbsp;<code>/m</code></td><td>Wrong service name or bad startup parameter</td><td>Check the name, remove stray startup options, read the log</td></tr><tr><td><code>Password validation failed</code></td><td>New password violates complexity rules</td><td>Use a longer password with mixed characters</td></tr><tr><td>Not a member of local Administrators</td><td>Insufficient host rights</td><td>Ask the server owner to run the procedure</td></tr><tr><td>Azure&nbsp;<code>az sql server update</code>&nbsp;is denied</td><td>Missing Azure role</td><td>Request SQL Server Contributor or equivalent</td></tr><tr><td>Applications still fail after reset</td><td>They store the old&nbsp;<code>sa</code>&nbsp;password</td><td>Create dedicated users and update the secrets</td></tr></tbody></table></figure>



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



<h4 class="wp-block-heading">How do I reset the SQL Server sa password if I forgot it?</h4>



<p class="wp-block-paragraph">Stop the SQL Server service, start it in single-user mode with&nbsp;<code>net start MSSQLSERVER /mSQLCMD</code>, and connect with&nbsp;<code>sqlcmd -S . -E</code>&nbsp;as a local Windows administrator. Then run&nbsp;<code>ALTER LOGIN sa WITH PASSWORD = ...</code>. Restart the service normally afterward.</p>



<h4 class="wp-block-heading">Can I reset the sa password without being a Windows administrator?</h4>



<p class="wp-block-paragraph">No. The single-user method requires local administrator rights on the host, and that requirement protects the server. Ask the system owner for access, or have another sysadmin reset the login.</p>



<h4 class="wp-block-heading">Does Azure SQL Database have an sa account?</h4>



<p class="wp-block-paragraph">No. Azure SQL Database and Managed Instance use a server admin login, plus an optional Microsoft Entra admin. If you lose the admin password, reset it with&nbsp;<code>az sql server update --admin-password</code>. Prefer Entra authentication so you do not depend on a password.</p>



<h4 class="wp-block-heading">How do I reset the sa password on SQL Server for Linux?</h4>



<p class="wp-block-paragraph">Stop&nbsp;<code>mssql-server</code>, run&nbsp;<code>sudo /opt/mssql/bin/mssql-conf set-sa-password</code>, and enter the new password when prompted. Then start the service again.</p>



<h4 class="wp-block-heading">Should I keep using sa after resetting it?</h4>



<p class="wp-block-paragraph">No. Create a named administrator group, store the new&nbsp;<code>sa</code>&nbsp;password in Key Vault as a break-glass credential, and disable&nbsp;<code>sa</code>&nbsp;for daily use. Applications should use their own least-privilege logins or managed identities.</p>



<p class="wp-block-paragraph">You learned how to regain control of a SQL Server instance by resetting <code>sa</code> through single-user mode, <code>mssql-conf</code>, or the Azure admin password, and how to lock down access afterward. The key principle is to treat <code>sa</code> as a break-glass credential: store it in Key Vault, disable it for daily use, and rely on named, audited, least-privilege identities. 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://www.sqlserverguides.com/how-to-check-object-level-permissions-in-sql-server/" target="_blank" rel="noopener">How to check object-level permissions in SQL Server</a></li>



<li><a href="https://databasefaqs.com/sql-server-user-permissions/" target="_blank" rel="noopener">SQL Server user permissions explained</a></li>



<li><a href="https://databasefaqs.com/sql-server-check-user-permissions-on-table/" target="_blank" rel="noopener">How to check user permissions on a table in SQL Server</a></li>



<li><a href="https://www.sqlserverguides.com/sql-server-grant-select-on-table/" target="_blank" rel="noopener">How to grant SELECT on a table in SQL Server</a></li>



<li><a href="https://azurelessons.com/azure-key-vault-best-practices/" target="_blank" rel="noopener">Azure Key Vault best practices</a></li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How To Check If SQL Server Is Reachable</title>
		<link>https://sqlserverguides.com/how-to-check-if-sql-server-is-reachable/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Mon, 05 Oct 2026 10:23:54 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[How To Check If SQL Server Is Reachable]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=23835</guid>

					<description><![CDATA[A retail company called me on a Monday morning because its order app had stopped working. The error message said &#8220;A network-related or instance-specific error occurred,&#8221; and the team had already restarted the app servers twice. Nobody had checked whether the app servers could even reach the database. It turned out someone had changed a ... <a title="How To Check If SQL Server Is Reachable" class="read-more" href="https://sqlserverguides.com/how-to-check-if-sql-server-is-reachable/" aria-label="Read more about How To Check If SQL Server Is Reachable">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A retail company called me on a Monday morning because its order app had stopped working. The error message said &#8220;A network-related or instance-specific error occurred,&#8221; and the team had already restarted the app servers twice. Nobody had checked whether the app servers could even reach the database. It turned out someone had changed a network rule over the weekend, and port 1433 was blocked.</p>



<p class="wp-block-paragraph">Knowing&nbsp;<strong>how to check if SQL Server is reachable</strong>&nbsp;saves hours in situations like this. A failed connection can come from DNS, the network path, a firewall, the SQL Server service, encryption, a login, or a paused database. Each layer needs a different fix, so guessing wastes time.</p>



<p class="wp-block-paragraph">This guide walks you through a layered test, from the cheapest check to the most detailed one. You will use PowerShell, sqlcmd, T-SQL, Azure CLI, and a short Python script. You will also learn the Azure-specific causes, such as firewall rules and private endpoints, and how to read the common error numbers.</p>



<h2 class="wp-block-heading">The Layered Approach to Testing Reachability</h2>



<p class="wp-block-paragraph">&#8220;Reachable&#8221; has several meanings. A server can answer a ping but refuse SQL connections. It can accept connections but reject your login. Test one layer at a time, and stop at the first failure.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Layer</th><th class="has-text-align-left" data-align="left">Question</th><th class="has-text-align-left" data-align="left">Quick test</th><th class="has-text-align-left" data-align="left">Typical failure</th></tr></thead><tbody><tr><td>1. DNS</td><td>Does the name resolve to the right IP?</td><td><code>nslookup</code></td><td>Wrong or private IP, or no answer</td></tr><tr><td>2. Network path and port</td><td>Can I open a TCP connection to the port?</td><td><code>Test-NetConnection</code></td><td>Timeout</td></tr><tr><td>3. Firewall and NSG</td><td>Is a rule blocking the port?</td><td>Check rules</td><td>Blocked at Windows, Azure, or network firewall</td></tr><tr><td>4. SQL Server service</td><td>Is the engine running and listening?</td><td>Service status, logs</td><td>Service stopped</td></tr><tr><td>5. Encryption</td><td>Does the TLS handshake succeed?</td><td><code>sqlcmd -N</code></td><td>Certificate not trusted</td></tr><tr><td>6. Authentication</td><td>Does the login work?</td><td><code>sqlcmd</code>&nbsp;with credentials</td><td>Error 18456</td></tr><tr><td>7. Database state</td><td>Is the database online?</td><td>Query status</td><td>Paused or restoring</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">SQL Server listens on TCP port 1433 by default. Named instances often use a dynamic port, and the SQL Server Browser service on UDP port 1434 tells clients which port to use. </p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong>&nbsp;In my experience, ping is the most misleading test in this list. Many networks block ping on purpose, and a successful ping says nothing about port 1433. I skip it and go straight to a port test.</p>
</blockquote>



<h3 class="wp-block-heading">Step 1: Gather the Connection Details</h3>



<p class="wp-block-paragraph">You cannot test what you cannot name. Collect these facts first:</p>



<ul class="wp-block-list">
<li><strong>Server name:</strong> For Azure SQL Database, this looks like <code>&lt;sql-server-name>.database.windows.net</code>. For a VM, it is a host name or IP address.</li>



<li><strong>Instance name:</strong> A named instance is written as <code>server\instance</code>. See how to <a href="https://www.sqlserverguides.com/how-to-find-sql-server-instance-name-in-ssms/" target="_blank" rel="noopener">find the SQL Server instance name in SSMS</a> if you are unsure.</li>



<li><strong>Port:</strong> Usually 1433. Named instances may use a dynamic port.</li>



<li><strong>Authentication method:</strong> Microsoft Entra ID, Windows authentication, or SQL authentication.</li>



<li><strong>Where you are testing from:</strong> Test from the same machine, subnet, or network that the application uses. A test from your laptop proves nothing about a server in another virtual network.</li>
</ul>



<p class="wp-block-paragraph">If you can already connect to one server and need its name, use this query:</p>



<pre class="wp-block-preformatted"><code>SELECT @@SERVERNAME AS ServerName,<br>       SERVERPROPERTY('MachineName') AS MachineName,<br>       SERVERPROPERTY('InstanceName') AS InstanceName;</code></pre>



<p class="wp-block-paragraph">This returns the server name, the host machine name, and the instance name (NULL for a default instance). You can also read the guide on how to&nbsp;<a rel="noopener" target="_blank" href="https://www.sqlserverguides.com/get-server-name-in-sql-server-using-query/">get the server name in SQL Server using a query</a>.</p>



<p class="wp-block-paragraph">For Azure SQL Database, get the server&#8217;s fully qualified name with the CLI:</p>



<pre class="wp-block-preformatted"><code>az sql server show \<br>  --name &lt;sql-server-name> \<br>  --resource-group &lt;resource-group-name> \<br>  --query fullyQualifiedDomainName \<br>  --output tsv</code></pre>



<p class="wp-block-paragraph">The&nbsp;<code>--query</code>&nbsp;and&nbsp;<code>--output tsv</code>&nbsp;options return only the host name, which is easy to copy into the tests below.</p>



<h3 class="wp-block-heading">Step 2: Test DNS Resolution</h3>



<p class="wp-block-paragraph">Before you test the port, confirm the name points where you expect.</p>



<pre class="wp-block-preformatted"><code>Resolve-DnsName &lt;sql-server-name>.database.windows.net</code></pre>



<p class="wp-block-paragraph"><code>Resolve-DnsName</code>&nbsp;returns the IP address for the name. On Linux or macOS, use&nbsp;<code>nslookup &lt;server-name&gt;</code>.</p>



<p class="wp-block-paragraph">Check the answer carefully. If you use a&nbsp;<strong>private endpoint</strong>, which gives an Azure service a private IP address inside your virtual network, the name should resolve to a private address such as&nbsp;<code>10.x.x.x</code>. If it returns a public IP, your private DNS zone is missing or not linked to the network. See how to&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/how-to-create-private-endpoint-in-azure/">create a private endpoint in Azure</a>&nbsp;and how a&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/private-dns-zone-azure/">private DNS zone works</a>. The message in the&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/the-server-was-not-found-or-was-not-accessible/">server was not found or was not accessible</a>&nbsp;error often points to this layer.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong>&nbsp;I once spent an hour on firewall rules before I ran a DNS lookup. The name resolved to a public IP, but the database only accepted private traffic. Now DNS is my first test every time.</p>
</blockquote>



<h3 class="wp-block-heading">Step 3: Test the Network Path and Port</h3>



<p class="wp-block-paragraph">This is the most useful single test. It tells you whether a TCP connection can open, without involving SQL Server logins at all.</p>



<pre class="wp-block-preformatted"><code>Test-NetConnection -ComputerName &lt;server-name> -Port 1433</code></pre>



<p class="wp-block-paragraph">Read the result:</p>



<ul class="wp-block-list">
<li><code>TcpTestSucceeded : True</code> means the network path and port are open.</li>



<li><code>TcpTestSucceeded : False</code> means something is blocking or nothing is listening.</li>
</ul>



<p class="wp-block-paragraph">For a named instance with a custom port, change the port number. On Linux or macOS, use this:</p>



<pre class="wp-block-preformatted"><code>nc -zv &lt;server-name> 1433</code></pre>



<p class="wp-block-paragraph">The&nbsp;<code>-z</code>&nbsp;flag checks for a listener without sending data, and&nbsp;<code>-v</code>&nbsp;prints the result.</p>



<p class="wp-block-paragraph">You can also test from a script, which is useful in pipelines and health checks:</p>



<pre class="wp-block-preformatted"><code>import socket<br>import sys<br><br>server = sys.argv[1]<br>port = int(sys.argv[2]) if len(sys.argv) > 2 else 1433<br><br>try:<br>    with socket.create_connection((server, port), timeout=5):<br>        print(f"OK: TCP connection to {server}:{port} succeeded")<br>except socket.gaierror:<br>    print("FAIL: DNS lookup failed")<br>except socket.timeout:<br>    print("FAIL: Timed out - likely blocked by a firewall or NSG")<br>except ConnectionRefusedError:<br>    print("FAIL: Refused - host reachable, but nothing is listening on that port")<br>except OSError as error:<br>    print(f"FAIL: {error}")</code></pre>



<p class="wp-block-paragraph">Run it with&nbsp;<code>python check_sql_port.py &lt;server-name&gt; 1433</code>. The script separates three different failures. A DNS error means the name is wrong. A timeout usually means a firewall is silently dropping packets. A refusal means the host answered, but SQL Server is not listening on that port. That distinction narrows your search before you touch a single setting.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong>&nbsp;I save this script in the team&#8217;s tools repository. When someone says &#8220;the database is down,&#8221; we run it from the app server first, and the answer usually arrives in under ten seconds.</p>
</blockquote>



<h3 class="wp-block-heading">Step 4: Check Firewalls and Network Rules</h3>



<p class="wp-block-paragraph">If the port test timed out, look at every firewall between the client and the server.</p>



<h4 class="wp-block-heading">SQL Server on a Windows VM</h4>



<ol class="wp-block-list">
<li>Confirm the <strong>Windows Defender Firewall</strong> has an inbound rule that allows TCP 1433 (and UDP 1434 for SQL Browser if you use named instances).</li>



<li>Check the <strong>network security group (NSG)</strong>, which holds allow and deny rules for traffic in Azure. Learn the basics in <a href="https://azurelessons.com/what-is-nsg-in-azure/" target="_blank" rel="noopener">what an NSG in Azure is</a>. Make sure no deny rule blocks port 1433 before your allow rule.</li>



<li>If you need to add one, see how to <a href="https://azurelessons.com/how-to-open-ports-on-azure-vm/" target="_blank" rel="noopener">open ports on an Azure VM</a>.</li>
</ol>



<pre class="wp-block-preformatted"><code>az network nsg rule create \<br>  --resource-group &lt;resource-group-name> \<br>  --nsg-name &lt;nsg-name> \<br>  --name Allow-SQL-From-App-Subnet \<br>  --priority 300 \<br>  --direction Inbound \<br>  --access Allow \<br>  --protocol Tcp \<br>  --source-address-prefixes 10.0.1.0/24 \<br>  --destination-port-ranges 1433</code></pre>



<p class="wp-block-paragraph">This creates an inbound rule that allows TCP 1433 only from the app subnet&nbsp;<code>10.0.1.0/24</code>. The&nbsp;<code>--priority 300</code>&nbsp;value must be lower than any deny rule that would block it.&nbsp;<strong>Warning:</strong>&nbsp;never set the source to&nbsp;<code>*</code>&nbsp;or&nbsp;<code>0.0.0.0/0</code>&nbsp;for a database port. That exposes SQL Server to the entire internet. Restrict it to the exact subnets or addresses that need access, and keep the VM inside a private&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/what-is-an-azure-virtual-network/">virtual network</a>.</p>



<h4 class="wp-block-heading">Azure SQL Database</h4>



<p class="wp-block-paragraph">Azure SQL Database has its own&nbsp;<strong>server firewall</strong>, which allows or blocks client IP addresses. Error 40615 means your IP is not allowed. See how to&nbsp;<a rel="noopener" target="_blank" href="https://databasefaqs.com/azure-sql-database-configure-firewall/">configure the Azure SQL Database firewall</a>&nbsp;and how to find the&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/azure-sql-database-ip-address/">Azure SQL Database IP address</a>&nbsp;your client uses.</p>



<pre class="wp-block-preformatted"><code>az sql server firewall-rule create \<br>  --resource-group &lt;resource-group-name> \<br>  --server &lt;sql-server-name> \<br>  --name AllowMyOfficeIP \<br>  --start-ip-address &lt;office-public-ip> \<br>  --end-ip-address &lt;office-public-ip></code></pre>



<p class="wp-block-paragraph">This creates a rule for one public IP address. Use the same value for start and end to allow a single address. Never use the range 0.0.0.0 to 255.255.255.255. For production, prefer private endpoints and disable public access.</p>



<p class="wp-block-paragraph">Also remember a connection policy detail. In the default Redirect mode, clients connect to the gateway on port 1433 and are then redirected to the node on ports in the 11000 to 11999 range, so outbound rules must allow that range. If a network team only opens 1433, connections may fail after the redirect.</p>



<h4 class="wp-block-heading">Azure SQL Managed Instance</h4>



<p class="wp-block-paragraph">A Managed Instance lives inside its own subnet in your virtual network, so reachability depends on routes, NSGs, and peering. If you enable its public endpoint, it uses port 3342 and needs an NSG rule to match. Once connected, see how to monitor the instance with&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/azure-sql-managed-instance-monitoring/">Azure SQL Managed Instance monitoring</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>&nbsp;I keep a one-page diagram of the network path for every production database: client, subnet, NSG, route, firewall, database. During an outage, nobody has to guess where to look.</p>
</blockquote>



<h3 class="wp-block-heading">Step 5: Confirm the SQL Server Service Is Running</h3>



<p class="wp-block-paragraph">If the port test failed with &#8220;refused,&#8221; or the host answers but SQL does not, the engine may be stopped or not listening.</p>



<p class="wp-block-paragraph">On a Windows host, check the service:</p>



<pre class="wp-block-preformatted"><code>Get-Service -Name 'MSSQLSERVER'</code></pre>



<p class="wp-block-paragraph">For a named instance, the service name is&nbsp;<code>MSSQL$&lt;instance-name&gt;</code>. If the status is&nbsp;<code>Stopped</code>, start it and read the error log to find out why. See&nbsp;<a rel="noopener" target="_blank" href="https://databasefaqs.com/how-to-check-if-sql-server-is-running/">how to check if SQL Server is running</a>. If the service will not start, these guides cover the&nbsp;<a rel="noopener" target="_blank" href="https://www.sqlserverguides.com/sql-server-service-not-starting/">SQL Server service not starting</a>&nbsp;problem.</p>



<p class="wp-block-paragraph">Next, open&nbsp;<strong>SQL Server Configuration Manager</strong>&nbsp;and verify:</p>



<ul class="wp-block-list">
<li><strong>TCP/IP</strong> is enabled under SQL Server Network Configuration.</li>



<li>The port in the TCP/IP properties matches what clients use.</li>



<li><strong>SQL Server Browser</strong> is running if you use named instances without a fixed port.</li>
</ul>



<p class="wp-block-paragraph">Restart the SQL Server service after changing network settings.</p>



<p class="wp-block-paragraph">Once you are on the host, a quick check of what the server is listening on helps:</p>



<pre class="wp-block-preformatted"><code>SELECT local_net_address, local_tcp_port, net_transport, encrypt_option<br>FROM sys.dm_exec_connections<br>WHERE session_id = @@SPID;</code></pre>



<p class="wp-block-paragraph">This shows the address and port your current session uses and whether it is encrypted. It is a fast way to confirm the real port instead of assuming 1433.</p>



<h3 class="wp-block-heading">Step 6: Test an Actual SQL Connection with sqlcmd</h3>



<p class="wp-block-paragraph">A successful TCP test proves the door is open. Now test the full connection, including encryption and login.</p>



<p class="wp-block-paragraph">For SQL Server on a VM with Windows authentication:</p>



<pre class="wp-block-preformatted"><code>sqlcmd -S tcp:&lt;server-name>,1433 -E -N -Q "SELECT @@SERVERNAME, GETDATE();"</code></pre>



<p class="wp-block-paragraph">For Azure SQL Database with Microsoft Entra authentication:</p>



<pre class="wp-block-preformatted"><code>sqlcmd -S tcp:&lt;sql-server-name>.database.windows.net,1433 -d &lt;database-name> -G -N -l 30 -Q "SELECT DB_NAME(), SUSER_SNAME();"</code></pre>



<p class="wp-block-paragraph">Here is what the flags do:</p>



<ul class="wp-block-list">
<li><code>-S tcp:&lt;server>,&lt;port></code> forces a TCP connection to a specific port. Writing the port after a comma avoids relying on SQL Browser.</li>



<li><code>-E</code> uses Windows authentication. <code>-G</code> uses Microsoft Entra ID authentication.</li>



<li><code>-N</code> requires an encrypted connection. Do not turn encryption off to make a test pass.</li>



<li><code>-l 30</code> sets a 30-second login timeout.</li>



<li><code>-Q</code> runs the query and exits.</li>
</ul>



<p class="wp-block-paragraph">If you get a certificate error on a test server with a self-signed certificate,&nbsp;<code>-C</code>&nbsp;trusts the server certificate. Use it only in a lab. In production, install a certificate from a trusted authority and keep validation on.</p>



<p class="wp-block-paragraph">Prefer Microsoft Entra ID and managed identities over SQL passwords. Read&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/what-is-a-managed-identity-in-azure/">what a managed identity in Azure is</a>, and if a password is unavoidable, store it in&nbsp;<strong>Azure Key Vault</strong>. See&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/how-does-azure-key-vault-work/">how Azure Key Vault works</a>. Never keep passwords in scripts, config files, or spreadsheets.</p>



<p class="wp-block-paragraph">To test from a desktop tool, use&nbsp;<a rel="noopener" target="_blank" href="https://www.sqlserverguides.com/how-to-connect-to-azure-sql-database-from-ssms/">SSMS to connect to Azure SQL Database</a>. If SSMS fails, the guide on&nbsp;<a rel="noopener" target="_blank" href="https://www.sqlserverguides.com/ssms-cannot-connect-to-server/">SSMS cannot connect to server</a>&nbsp;lists common causes.</p>



<h3 class="wp-block-heading">Test from Application Code</h3>



<p class="wp-block-paragraph">Applications fail in ways sqlcmd does not, such as driver or timeout problems. This Python test uses the same driver your app would:</p>



<pre class="wp-block-preformatted"><code>import pyodbc<br><br>connection_string = (<br>    "Driver={ODBC Driver 18 for SQL Server};"<br>    "Server=tcp:&lt;sql-server-name>.database.windows.net,1433;"<br>    "Database=&lt;database-name>;"<br>    "Authentication=ActiveDirectoryDefault;"<br>    "Encrypt=yes;"<br>    "TrustServerCertificate=no;"<br>    "Connection Timeout=30;"<br>)<br><br>try:<br>    with pyodbc.connect(connection_string) as connection:<br>        row = connection.cursor().execute("SELECT 1").fetchone()<br>        print("Connected, result:", row[0])<br>except pyodbc.Error as error:<br>    print("Connection failed:", error)</code></pre>



<p class="wp-block-paragraph"><code>ActiveDirectoryDefault</code>&nbsp;uses your signed-in identity, such as an&nbsp;<code>az login</code>&nbsp;session on a laptop or a managed identity in Azure, so no password appears in the code.&nbsp;<code>Encrypt=yes</code>&nbsp;and&nbsp;<code>TrustServerCertificate=no</code>&nbsp;enforce encryption and certificate validation. The&nbsp;<code>Connection Timeout=30</code>&nbsp;setting gives a gateway or a resuming database time to respond. Make sure the ODBC driver is installed, and check your driver version supports this authentication option.</p>



<p class="wp-block-paragraph">For a connection string reference, see&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/azure-sql-database-connection-string/">Azure SQL Database connection string</a>&nbsp;and how to&nbsp;<a rel="noopener" target="_blank" href="https://databasefaqs.com/how-to-get-the-connection-string-from-sql-server/">get the connection string from 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>&nbsp;I always run both sqlcmd and a short app-language test. When sqlcmd works and the app fails, the problem is the driver, the connection string, or the identity, and I can stop checking the network.</p>
</blockquote>



<h3 class="wp-block-heading">Step 7: Check the Database State</h3>



<p class="wp-block-paragraph">Sometimes the server is fine, but the database is not ready. For Azure SQL, check its status:</p>



<pre class="wp-block-preformatted"><code>az sql db show \<br>  --resource-group &lt;resource-group-name> \<br>  --server &lt;sql-server-name> \<br>  --name &lt;database-name> \<br>  --query "{name:name, status:status}" \<br>  --output table</code></pre>



<p class="wp-block-paragraph">The&nbsp;<code>status</code>&nbsp;field should read&nbsp;<code>Online</code>. A serverless database that is paused returns error 40613 on the first connection while it resumes. Retrying after about a minute usually works, and apps should include retry logic. Learn how it behaves in&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/azure-sql-database-serverless/">Azure SQL Database serverless</a>.</p>



<p class="wp-block-paragraph">On a VM or Managed Instance, check the state with T-SQL:</p>



<pre class="wp-block-preformatted"><code>SELECT name, state_desc, user_access_desc<br>FROM sys.databases<br>WHERE name = N'&lt;database-name>';</code></pre>



<p class="wp-block-paragraph">This shows whether the database is&nbsp;<code>ONLINE</code>,&nbsp;<code>RESTORING</code>,&nbsp;<code>RECOVERING</code>, or&nbsp;<code>OFFLINE</code>, and who is allowed in. A database in the middle of a restore will reject connections until it finishes.</p>



<h3 class="wp-block-heading">Read the Error Message and Number</h3>



<p class="wp-block-paragraph">The error text usually tells you which layer failed. Use this table as a quick map.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Error</th><th class="has-text-align-left" data-align="left">Meaning</th><th class="has-text-align-left" data-align="left">Likely layer</th><th class="has-text-align-left" data-align="left">First fix</th></tr></thead><tbody><tr><td>53 or 40</td><td>Cannot open a connection</td><td>DNS, network, or firewall</td><td>Run the port test, then check rules. See&nbsp;<a rel="noopener" target="_blank" href="https://www.sqlserverguides.com/error-40-could-not-open-connection-to-sql-server/">error 40</a></td></tr><tr><td>26</td><td>Server or instance not found</td><td>Name or SQL Browser</td><td>Check the instance name and the Browser service</td></tr><tr><td>18456</td><td>Login failed</td><td>Authentication</td><td>See&nbsp;<a rel="noopener" target="_blank" href="https://www.sqlserverguides.com/sql-server-error-18456/">SQL Server error 18456</a></td></tr><tr><td>40615</td><td>Client IP not allowed</td><td>Azure SQL firewall</td><td>Add a firewall rule for the correct IP</td></tr><tr><td>40613</td><td>Database not currently available</td><td>Database state</td><td>Wait for resume or check the status</td></tr><tr><td>4060</td><td>Cannot open the requested database</td><td>Permissions or name</td><td>Check the database name and user rights</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">For more detail on these messages, see the guides on&nbsp;<a rel="noopener" target="_blank" href="https://databasefaqs.com/cannot-open-server-requested-by-the-login/">cannot open server requested by the login</a>, the&nbsp;<a rel="noopener" target="_blank" href="https://azurelessons.com/an-instance-specific-error-occurred-while-establishing-a-connection-to-sql-server/">instance-specific error</a>, and&nbsp;<a rel="noopener" target="_blank" href="https://databasefaqs.com/could-not-open-a-connection-to-sql-server/">could not open a connection to SQL Server</a>.</p>



<p class="wp-block-paragraph">If the connection works but queries are denied, the problem is permissions, not reachability. Review&nbsp;<a rel="noopener" target="_blank" href="https://www.sqlserverguides.com/sql-server-permissions/">SQL Server permissions</a>&nbsp;and test with a non-administrator account that has only the rights your application needs. Give each application its own least-privilege user, never&nbsp;<code>sa</code>&nbsp;or&nbsp;<code>sysadmin</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>&nbsp;I write the error number in the incident ticket before anything else. Teams that Google the text instead of the number often chase the wrong layer for an hour.</p>
</blockquote>



<h3 class="wp-block-heading">Monitor Reachability So You Find Out First</h3>



<p class="wp-block-paragraph">Manual tests help during an incident. Monitoring helps you avoid one.</p>



<ul class="wp-block-list">
<li><strong>Azure Monitor:</strong> It collects metrics and logs from Azure SQL and Managed Instance. Learn <a href="https://azurelessons.com/what-does-azure-monitor-do/" target="_blank" rel="noopener">what Azure Monitor does</a>. Alert on failed connections, firewall blocks, and availability drops.</li>



<li><strong>Synthetic checks:</strong> Schedule the Python port test from the app subnet every minute, and alert on two failures in a row.</li>



<li><strong>Log connection events:</strong> Turn on diagnostic logs, and keep them long enough to compare the time a problem started with the time a change was made.</li>



<li><strong>Track changes:</strong> Alert on NSG, firewall, and DNS changes. In my experience, most &#8220;sudden&#8221; outages follow a quiet configuration change.</li>
</ul>



<p class="wp-block-paragraph">Use tags such as owner and environment so alerts reach the right team, and create a budget with alerts so a retry loop does not quietly raise a bill.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Pro Tip:</strong>&nbsp;I add a &#8220;change in the last 24 hours&#8221; check to every outage checklist. It finds the cause faster than any tool, because the network usually breaks because someone changed it.</p>
</blockquote>



<h3 class="wp-block-heading">SQL Server Connectivity Checklist</h3>



<ul class="wp-block-list">
<li><strong>Test in layers:</strong> Check DNS, then the port, then firewalls, then the service, then encryption, then login. Stop at the first failure.</li>



<li><strong>Test from the right place:</strong> Run checks from the application&#8217;s own subnet or host, not from your laptop.</li>



<li><strong>Encrypted connections:</strong> Keep <code>-N</code> and <code>Encrypt=yes</code> on, and install a trusted certificate instead of trusting any server certificate.</li>



<li><strong>Private access:</strong> Use private endpoints or VNet integration for production databases, and never open a database port to the whole internet.</li>



<li><strong>Least privilege:</strong> Give applications their own users, prefer Microsoft Entra ID and managed identities, and test with a non-administrator account.</li>



<li><strong>Retry and monitoring:</strong> Add retry logic for serverless resume and brief gateway failovers, and alert on connectivity changes.</li>
</ul>



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



<h4 class="wp-block-heading">How do I check if SQL Server is reachable from another computer?</h4>



<p class="wp-block-paragraph">Run&nbsp;<code>Test-NetConnection -ComputerName &lt;server-name&gt; -Port 1433</code>&nbsp;from that computer. If it returns&nbsp;<code>TcpTestSucceeded : True</code>, the network path and port are open. Then run&nbsp;<code>sqlcmd</code>&nbsp;with&nbsp;<code>-N</code>&nbsp;to confirm that encryption and your login work.</p>



<h4 class="wp-block-heading">Why can I ping SQL Server but not connect?</h4>



<p class="wp-block-paragraph">Ping only tests basic network reachability. The connection may fail because port 1433 is blocked, the SQL Server service is stopped, TCP/IP is disabled, or the login is wrong. Test the port next, not another ping.</p>



<h4 class="wp-block-heading">What port does SQL Server use?</h4>



<p class="wp-block-paragraph">The default is TCP 1433. Named instances often use a dynamic port, and SQL Server Browser on UDP 1434 helps clients find it. Azure SQL Database also redirects connections to ports 11000 to 11999 in Redirect mode.</p>



<h4 class="wp-block-heading">How do I fix error 40 or error 53 in SQL Server?</h4>



<p class="wp-block-paragraph">Both mean the client cannot find or open a path to the server. Check the server name, DNS resolution, port 1433, firewalls and NSGs, and whether the SQL Server service is running. For Azure SQL, also check the server firewall and private endpoint DNS.</p>



<h4 class="wp-block-heading">How do I check if an Azure SQL Database is online?</h4>



<p class="wp-block-paragraph">Run&nbsp;<code>az sql db show</code>&nbsp;and read the&nbsp;<code>status</code>&nbsp;field, or query&nbsp;<code>sys.databases</code>&nbsp;on a VM or Managed Instance. A serverless database may be paused and take about a minute to resume. Build retry logic into your application.</p>



<p class="wp-block-paragraph">You learned how to test SQL Server reachability layer by layer, from DNS and the port to encryption, login, and database state. The key principle is to test from the application&#8217;s own network, keep encryption and least-privilege access on, and never open a database port to the whole internet. 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://databasefaqs.com/how-to-check-sql-server-edition/" target="_blank" rel="noopener">How to check the SQL Server edition</a></li>



<li><a href="https://databasefaqs.com/view-sql-server-error-logs/" target="_blank" rel="noopener">How to view SQL Server error logs</a></li>



<li><a href="https://databasefaqs.com/sql-server-user-permissions/" target="_blank" rel="noopener">SQL Server user permissions explained</a></li>



<li><a href="https://databasefaqs.com/how-to-reset-sa-password-in-sql-server/" target="_blank" rel="noopener">How to reset the sa password in SQL Server</a></li>



<li><a href="https://azurelessons.com/python-connect-to-azure-sql-database/" target="_blank" rel="noopener">Connect to Azure SQL Database using Python</a></li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to Save Stored Procedure in SQL Server</title>
		<link>https://sqlserverguides.com/how-to-save-stored-procedure-in-sql-server/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Mon, 21 Sep 2026 16:50:58 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[How to Save Stored Procedure in SQL Server]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=23829</guid>

					<description><![CDATA[When I trained a new database developer on my team last year, one of the first questions he asked me was, &#8220;How do I actually save this stored procedure once I&#8217;ve written it?&#8221; It&#8217;s a fair question, because unlike saving a Word document or an Excel file, saving a stored procedure in SQL Server doesn&#8217;t ... <a title="How to Save Stored Procedure in SQL Server" class="read-more" href="https://sqlserverguides.com/how-to-save-stored-procedure-in-sql-server/" aria-label="Read more about How to Save Stored Procedure in SQL Server">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">When I trained a new database developer on my team last year, one of the first questions he asked me was, &#8220;How do I actually save this stored procedure once I&#8217;ve written it?&#8221; It&#8217;s a fair question, because unlike saving a Word document or an Excel file, saving a <strong>stored procedure</strong> in SQL Server doesn&#8217;t work through a simple &#8220;Save&#8221; button. It works through executing specific T-SQL statements that create the procedure as an object inside your database.</p>



<p class="wp-block-paragraph">In this guide, I&#8217;m going to walk you through exactly how I save, modify, and manage stored procedures in SQL Server, using the same approach I&#8217;ve taught to dozens of developers and DBAs over the years. Whether you&#8217;re working in SQL Server Management Studio (SSMS) on a Windows machine in a corporate office, or connecting remotely from a laptop in Seattle, the process is the same, and I&#8217;ll explain not just the commands but why each step matters.</p>



<h2 class="wp-block-heading">How to Save Stored Procedure in SQL Server</h2>



<h3 class="wp-block-heading">What Does &#8220;Saving&#8221; a Stored Procedure Actually Mean?</h3>



<p class="wp-block-paragraph">I want to clear up a common point of confusion right away. A <strong>stored procedure</strong> is a precompiled collection of one or more T-SQL statements stored as a named object inside a SQL Server database. When people ask how to &#8220;save&#8221; one, what they really mean is how to persist that procedure into the database so it can be executed later, by them or by an application, without rewriting the code each time.</p>



<p class="wp-block-paragraph">Unlike a script file sitting on your desktop, a stored procedure isn&#8217;t saved to disk as a standalone file. It&#8217;s saved directly into the database itself, as a database object, right alongside your tables, views, and functions. </p>



<p class="wp-block-paragraph">This is an important distinction I make with every developer I mentor: writing a stored procedure in a query window and executing that script is what actually saves the procedure. Closing the query window without executing it means nothing has been saved at all.</p>



<h3 class="wp-block-heading">The CREATE PROCEDURE Statement</h3>



<p class="wp-block-paragraph">The primary way to save a new stored procedure in SQL Server is with the <code>CREATE PROCEDURE</code> statement, which I use interchangeably with its shorter alias, <code>CREATE PROC</code>.</p>



<h4 class="wp-block-heading">Basic Syntax</h4>



<p class="wp-block-paragraph">Here&#8217;s the fundamental structure I teach every developer starting out:</p>



<pre class="wp-block-preformatted"><code>CREATE PROCEDURE dbo.usp_GetCustomerOrders<br>    @CustomerID INT<br>AS<br>BEGIN<br>    SELECT OrderID, OrderDate, TotalAmount<br>    FROM Orders<br>    WHERE CustomerID = @CustomerID;<br>END;</code></pre>



<p class="wp-block-paragraph">When you highlight this block of code in SSMS and press Execute (or hit F5), SQL Server compiles this statement and saves the procedure as a permanent object in the current database. From that point forward, anyone with the right permissions can call it using the <code>EXEC</code> command, without needing to know or rewrite the underlying logic.</p>



<h3 class="wp-block-heading">Step-by-Step Process I Follow</h3>



<p class="wp-block-paragraph">Whenever I create a new stored procedure for a client project, I follow this same sequence:</p>



<ol class="wp-block-list">
<li>Connect to the correct SQL Server instance and select the correct database context using the <code>USE</code> statement.</li>



<li>Write the <code>CREATE PROCEDURE</code> statement with a clear, descriptive name.</li>



<li>Define any input parameters the procedure needs.</li>



<li>Write the T-SQL logic inside the <code>BEGIN...END</code> block.</li>



<li>Execute the statement to save the procedure into the database.</li>



<li>Test the procedure using <code>EXEC</code> with sample parameter values.</li>



<li>Verify the procedure appears under the Programmability folder in SSMS.</li>
</ol>



<p class="wp-block-paragraph">I never skip step six. I&#8217;ve seen too many developers assume a procedure works simply because it saved without an error, only to discover a logic mistake the first time someone actually runs it in production.</p>



<h3 class="wp-block-heading">Choosing the Right Schema and Naming Convention</h3>



<p class="wp-block-paragraph">Before you save any stored procedure, I strongly recommend deciding on a consistent naming convention, because a database with dozens or hundreds of procedures becomes unmanageable without one.</p>



<h4 class="wp-block-heading">Why Schema Matters</h4>



<p class="wp-block-paragraph">I always explicitly specify a schema, typically <code>dbo</code>, when creating a procedure. Leaving the schema unspecified can lead to ownership and permission confusion later, especially in larger organizations where multiple teams share a single database.</p>



<h4 class="wp-block-heading">Naming Conventions I Recommend</h4>



<p class="wp-block-paragraph">Over the years, I&#8217;ve settled into a naming pattern that keeps things predictable for anyone who inherits my code:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Prefix</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td>usp_</td><td>User stored procedure</td><td>usp_GetCustomerOrders</td></tr><tr><td>usp_Get</td><td>Retrieves data</td><td>usp_GetEmployeeById</td></tr><tr><td>usp_Insert</td><td>Adds new data</td><td>usp_InsertNewOrder</td></tr><tr><td>usp_Update</td><td>Modifies existing data</td><td>usp_UpdateCustomerAddress</td></tr><tr><td>usp_Delete</td><td>Removes data</td><td>usp_DeleteExpiredSessions</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">One rule I follow without exception: never prefix a stored procedure name with <code>sp_</code>. That prefix is reserved for SQL Server&#8217;s own system stored procedures, and using it on a user-defined procedure can cause SQL Server to search system objects first, adding a small but unnecessary performance cost every time it&#8217;s called.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">I once reviewed a legacy database for a client in Ohio where nearly every procedure was prefixed with <code>sp_</code>. It wasn&#8217;t causing a major performance crisis, but cleaning up that naming convention was one of the first recommendations I made, purely to avoid the unnecessary lookup overhead and the confusion it caused for new developers.</p>
</blockquote>



<h3 class="wp-block-heading">Saving Changes to an Existing Stored Procedure</h3>



<p class="wp-block-paragraph">Once a stored procedure already exists, you can&#8217;t use <code>CREATE PROCEDURE</code> again without either dropping it first or using a different approach. This is where many beginners get stuck.</p>



<h4 class="wp-block-heading">Using ALTER PROCEDURE</h4>



<p class="wp-block-paragraph">The traditional method is <code>ALTER PROCEDURE</code>, which modifies an existing procedure while preserving any permissions already granted on it.</p>



<pre class="wp-block-preformatted"><code>ALTER PROCEDURE dbo.usp_GetCustomerOrders<br>    @CustomerID INT<br>AS<br>BEGIN<br>    SELECT OrderID, OrderDate, TotalAmount, ShippingStatus<br>    FROM Orders<br>    WHERE CustomerID = @CustomerID;<br>END;</code></pre>



<p class="wp-block-paragraph">Notice the syntax is nearly identical to <code>CREATE PROCEDURE</code>. The only difference is the keyword itself. I use <code>ALTER</code> whenever I know for certain the procedure already exists and I simply need to update its logic.</p>



<h3 class="wp-block-heading">Using CREATE OR ALTER (My Preferred Method)</h3>



<p class="wp-block-paragraph">Since SQL Server 2016 Service Pack 1, I almost exclusively use <code>CREATE OR ALTER</code> instead of choosing between <code>CREATE</code> and <code>ALTER</code> manually.</p>



<pre class="wp-block-preformatted"><code>CREATE OR ALTER PROCEDURE dbo.usp_GetCustomerOrders<br>    @CustomerID INT<br>AS<br>BEGIN<br>    SELECT OrderID, OrderDate, TotalAmount, ShippingStatus<br>    FROM Orders<br>    WHERE CustomerID = @CustomerID;<br>END;</code></pre>



<p class="wp-block-paragraph">This single statement creates the procedure if it doesn&#8217;t exist yet, or alters it if it does, without requiring me to check first. I recommend this to every developer I train because it eliminates an entire category of deployment errors, particularly the old habit of writing a <code>DROP PROCEDURE IF EXISTS</code> followed by a fresh <code>CREATE PROCEDURE</code>. </p>



<p class="wp-block-paragraph">That older pattern technically works, but it destroys any permissions previously granted on the procedure, forcing you to reapply them manually every time you redeploy.</p>



<h3 class="wp-block-heading">Why I Avoid DROP and Recreate</h3>



<p class="wp-block-paragraph">I want to be direct about this because I still see it in production environments: dropping a procedure and recreating it from scratch is rarely the right approach for routine updates. Here&#8217;s why I steer clients away from it:</p>



<ul class="wp-block-list">
<li>Permissions granted with <code>GRANT EXECUTE</code> are lost the moment the procedure is dropped.</li>



<li>Any dependent objects or scripts referencing the procedure can briefly fail during the drop-and-recreate window.</li>



<li>It adds an unnecessary step compared to a single <code>CREATE OR ALTER</code> statement.</li>
</ul>



<h3 class="wp-block-heading">Verifying That Your Stored Procedure Was Saved</h3>



<p class="wp-block-paragraph">After executing a <code>CREATE PROCEDURE</code> or <code>CREATE OR ALTER PROCEDURE</code> statement, I always verify the save was successful before moving on.</p>



<h4 class="wp-block-heading">Checking in SQL Server Management Studio</h4>



<p class="wp-block-paragraph">In the Object Explorer panel, I navigate to Databases, then the specific database, then Programmability, then Stored Procedures, and refresh that folder. If the procedure appears there with the name I specified, I know it saved correctly.</p>



<h4 class="wp-block-heading">Checking with a Query</h4>



<p class="wp-block-paragraph">I also like confirming this programmatically, especially when working across multiple environments:</p>



<pre class="wp-block-preformatted"><code>SELECT name, create_date, modify_date<br>FROM sys.procedures<br>WHERE name = 'usp_GetCustomerOrders';</code></pre>



<p class="wp-block-paragraph">This query returns the procedure&#8217;s name along with its creation and last-modified timestamps, which is particularly useful when I need to confirm a deployment actually went through on a production server rather than just my local development instance.</p>



<h3 class="wp-block-heading">Common Mistakes to Avoid When Saving Stored Procedures</h3>



<p class="wp-block-paragraph">Based on years of code reviews and troubleshooting sessions with development teams across the country, these are the mistakes I see most often:</p>



<ul class="wp-block-list">
<li><strong>Forgetting the semicolon.</strong> While SQL Server is often forgiving about missing semicolons, T-SQL best practice, and future compatibility, requires terminating statements properly.</li>



<li><strong>Not specifying a schema.</strong> Always write <code>dbo.usp_ProcedureName</code> rather than just <code>usp_ProcedureName</code> to avoid ambiguity.</li>



<li><strong>Using SELECT * inside a procedure.</strong> This can break calling applications if the underlying table structure changes later.</li>



<li><strong>Skipping error handling.</strong> I always wrap data-modifying logic in a <code>TRY...CATCH</code> block so failures are handled gracefully rather than left to bubble up unpredictably.</li>



<li><strong>Not testing with edge-case parameters.</strong> A procedure that works with a valid customer ID needs to be tested with an invalid one too, before you consider the save process complete.</li>
</ul>



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



<p class="wp-block-paragraph"><strong>Do I need special permissions to save a stored procedure?</strong><br>Yes. You need <code>CREATE PROCEDURE</code> permission in the database, or you need to be a member of a role like <code>db_ddladmin</code> or <code>db_owner</code>. Without this permission, SQL Server will return an error when you try to execute the <code>CREATE PROCEDURE</code> statement.</p>



<p class="wp-block-paragraph"><strong>Can I save a stored procedure without giving it a name?</strong><br>No. Every stored procedure requires a unique name within its schema. SQL Server uses this name to store, locate, and execute the procedure later.</p>



<p class="wp-block-paragraph"><strong>What happens if I try to create a procedure that already exists?</strong><br>SQL Server will return an error stating that an object with that name already exists, unless you use <code>CREATE OR ALTER</code>, which handles both scenarios automatically.</p>



<p class="wp-block-paragraph"><strong>Is saving a stored procedure the same as saving my SQL script file?</strong><br>No, and this trips up a lot of beginners. Saving your <code>.sql</code> script file to your computer only preserves your code locally. The procedure itself is only saved into the database once you execute the <code>CREATE PROCEDURE</code> or <code>CREATE OR ALTER PROCEDURE</code> statement against that database.</p>



<h2 class="wp-block-heading">Final Thoughts</h2>



<p class="wp-block-paragraph">Saving a stored procedure in SQL Server ultimately comes down to executing the right T-SQL statement against the correct database, whether that&#8217;s <code>CREATE PROCEDURE</code> for something brand new or <code>CREATE OR ALTER PROCEDURE</code> for updating existing logic without losing permissions. </p>



<p class="wp-block-paragraph">I&#8217;ve found that developers who internalize this distinction early, rather than thinking of it like saving a document, avoid a whole category of deployment headaches later in their careers.</p>



<p class="wp-block-paragraph">You may also like the following articles:</p>



<ul class="wp-block-list">
<li><a href="https://sqlserverguides.com/how-to-execute-function-in-sql-server-with-parameters/" target="_blank" rel="noreferrer noopener">How to Execute Function in SQL Server with Parameters</a></li>



<li><a href="https://sqlserverguides.com/how-to-insert-into-table-from-stored-procedure-with-parameters/" target="_blank" rel="noreferrer noopener">How to Insert into Table from Stored Procedure with Parameters</a></li>



<li><a href="https://sqlserverguides.com/get-stored-procedure-list-in-sql-server-by-modified-date/" target="_blank" rel="noreferrer noopener">Get Stored Procedure List in SQL Server by Modified Date</a></li>



<li><a href="https://sqlserverguides.com/create-stored-procedure-in-sql-server/" target="_blank" rel="noreferrer noopener">Create Stored Procedure in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/how-to-find-second-highest-salary-in-sql-server/" target="_blank" rel="noreferrer noopener">How to Find Second Highest Salary in SQL Server</a></li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Stored Procedure vs View</title>
		<link>https://sqlserverguides.com/stored-procedure-vs-view/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Fri, 18 Sep 2026 11:45:17 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Stored Procedures]]></category>
		<category><![CDATA[Stored Procedure vs View]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=23825</guid>

					<description><![CDATA[When I first started managing databases for a company in Dallas, my team lead asked me to &#8220;just create a view instead of a procedure&#8221; for a reporting request. I remember pausing, because at that point I genuinely wasn&#8217;t sure why it mattered. Years later, after building and maintaining dozens of production databases, I can ... <a title="Stored Procedure vs View" class="read-more" href="https://sqlserverguides.com/stored-procedure-vs-view/" aria-label="Read more about Stored Procedure vs View">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">When I first started managing databases for a company in Dallas, my team lead asked me to &#8220;just create a <a href="https://sqlserverguides.com/sql-server-views/" target="_blank" rel="noreferrer noopener">view </a>instead of a procedure&#8221; for a reporting request. I remember pausing, because at that point I genuinely wasn&#8217;t sure why it mattered. </p>



<p class="wp-block-paragraph">Years later, after building and maintaining dozens of production databases, I can tell you that the <strong>stored procedure vs view</strong> decision is one of the most fundamental choices you&#8217;ll make as a SQL developer, and getting it wrong quietly costs you performance, security, and maintainability down the road.</p>



<p class="wp-block-paragraph">In this guide, I&#8217;m going to walk you through exactly what separates a stored procedure from a view, when to use each one, and the practical reasoning I use every time I sit down to design a database object. I write this from firsthand experience working with SQL Server across several U.S.-based teams, so expect straight answers, not textbook fluff.</p>



<h2 class="wp-block-heading">Stored Procedure vs View</h2>



<h3 class="wp-block-heading">What Is a View in SQL Server?</h3>



<p class="wp-block-paragraph">A&nbsp;<strong>view</strong>&nbsp;is a virtual table built from the result of a&nbsp;<code>SELECT</code>&nbsp;statement. It doesn&#8217;t store data on its own — it stores the query definition, and every time you call the view, SQL Server runs that underlying query against the real tables.</p>



<p class="wp-block-paragraph">I think of a view as a window into your data. When my colleague Sarah Mitchell needed a simplified way to look at &#8220;active customers only&#8221; without repeatedly writing the same three-table join, I built her a view. She could then query that view just like it was a regular table, without knowing or caring about the joins happening behind the scenes.</p>



<p class="wp-block-paragraph">Here&#8217;s what defines a view at its core:</p>



<ul class="wp-block-list">
<li>It&#8217;s based on a single <code>SELECT</code> statement.</li>



<li>It doesn&#8217;t accept parameters.</li>



<li>It can be queried, joined, and filtered just like a table.</li>



<li>It doesn&#8217;t physically store data unless you create an <strong>indexed view</strong> (also called a materialized view in other database systems).</li>



<li>It&#8217;s primarily read-oriented, though updatable views exist under specific conditions.</li>
</ul>



<h4 class="wp-block-heading">Why I Use Views</h4>



<p class="wp-block-paragraph">I reach for a view whenever I need to simplify a complex query or restrict what columns and rows a user can see. If David Chen on my reporting team only needs order totals and customer names — not payment details or internal notes — I build a view that exposes exactly those columns. This becomes a security layer as much as a convenience feature, since I can grant SELECT access to the view without exposing the entire underlying table.</p>



<h3 class="wp-block-heading">What Is a Stored Procedure in SQL Server?</h3>



<p class="wp-block-paragraph">A&nbsp;<strong>stored procedure</strong>&nbsp;is a precompiled collection of one or more SQL statements stored in the database and executed as a single unit. Unlike a view, a stored procedure can accept input parameters, return output parameters, run conditional logic, loop through operations, and perform inserts, updates, and deletes.</p>



<p class="wp-block-paragraph">When Jennifer Adams on my team needed to process monthly billing adjustments — validating data, updating multiple tables, and logging every change — a view couldn&#8217;t handle that. I wrote a stored procedure instead, because the task involved actual business logic, not just data retrieval.</p>



<p class="wp-block-paragraph">Key characteristics of a stored procedure:</p>



<ul class="wp-block-list">
<li>Accepts input and output parameters.</li>



<li>Can contain multiple SQL statements, including <code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code>, and <code>SELECT</code>.</li>



<li>Supports control-of-flow logic like <code>IF</code>, <code>WHILE</code>, and <code>TRY...CATCH</code>.</li>



<li>Can manage transactions explicitly.</li>



<li>Executes as a compiled, callable unit rather than being embedded inside another query.</li>
</ul>



<h4 class="wp-block-heading">Why I Use Stored Procedures</h4>



<p class="wp-block-paragraph">I use stored procedures whenever a task involves more than just reading data. If I need to validate input, enforce business rules, wrap multiple statements in a transaction, or perform any kind of data modification, a stored procedure is the only correct tool. </p>



<p class="wp-block-paragraph">It also gives me a controlled entry point — instead of letting an application send raw <code>UPDATE</code> statements to my database, I expose a procedure that only allows changes through approved logic.</p>



<h3 class="wp-block-heading">Stored Procedure vs View: Side-by-Side Comparison</h3>



<p class="wp-block-paragraph">I find that most confusion clears up once you see the two side by side. Here&#8217;s the comparison table I wish someone had shown me on day one.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Feature</th><th class="has-text-align-left" data-align="left">View</th><th class="has-text-align-left" data-align="left">Stored Procedure</th></tr></thead><tbody><tr><td>Core purpose</td><td>Simplify and present data</td><td>Execute logic and operations</td></tr><tr><td>Accepts parameters</td><td>No</td><td>Yes</td></tr><tr><td>Can modify data (INSERT/UPDATE/DELETE)</td><td>Generally no (limited exceptions)</td><td>Yes</td></tr><tr><td>Can be used inside another query (JOIN, WHERE)</td><td>Yes</td><td>No</td></tr><tr><td>Supports control-of-flow logic (IF, WHILE, loops)</td><td>No</td><td>Yes</td></tr><tr><td>Can return multiple result sets</td><td>No</td><td>Yes</td></tr><tr><td>Stores an execution plan</td><td>No (plan belongs to the calling query)</td><td>Yes, cached after first execution</td></tr><tr><td>Can call other procedures</td><td>No</td><td>Yes</td></tr><tr><td>Primary use case</td><td>Reporting, simplified reads, access control</td><td>Business logic, data changes, automation</td></tr><tr><td>Transaction control</td><td>Not applicable</td><td>Yes, full support</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">I keep coming back to one simple rule when I explain this to junior developers on my team: a view answers &#8220;what does this data look like,&#8221; while a stored procedure answers &#8220;what should happen with this data.&#8221;</p>



<h3 class="wp-block-heading">Performance Differences I&#8217;ve Actually Observed</h3>



<p class="wp-block-paragraph">People ask me constantly which one performs better, and the honest answer is: it depends on what you&#8217;re doing with it. I&#8217;ve run both in production long enough to have some real opinions here.</p>



<p class="wp-block-paragraph">A view itself doesn&#8217;t have its own stored execution plan — when you query a view, SQL Server folds its definition into the calling query and optimizes the whole thing together. </p>



<p class="wp-block-paragraph">That means a poorly written view with non-sargable conditions (conditions that prevent SQL Server from using an index efficiently) can silently degrade performance across every query that touches it. I&#8217;ve seen a single badly designed view slow down five different reports because nobody realized the view itself was the bottleneck.</p>



<p class="wp-block-paragraph">A stored procedure, on the other hand, gets its execution plan compiled and cached the first time it runs. Subsequent calls reuse that plan, which usually means faster and more predictable execution for repeated operations. This caching becomes especially valuable for procedures that run frequently, like an order-processing routine that fires hundreds of times a day.</p>



<p class="wp-block-paragraph">My practical takeaways:</p>



<ul class="wp-block-list">
<li><strong>For simple SELECT reporting</strong>, a well-written view performs just fine and keeps your queries clean.</li>



<li><strong>For repeated, parameter-driven operations</strong>, a stored procedure&#8217;s cached execution plan usually wins.</li>



<li><strong>For complex joins queried often</strong>, test both — sometimes an indexed view outperforms a procedure for read-heavy workloads.</li>



<li><strong>Never assume</strong> one is universally faster; I always test with realistic data volumes before making that call.</li>
</ul>



<h3 class="wp-block-heading">Security and Access Control Considerations</h3>



<p class="wp-block-paragraph">I lean on both objects differently when it comes to security, and understanding the distinction has saved me from a few uncomfortable audit conversations.</p>



<p class="wp-block-paragraph">With a view, I control access by exposing only the columns and rows a user needs. If our HR contact, Patricia Williams, needed employee directory data but not salary information, I built a view that simply excluded the salary column. Granting SELECT permission on that view means she never touches the underlying table directly.</p>



<p class="wp-block-paragraph">With a stored procedure, I control access to actions, not just data. If I want to allow an application to update customer addresses but never let it run an arbitrary&nbsp;<code>UPDATE</code>&nbsp;statement against the customers table, I wrap that logic inside a procedure and grant EXECUTE permission only on the procedure itself. The application never gets direct table access at all.</p>



<p class="wp-block-paragraph">A few security habits I always follow:</p>



<ul class="wp-block-list">
<li>Grant permissions on the view or procedure, not on the base tables, whenever possible.</li>



<li>Use stored procedures as the only path for data modification in sensitive tables.</li>



<li>Avoid embedding dynamic SQL inside procedures unless you&#8217;re validating and parameterizing inputs carefully, since that&#8217;s a common SQL injection risk.</li>



<li>Review view definitions periodically, since a view referencing a table that changed structure can quietly break or expose unintended columns.</li>
</ul>



<h3 class="wp-block-heading">When to Use a View vs a Stored Procedure</h3>



<p class="wp-block-paragraph">I get asked this in almost every code review, so here&#8217;s the decision process I actually use.</p>



<p class="wp-block-paragraph"><strong>Choose a view when:</strong></p>



<ul class="wp-block-list">
<li>You need to simplify a complex join for repeated use.</li>



<li>You&#8217;re building a reporting layer that only reads data.</li>



<li>You want to restrict visible columns or rows for specific users.</li>



<li>The result needs to be joined with other tables or views in a larger query.</li>
</ul>



<p class="wp-block-paragraph"><strong>Choose a stored procedure when:</strong></p>



<ul class="wp-block-list">
<li>You need to insert, update, or delete data.</li>



<li>The logic involves conditions, loops, or multiple steps.</li>



<li>You need to accept parameters to control the operation&#8217;s behavior.</li>



<li>You want transaction control to ensure multiple changes succeed or fail together.</li>



<li>You&#8217;re building a reusable operation that an application will call directly.</li>
</ul>



<h3 class="wp-block-heading">Common Mistakes I See Teams Make</h3>



<p class="wp-block-paragraph">Over the years, I&#8217;ve noticed the same handful of mistakes repeat across different teams and companies.</p>



<ul class="wp-block-list">
<li><strong>Using a view where a procedure was needed</strong>, then trying to force data modifications through triggers on the view, which adds unnecessary complexity.</li>



<li><strong>Stacking views on top of views</strong> several layers deep, which makes performance tuning nearly impossible because nobody can trace the actual execution path.</li>



<li><strong>Writing stored procedures that only do a SELECT</strong>, when a simple view would have been easier to maintain and reuse in other queries.</li>



<li><strong>Skipping parameterization in procedures</strong>, opening the door to SQL injection when dynamic SQL gets built from raw string concatenation.</li>



<li><strong>Forgetting to document ownership</strong>, so nobody remembers whether a given report depends on a view, a procedure, or both.</li>
</ul>



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



<h4 class="wp-block-heading">Can a view accept parameters like a stored procedure?</h4>



<p class="wp-block-paragraph">No, a standard SQL Server view cannot accept parameters directly. If you need parameter-driven filtering, you either use a stored procedure or an inline table-valued function, which behaves more like a parameterized view.</p>



<h4 class="wp-block-heading">Can a stored procedure be used inside a SELECT statement?</h4>



<p class="wp-block-paragraph">Generally, no. A stored procedure is called and executed as its own statement, while a view can be embedded directly inside a&nbsp;<code>SELECT</code>,&nbsp;<code>JOIN</code>, or&nbsp;<code>WHERE</code>&nbsp;clause just like a table.</p>



<h4 class="wp-block-heading">Which one is better for security?</h4>



<p class="wp-block-paragraph">Both serve security purposes differently. Views restrict visible columns and rows for read access, while stored procedures restrict and control what data modifications are allowed, so most well-designed databases use both together.</p>



<h4 class="wp-block-heading">Is a stored procedure always faster than a view?</h4>



<p class="wp-block-paragraph">Not always. Stored procedures benefit from cached execution plans for repeated operations, but a well-optimized view used in a read-heavy reporting scenario can perform just as well or better, depending on the query pattern.</p>



<h4 class="wp-block-heading">Can I update data through a view?</h4>



<p class="wp-block-paragraph">In limited cases, yes, if the view is based on a single table and meets specific SQL Server requirements. For anything involving multiple tables or complex logic, a stored procedure is the more reliable and maintainable choice.</p>



<h2 class="wp-block-heading">Final Thoughts</h2>



<p class="wp-block-paragraph">After years of building database layers for different teams, my rule of thumb stays simple: I use a view when I need to present or simplify data, and I use a stored procedure when I need to act on data. Both objects exist for different reasons, and treating them as interchangeable is where most performance and maintenance headaches begin. Once you internalize that distinction, choosing between them stops being a guessing game and becomes second nature.</p>



<p class="wp-block-paragraph">You may also like the following articles:</p>



<ul class="wp-block-list">
<li><a href="https://sqlserverguides.com/create-stored-procedure-in-sql-server/" target="_blank" rel="noreferrer noopener">Create Stored Procedure in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/how-to-view-stored-procedures-in-sql-server/" target="_blank" rel="noreferrer noopener">How to View Stored Procedures in SQL Server</a></li>



<li><a href="https://sqlserverguides.com/temp-table-vs-view-in-sql-server/" target="_blank" rel="noreferrer noopener">Temp Table vs View in SQL Server</a></li>
</ul>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>SQL Server Reporting Services</title>
		<link>https://sqlserverguides.com/sql-server-reporting-services/</link>
		
		<dc:creator><![CDATA[Bijay Kumar Sahoo]]></dc:creator>
		<pubDate>Wed, 16 Sep 2026 06:55:40 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[SQL Server Reporting Services]]></category>
		<guid isPermaLink="false">https://sqlserverguides.com/?p=23816</guid>

					<description><![CDATA[I&#8217;ve lost count of how many times a client in Denver or Atlanta has handed me a pile of spreadsheets and asked, &#8220;Can you turn this into something our executives can actually look at every Monday morning?&#8221; That&#8217;s the exact problem SQL Server Reporting Services was designed to solve. If you&#8217;re trying to understand what ... <a title="SQL Server Reporting Services" class="read-more" href="https://sqlserverguides.com/sql-server-reporting-services/" aria-label="Read more about SQL Server Reporting Services">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I&#8217;ve lost count of how many times a client in Denver or Atlanta has handed me a pile of spreadsheets and asked, &#8220;Can you turn this into something our executives can actually look at every Monday morning?&#8221; That&#8217;s the exact problem <strong>SQL Server Reporting Services</strong> was designed to solve. If you&#8217;re trying to understand what SSRS does and whether it fits your organization&#8217;s reporting needs, you&#8217;re in the right place.</p>



<p class="wp-block-paragraph">In this guide, I&#8217;ll walk through what SQL Server Reporting Services is, how its architecture works, the types of reports it supports, and where it fits in a modern data environment. I&#8217;ve spent years designing and deploying reporting solutions for organizations across the country, so I&#8217;ll give you the practical, authority-driven view rather than a marketing pitch.</p>



<h2 class="wp-block-heading">SQL Server Reporting Services</h2>



<h3 class="wp-block-heading">What Is SQL Server Reporting Services?</h3>



<p class="wp-block-paragraph">SQL Server Reporting Services, known as <strong>SSRS</strong>, is a server-based reporting platform included with SQL Server that lets organizations create, deploy, and manage paginated reports, mobile reports, and KPIs. It&#8217;s a middle-tier reporting engine, meaning it sits between your raw data sources and the people who need to consume that data in a readable, formatted way.</p>



<p class="wp-block-paragraph">I describe SSRS to clients as the printing press of the Microsoft data stack. It doesn&#8217;t store your business data; it pulls data from wherever it lives, formats it according to a report definition, and delivers it through a web portal, email subscription, file share, or embedded application. </p>



<p class="wp-block-paragraph">A report server can connect to SQL Server&#8217;s relational database engine, SQL Server Analysis Services, or any other data source with an ADO.NET, OLE DB, or ODBC provider, which gives it flexibility beyond just SQL Server data.</p>



<p class="wp-block-paragraph">Organizations from insurance firms in Hartford to manufacturing companies in Detroit still rely on SSRS because it delivers highly formatted, print-ready reports that dashboards alone often can&#8217;t replicate, especially for regulatory or financial reporting where exact layout matters.</p>



<h3 class="wp-block-heading">Core Capabilities of SSRS</h3>



<p class="wp-block-paragraph">Understanding what SSRS actually does day to day makes it easier to decide whether it&#8217;s the right tool for your reporting needs.</p>



<h4 class="wp-block-heading">Building Paginated Reports</h4>



<p class="wp-block-paragraph"><strong>Paginated reports</strong> are SSRS&#8217;s signature output. These are highly formatted reports designed to look correct whether viewed on screen or printed on paper, with defined page breaks, headers, footers, and precise layout control. I use these constantly for financial statements, invoices, and compliance reports where the exact placement of every column matters.</p>



<h4 class="wp-block-heading">Supporting Multiple Data Sources</h4>



<p class="wp-block-paragraph">SSRS can pull from a wide range of data providers, which makes it useful even in organizations running mixed database environments. Typical connections include:</p>



<ul class="wp-block-list">
<li>SQL Server relational databases</li>



<li>SQL Server Analysis Services cubes</li>



<li>Oracle databases</li>



<li>Any ADO.NET, OLE DB, or ODBC-compliant source</li>
</ul>



<h4 class="wp-block-heading">Delivering Reports Through Multiple Channels</h4>



<p class="wp-block-paragraph">A report built once in SSRS doesn&#8217;t have to be viewed just one way. It can be delivered through several channels depending on who needs it and how they prefer to consume it:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Delivery Method</th><th>Typical Use Case</th></tr></thead><tbody><tr><td>Web portal</td><td>Interactive viewing with drill-down and parameters</td></tr><tr><td>Email subscription</td><td>Automatic delivery to executives on a schedule</td></tr><tr><td>File share</td><td>Archiving reports for compliance or audit needs</td></tr><tr><td>SharePoint integration</td><td>Centralized access alongside other business documents</td></tr><tr><td>Export formats (PDF, Excel, CSV, Word, XML)</td><td>Sharing with people outside the reporting platform</td></tr></tbody></table></figure>



<h4 class="wp-block-heading">Parameterized and Ad Hoc Reporting</h4>



<p class="wp-block-paragraph">SSRS supports <strong>parameterized reports</strong>, which let a single report definition run with different filters, such as date ranges, regions, or customer accounts, without building a separate report for every variation. </p>



<p class="wp-block-paragraph">For business users who need more flexibility, <strong>Report Builder</strong> provides a self-service tool for creating ad hoc reports without needing developer involvement for every request.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">In my experience, parameterized reports are the single biggest time-saver in any SSRS deployment. I&#8217;ve replaced dozens of nearly identical report files with one well-built parameterized report, cutting maintenance work dramatically.</p>
</blockquote>



<h3 class="wp-block-heading">SSRS Architecture Explained</h3>



<p class="wp-block-paragraph">Understanding the architecture behind SSRS makes troubleshooting and scaling decisions far easier. I break it into three core pieces whenever I&#8217;m onboarding a new report developer.</p>



<h4 class="wp-block-heading">The Report Server</h4>



<p class="wp-block-paragraph">The <strong>Report Server</strong> is the engine at the center of SSRS. It handles every client request, whether that&#8217;s rendering a report, running a scheduled subscription, or processing a management task like creating a new data source. Think of it as the conductor coordinating every moving part of the platform.</p>



<h4 class="wp-block-heading">The Report Server Databases</h4>



<p class="wp-block-paragraph">Every SSRS deployment relies on two SQL Server databases:</p>



<ul class="wp-block-list">
<li><strong>ReportServer</strong> — stores report definitions, configuration settings, security permissions, and execution history</li>



<li><strong>ReportServerTempDB</strong> — functions as a temporary workspace during report processing, similar to how tempdb works for the SQL Server engine itself</li>
</ul>



<p class="wp-block-paragraph">These databases must live on a SQL Server instance, which is why SSRS is always tied closely to the broader SQL Server ecosystem even though it functions as a separate service.</p>



<h4 class="wp-block-heading">Report Development Tools</h4>



<p class="wp-block-paragraph">Reports are typically built using one of these tools, depending on who&#8217;s building them and how much flexibility they need:</p>



<ul class="wp-block-list">
<li><strong>SQL Server Data Tools (SSDT)</strong> — used by developers for structured, version-controlled report development</li>



<li><strong>Report Builder</strong> — a more approachable, standalone tool aimed at business analysts building ad hoc reports</li>



<li><strong>Report Designer</strong> — an older design surface, still found in some legacy environments</li>
</ul>



<h4 class="wp-block-heading">Deployment Models</h4>



<p class="wp-block-paragraph">SSRS can be deployed in a few different configurations depending on organizational needs:</p>



<ul class="wp-block-list">
<li><strong>Native mode</strong> — a stand-alone report server with its own web portal</li>



<li><strong>SharePoint-integrated mode</strong> — reports are managed and accessed through a SharePoint site</li>



<li><strong>Scale-out deployment</strong> — multiple report servers share a single set of databases to handle higher load</li>
</ul>



<h3 class="wp-block-heading">Types of Reports You Can Build</h3>



<p class="wp-block-paragraph">SSRS isn&#8217;t limited to one style of report. Over the years, I&#8217;ve built nearly every type it supports, and each one fits a different business need.</p>



<ul class="wp-block-list">
<li><strong>Tabular reports</strong> — straightforward row-and-column layouts, ideal for detailed transaction listings</li>



<li><strong>Matrix reports</strong> — cross-tabulated data similar to a pivot table, useful for summarizing sales by region and product</li>



<li><strong>Chart reports</strong> — visual representations like bar charts and line graphs layered into a report</li>



<li><strong>Free-form reports</strong> — flexible layouts combining text, images, and data regions for things like customer statements</li>



<li><strong>Mobile reports</strong> — responsive layouts that adjust to different screen sizes and orientations</li>



<li><strong>Subreports</strong> — smaller reports embedded within a larger parent report for layered detail</li>
</ul>



<h3 class="wp-block-heading">Security and Permissions in SSRS</h3>



<p class="wp-block-paragraph">Reporting platforms sit close to sensitive business data, so access control deserves real attention. SSRS uses a role-based permission model applied at the folder and item level within the report server catalog. </p>



<p class="wp-block-paragraph">This means you can grant a regional sales manager in Phoenix access to only their region&#8217;s reports, while giving a finance director in New York broader visibility across the organization.</p>



<p class="wp-block-paragraph">Permissions are typically managed through roles like:</p>



<ul class="wp-block-list">
<li><strong>Browser</strong> — can view and run reports, but not modify them</li>



<li><strong>Content Manager</strong> — can publish, edit, and manage report content</li>



<li><strong>Report Builder</strong> — can create and edit ad hoc reports through Report BuilderI always recommend starting with the most restrictive role that still lets someone do their job, then expanding access only when there&#8217;s a clear business need. I&#8217;ve seen too many organizations grant broad Content Manager access by default, which makes it far too easy for someone to accidentally overwrite a production report.</li>
</ul>



<h3 class="wp-block-heading">SSRS vs. Modern BI Tools: Where It Still Fits</h3>



<p class="wp-block-paragraph">A question I get constantly from many is whether SSRS is still relevant now that interactive BI tools are everywhere. My honest answer: it depends on what you&#8217;re building.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Factor</th><th>SSRS</th><th>Interactive BI Tools</th></tr></thead><tbody><tr><td>Best for</td><td>Fixed-format, print-ready reports</td><td>Interactive dashboards and exploration</td></tr><tr><td>Report precision</td><td>Exact pixel-level layout control</td><td>Flexible but less print-precise</td></tr><tr><td>Typical use case</td><td>Invoices, regulatory filings, financial statements</td><td>Trend analysis, executive dashboards</td></tr><tr><td>Self-service capability</td><td>Limited, improved through Report Builder</td><td>Generally stronger for business users</td></tr><tr><td>Integration</td><td>Deep native SQL Server integration</td><td>Varies by platform</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">SSRS still wins when the output needs to look exactly the same every time, especially for anything with legal, financial, or compliance implications. Interactive BI tools tend to win when the goal is exploration and visual analysis rather than a fixed, formatted document.</p>



<h3 class="wp-block-heading">Common Challenges to Plan For</h3>



<p class="wp-block-paragraph">Every reporting platform has friction points, and SSRS is no exception. Here&#8217;s what I flag for teams before they commit to a large-scale deployment.</p>



<ul class="wp-block-list">
<li><strong>Report sprawl.</strong> Without governance, organizations accumulate hundreds of near-duplicate reports over time, making maintenance difficult.</li>



<li><strong>Performance on large datasets.</strong> Poorly optimized queries behind a report can slow rendering significantly, especially with complex parameterized reports.</li>



<li><strong>Version control gaps.</strong> Reports built directly through Report Builder without a structured deployment process can bypass standard change management.</li>



<li><strong>Skill dependency.</strong> Well-designed reports still require someone with strong SQL and report design skills; self-service tools reduce but don&#8217;t eliminate this need.</li>
</ul>



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



<h4 class="wp-block-heading">What is SQL Server Reporting Services used for?</h4>



<p class="wp-block-paragraph">SSRS is used to create, manage, and deliver formatted business reports, including paginated reports, KPIs, and mobile reports. It&#8217;s commonly used for financial statements, compliance reporting, and any scenario requiring precise, print-ready formatting.</p>



<h4 class="wp-block-heading">Is SSRS free with SQL Server?</h4>



<p class="wp-block-paragraph">SSRS is included with most SQL Server licenses, though the exact features and scale-out capabilities available depend on the specific SQL Server edition. It&#8217;s worth checking the licensing terms for your organization&#8217;s SQL Server edition before planning a large deployment.</p>



<h4 class="wp-block-heading">What is the difference between SSRS and Report Builder?</h4>



<p class="wp-block-paragraph">SSRS is the overall reporting platform, including the report server, databases, and web portal. Report Builder is one of the tools within that platform, designed for business users to create ad hoc reports without needing full developer involvement.</p>



<h4 class="wp-block-heading">Can SSRS connect to non-Microsoft databases?</h4>



<p class="wp-block-paragraph">Yes. SSRS can connect to any data source with an ADO.NET, OLE DB, or ODBC provider, which includes many non-Microsoft databases like Oracle. This makes it usable even in mixed-database environments, not just pure SQL Server shops.</p>



<h4 class="wp-block-heading">Is SSRS still relevant compared to modern BI platforms?</h4>



<p class="wp-block-paragraph">Yes, particularly for fixed-format, print-precise reporting like invoices and regulatory filings where interactive dashboards fall short. Many organizations run SSRS alongside modern BI tools rather than choosing one exclusively.</p>



<p class="wp-block-paragraph">SQL Server Reporting Services remains one of the most dependable platforms for structured, formatted business reporting, especially where exact layout and reliable delivery matter more than interactive exploration. The real value comes from disciplined report governance, thoughtful permission design, and knowing when SSRS is the right tool versus when a modern BI platform fits better. I hope you found this article helpful.</p>



<p class="wp-block-paragraph">You may also like the following articles:</p>



<ul class="wp-block-list">
<li><a href="https://sqlserverguides.com/what-do-sql-server-integration-services-do/" target="_blank" rel="noreferrer noopener">What Do SQL Server Integration Services Do?</a></li>



<li><a href="https://sqlserverguides.com/ssis-vs-ssms/" target="_blank" rel="noreferrer noopener">SSIS vs SSMS</a></li>



<li><a href="https://sqlserverguides.com/sql-server-views/" target="_blank" rel="noreferrer noopener">SQL Server Views</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-10-16 13:05:14 by W3 Total Cache
-->