varchar(255) or varchar(256)?

Size each and every column appropriately. Do NOT use a "standard" size for each column. If you only need 30 characters, why create a column that can handle 255? I'm so glad you're not advocating using varchar(max) for your string columns.

This is especially prudent advice if you ever need to index a column, or if you are using a column as a primary key and it has foreign key references. SQL Server uses the size of each column in it's query optimizer to understand estimated memory requirements for query processing. Having oversized columns can be detrimental to performance.

Indexes on columns that are oversize can result in errors being generated:

CREATE TABLE dbo.WideIndex
(
    col1 varchar(255) NOT NULL
    , col2 varchar(255) NOT NULL
    , col3 varchar(600) NOT NULL    
);

CREATE INDEX IX_WideIndex_01
ON dbo.WideIndex (col1, col2, col3);

The attempt to create the index above results in this warning:

Warning! The maximum key length is 900 bytes. The index 'IX_WideIndex_01' has maximum length of 1110 bytes. For some combination of large values, the insert/update operation will fail.

900 bytes is the maximum key size for clustered indexes (and non-clustered indexes on SQL Server 2012 and older). 1700 bytes is the maximum key size for non-clustered indexes on newer versions of SQL Server. If you design columns with a generic width, such as (255), you may run into this warning far more often than expected.

In case you are interested in storage internals, you can use the following tiny test to better understand how SQL Server stores uncompressed row-store data.

First, we'll create a table where we can store columns of various sizes:

IF OBJECT_ID(N'dbo.varchartest', N'U') IS NOT NULL
DROP TABLE dbo.varchartest;
GO

CREATE TABLE dbo.varchartest
(
    varchar30 varchar(30) NOT NULL
    , varchar255 varchar(255) NOT NULL
    , varchar256 varchar(256) NOT NULL
);

Now we'll insert a single row:

INSERT INTO dbo.varchartest (varchar30, varchar255, varchar256)
VALUES (REPLICATE('1', 30), REPLICATE('2', 255), REPLICATE('3', 256));

This query uses the undocumented, and unsupported, functions sys.fn_RowDumpCracker and sys.fn_PhyslocCracker to show some interesting details about the table:

SELECT rdc.*
    , plc.*
FROM dbo.varchartest vct
CROSS APPLY  sys.fn_RowDumpCracker(%%rowdump%%) rdc
CROSS APPLY sys.fn_physlocCracker(%%physloc%%) plc

Output will look similar to this:

╔═════════════════════╦════════════╦═════════╦══════════╦══════════════════════════╦══════════╦═════════════╦═════════════╦═════════╦═════════╦═════════╗
║ partition_id        ║ colName    ║ IsInrow ║ IsSparse ║ IsRecordPrefixCompressed ║ IsSymbol ║ PrefixBytes ║ InRowLength ║ file_id ║ page_id ║ slot_id ║
╠═════════════════════╬════════════╬═════════╬══════════╬══════════════════════════╬══════════╬═════════════╬═════════════╬═════════╬═════════╬═════════╣
║ 1729382263096344576 ║ varchar30  ║ 1       ║ 0        ║ 0                        ║ 0        ║ 0           ║ 30          ║ 1       ║ 1912    ║ 0       ║
║ 1729382263096344576 ║ varchar255 ║ 1       ║ 0        ║ 0                        ║ 0        ║ 0           ║ 255         ║ 1       ║ 1912    ║ 0       ║
║ 1729382263096344576 ║ varchar256 ║ 1       ║ 0        ║ 0                        ║ 0        ║ 0           ║ 256         ║ 1       ║ 1912    ║ 0       ║
╚═════════════════════╩════════════╩═════════╩══════════╩══════════════════════════╩══════════╩═════════════╩═════════════╩═════════╩═════════╩═════════╝

As you can see, the InRowLength for each value is shown, along with the physical storage location of each row - the "file_id", "page_id", and "slot_id".

If we take the file_id and page_id values from the query results above and run DBCC PAGE with them, we can see the actual physical page contents:

