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