SUBSTRING Boundaries: Start at Zero, One or Beyond

I test SUBSTRING boundaries before using a calculated position. SQL Server numbers the first character as one. A start of zero doesn’t behave like one, even when the requested length looks reasonable.

A complete wooden garden gate with one sage-coloured slat beside a woodworking plane.
A wooden garden gate with one sage-colored slat.

Start with the ordinary slice

The first row requests two characters starting at position one. Its expected result is ab. This supplies a familiar baseline before the example changes the start position or the requested count.

I keep the input text and both numeric arguments beside the result. A small slice alone can hide why it was returned. Those extra columns make the boundary examples easier to compare without mentally reconstructing several nearly identical expressions.

WITH Cases AS
(
 SELECT CaseId, CAST(InputText AS nvarchar(12)) AS InputText,
        StartAt, TakeLength
 FROM (VALUES
 (1,N'abcde',1,2),(2,N'abcde',0,2),(3,N'abcde',0,1),
 (4,N'abcde',-1,3),(5,N'abcde',6,2),(6,N'abcde',2,0),
 (7,CAST(NULL AS nvarchar(12)),1,2)) v(CaseId,InputText,StartAt,TakeLength)
)
SELECT CaseId, InputText, StartAt, TakeLength,
       CAST(SUBSTRING(InputText,StartAt,TakeLength) AS nvarchar(40)) AS Slice
FROM Cases
ORDER BY CaseId;
Native SSMS results showing SUBSTRING behavior for zero and negative starts, out-of-range starts, zero length and NULL input.
Native SSMS results for all seven cases. A start below 1 affects how many characters are returned. The third, fifth and sixth cases return an empty string, while NULL input returns NULL. Open the result at full size.

Understand a start below one

When the start is below one, SUBSTRING begins at the first available character. Its returned count is reduced by the portion requested before that position. The count returned is the larger of start plus length minus one, or zero.

The second row therefore returns a rather than ab. The third requests a start of zero and length one, so nothing remains to return. Zero doesn’t become an alternate spelling of the first valid character position.

Test the edges first

Keep negative starts separate from negative lengths

The fourth row uses start minus one with length three. Its expected result is a because the requested range reaches the first available character. This is different from supplying a negative length.

A negative length raises an error and terminates the statement. The demonstration avoids that error-producing input. I’d reject an invalid calculated length before using it. Starts below one need separate tests if the application’s rules allow them.

Read empty results deliberately

A start beyond the end returns an empty string. A valid start with zero requested length also returns an empty string. The fifth and sixth rows retain both reasons instead of collapsing them into one vague blank case.

A blank native grid cell can be easy to miss. I’d retain the case number and its input arguments during review. If the display needs a visible label for empty text, add that label separately without changing the Slice column’s value.

Preserve the missing-input case

The seventh row supplies typed NULL input and returns NULL. That isn’t the same value as either empty result. The outer cast gives every Slice an explicit nvarchar(40) result contract.

I don’t replace missing text with an empty string unless the business contract asks for that default. The substitution would erase a distinction that this example is trying to preserve. A downstream reader should decide how to display or reject the missing value.

Specify length for a portable example

This demonstration supplies all three arguments. SQL Server 2022 and earlier require the length argument. SQL Server 2025 supports an omitted length, but that is a separate syntax choice with a version requirement.

I’d keep an explicit length when sharing a boundary test across those versions. The focus here is the requested range, not shorthand syntax. If a production expression omits length, its minimum supported engine version should remain visible in the surrounding documentation.

Review the input type and counting rules

These examples use short Unicode text and ordinary characters. Binary inputs count bytes instead of characters. Supplementary-character collations also affect how surrogate pairs are counted.

I’d test representative text before reusing an offset from another encoding or binary format. A byte position isn’t automatically a character position. The query demonstrates these boundaries for its typed inputs. A real parser still needs a complete input-format contract.

Test the edges once, and the middle takes care of itself.

A calculated position is not a valid slice by itself, it is an argument that needs boundary checks.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

SQL Function, SQL Scripts, SQL String
Previous Post
SWITCHOFFSET Versus TODATETIMEOFFSET: Two Offset Operations
Next Post
SQL SERVER – 2008 – TRIM() Function – User Defined Function

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.