DBCC TRACEON (3604); --send display to the client
DBCC PAGE (tempdb, 1, 1912, 3); --database, file_id, page_id, 3 to show page contents
DBCC TRACEOFF (3604);--reset display back to the error log

The results from my machine are:

PAGE: (1:1912)


BUFFER:


BUF @0x00000000FF5B2E80

bpage = 0x0000000024130000          bhash = 0x0000000000000000          bpageno = (1:1912)
bdbid = 2                           breferences = 0                     bcputicks = 0
bsampleCount = 0                    bUse1 = 32497                       bstat = 0x10b
blog = 0x212121cc                   bnext = 0x0000000000000000          

PAGE HEADER:


Page @0x0000000024130000

m_pageId = (1:1912)                 m_headerVersion = 1                 m_type = 1
m_typeFlagBits = 0x0                m_level = 0                         m_flagBits = 0x8000
m_objId (AllocUnitId.idObj) = 98834 m_indexId (AllocUnitId.idInd) = 7936
Metadata: AllocUnitId = 2233785421652951040                              
Metadata: PartitionId = 1945555045333008384                              Metadata: IndexId = 0
Metadata: ObjectId = 34099162       m_prevPage = (0:0)                  m_nextPage = (0:0)
pminlen = 4                         m_slotCnt = 1                       m_freeCnt = 7538
m_freeData = 652                    m_reservedCnt = 0                   m_lsn = (35:210971:362)
m_xactReserved = 0                  m_xdesId = (0:0)                    m_ghostRecCnt = 0
m_tornBits = 0                      DB Frag ID = 1                      

Allocation Status

GAM (1:2) = ALLOCATED               SGAM (1:3) = NOT ALLOCATED          PFS (1:1) = 0x41 ALLOCATED  50_PCT_FULL
DIFF (1:6) = NOT CHANGED            ML (1:7) = NOT MIN_LOGGED           

Slot 0 Offset 0x60 Length 556

Record Type = PRIMARY_RECORD        Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
Record Size = 556                   
Memory Dump @0x000000005145A060

0000000000000000:   30000400 03000003 002d002c 012c0231 31313131  0........-.,.,.11111
0000000000000014:   31313131 31313131 31313131 31313131 31313131  11111111111111111111
0000000000000028:   31313131 31323232 32323232 32323232 32323232  11111222222222222222
000000000000003C:   32323232 32323232 32323232 32323232 32323232  22222222222222222222
0000000000000050:   32323232 32323232 32323232 32323232 32323232  22222222222222222222
0000000000000064:   32323232 32323232 32323232 32323232 32323232  22222222222222222222
0000000000000078:   32323232 32323232 32323232 32323232 32323232  22222222222222222222
000000000000008C:   32323232 32323232 32323232 32323232 32323232  22222222222222222222
00000000000000A0:   32323232 32323232 32323232 32323232 32323232  22222222222222222222
00000000000000B4:   32323232 32323232 32323232 32323232 32323232  22222222222222222222
00000000000000C8:   32323232 32323232 32323232 32323232 32323232  22222222222222222222
00000000000000DC:   32323232 32323232 32323232 32323232 32323232  22222222222222222222
00000000000000F0:   32323232 32323232 32323232 32323232 32323232  22222222222222222222
0000000000000104:   32323232 32323232 32323232 32323232 32323232  22222222222222222222
0000000000000118:   32323232 32323232 32323232 32323232 32323232  22222222222222222222
000000000000012C:   33333333 33333333 33333333 33333333 33333333  33333333333333333333
0000000000000140:   33333333 33333333 33333333 33333333 33333333  33333333333333333333
0000000000000154:   33333333 33333333 33333333 33333333 33333333  33333333333333333333
0000000000000168:   33333333 33333333 33333333 33333333 33333333  33333333333333333333
000000000000017C:   33333333 33333333 33333333 33333333 33333333  33333333333333333333
0000000000000190:   33333333 33333333 33333333 33333333 33333333  33333333333333333333
00000000000001A4:   33333333 33333333 33333333 33333333 33333333  33333333333333333333
00000000000001B8:   33333333 33333333 33333333 33333333 33333333  33333333333333333333
00000000000001CC:   33333333 33333333 33333333 33333333 33333333  33333333333333333333
00000000000001E0:   33333333 33333333 33333333 33333333 33333333  33333333333333333333
00000000000001F4:   33333333 33333333 33333333 33333333 33333333  33333333333333333333
0000000000000208:   33333333 33333333 33333333 33333333 33333333  33333333333333333333
000000000000021C:   33333333 33333333 33333333 33333333           3333333333333333

Slot 0 Column 1 Offset 0xf Length 30 Length (physical) 30

varchar30 = 111111111111111111111111111111                               

Slot 0 Column 2 Offset 0x2d Length 255 Length (physical) 255

varchar255 = 2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222
22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222
222222222222222222222222222222222222222222                               

Slot 0 Column 3 Offset 0x12c Length 256 Length (physical) 256

varchar256 = 3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333
33333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333
3333333333333333333333333333333333333333333                              

Others have already pointed out that that the number of bytes required to store the length is fixed. I wanted to focus on this part in your question:

Does it matter anymore at this point?

You have your question tagged with enterprise edition, which generally means you'll have a fair amount of data. Often differences of one byte per row really don't matter too much in practice. For example, the following table with a fully filled VARCHAR(255) column takes up 143176 KB space on disk:

DROP TABLE IF EXISTS dbo.V255_FULL;

CREATE TABLE dbo.V255_FULL (
    ID1 BIGINT NOT NULL,
    ID2 BIGINT NOT NULL,
    V255 VARCHAR(255)
);

INSERT INTO dbo.V255_FULL WITH (TABLOCK)
SELECT TOP (500000) 0, 0, REPLICATE('A', 255)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

EXEC sp_spaceused 'V255_FULL';

Results:

╔═══════════╦══════════════════════╦═══════════╦═══════════╦════════════╦════════╗
║   name    ║         rows         ║ reserved  ║   data    ║ index_size ║ unused ║
╠═══════════╬══════════════════════╬═══════════╬═══════════╬════════════╬════════╣
║ V255_FULL ║ 500000               ║ 143176 KB ║ 142888 KB ║ 8 KB       ║ 280 KB ║
╚═══════════╩══════════════════════╩═══════════╩═══════════╩════════════╩════════╝

Let's create a second table with a fully filled VARCHAR(256) column. That's going to take at least one more byte per row, right?

DROP TABLE IF EXISTS dbo.V256_FULL;

CREATE TABLE dbo.V256_FULL (
    ID1 BIGINT NOT NULL,
    ID2 BIGINT NOT NULL,
    V256 VARCHAR(256)
);

INSERT INTO dbo.V256_FULL WITH (TABLOCK)
SELECT TOP (500000) 0, 0, REPLICATE('A', 256)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

EXEC sp_spaceused 'V256_FULL';

Results:

╔═══════════╦══════════════════════╦═══════════╦═══════════╦════════════╦════════╗
║   name    ║         rows         ║ reserved  ║   data    ║ index_size ║ unused ║
╠═══════════╬══════════════════════╬═══════════╬═══════════╬════════════╬════════╣
║ V256_FULL ║ 500000               ║ 143176 KB ║ 142888 KB ║ 8 KB       ║ 280 KB ║
╚═══════════╩══════════════════════╩═══════════╩═══════════╩════════════╩════════╝

It just so happens that both tables take up the same amount of space. The same number of rows fits on each 8k page. It's great that you want to spent time optimizing your application, but I suspect that you're better off focusing on different areas.


The declared size of the varchar rarely has a performance impact1. The data might be actually stored as a rowstore with page compression or row compression. As a Clustered Columnstore, or as a Memory-Optimized table. Each of these will have different performance trade-offs, but it never matters whether you declare a varchar(255) or varchar(256).


1 - under specific circumstances, there are performance implications around the size of character-varying columns; Brent Ozar has a great write-up about it here