Back to Article List

SQL Server data types, CAST, CONVERT and modulo

SQL Server data types, CAST, CONVERT and modulo

Updated and tested 9 October 2026

Column types are the one part of a schema you cannot walk back on a whim. An index goes in with a routine deploy. A stored procedure gets rewritten over lunch. Changing varchar(50) to nvarchar(100) on a table with 200 million rows and four foreign keys pointing at it means a maintenance window, a rollback plan and an argument with someone from the application team about encoding.

So the twenty minutes you spend on the choice up front are cheap. The second half of this is the conversion rules that fire when two types meet in an expression, since that is where wrong answers come from. Data types in MSSQL are documented well and read badly, which is most of why the precedence list below surprises people. Examples use the same schema as the rest of this series: dbo.customers, dbo.orders, dbo.order_items and dbo.employees.

Choosing between the numeric types

int, bigint, smallint and tinyint

Four integer types. The question is how big the numbers get.

  • tinyint, 1 byte, 0 to 255. Unsigned only, which surprises people. Good for small enumerations like a status code.
  • smallint, 2 bytes, -32,768 to 32,767.
  • int, 4 bytes, roughly plus or minus 2.1 billion.
  • bigint, 8 bytes, about 9.2 quintillion either way.

I default to int for a surrogate key. Two billion rows is more than most tables will ever hold, and the four bytes saved over bigint get multiplied by every non-clustered index that carries the clustering key. Anything that logs events goes straight to bigint, because event tables grow at a rate nobody predicts and running out of identity values on a live table at 2.1 billion is a bad day. Skipping tinyint for status columns and using int everywhere is a defensible laziness. Using bigint for everything because storage is cheap is not, since it costs memory in the buffer pool on every page read too.

decimal against float

decimal(p, s) is exact. You state precision and scale, and the engine stores the digits. 0.1 + 0.2 equals 0.3. Precision goes up to 38 digits. Storage grows in steps: 5 bytes up to 9 digits, 9 bytes to 19, 13 bytes to 28 and 17 bytes to 38.

numeric is the same type under a different name.

float(n) is approximate binary floating point, IEEE 754. It cannot represent 0.1 exactly, in the same way decimal cannot represent one third. float(53) is 8 bytes and is what a bare float gives you. real is float(24) and 4 bytes.

DECLARE @dec decimal(19,4) = 0.1;
DECLARE @flt float          = 0.1;

SELECT CASE WHEN @dec * 3 = 0.3 THEN 'exact' ELSE 'not exact' END AS decimal_check,
       CASE WHEN @flt * 3 = 0.3 THEN 'exact' ELSE 'not exact' END AS float_check;
decimal_check   float_check
-------------   -----------
exact           not exact

Money goes in decimal. Always. Anything a customer sees on an invoice, anything that gets summed and reconciled, anything a regulator asks about. Float is for measurements whose input already carries error bars: sensor readings, latency samples, scientific data spanning fifteen orders of magnitude. I also avoid the money type. It looks purpose-built and has a well-known habit of losing precision in division, because it holds only four decimal places through intermediate results. decimal(19,4) does the same job with rules you can predict.

varchar against nvarchar

varchar(n) stores one byte per character in the column's collation, up to 8,000. nvarchar(n) stores UTF-16, two bytes per character, up to 4,000. Both have a max variant that spills out of the row and holds up to 2 GB. The fixed-length char and nchar pad to the declared length, and they earn their place only on codes of a fixed width, like a two-letter country code.

Storage is the obvious cost and the smaller one. A wider column means fewer rows per 8 KB page, which means more pages read for the same query, which shows up as buffer pool pressure long before it shows up as disk usage. An nvarchar(4000) column holding email addresses is a real tax on a big table.

Set against that, guessing wrong about encoding is expensive to undo. So: nvarchar for anything a human types, meaning names, addresses, free text and product descriptions. varchar for machine-generated identifiers you control, meaning SKUs, ISO codes, hashes and base64. Modern SQL Server also offers UTF-8 collations for char and varchar, which changes the arithmetic for mostly-ASCII text with occasional Unicode in it. I have not benchmarked those on a table large enough to say where the crossover sits, so treat that as a direction to test and not a recommendation. Check what your instance has:

SELECT name, description
FROM sys.fn_helpcollations()
WHERE name LIKE '%UTF8%';

Collation rides along with this choice too, and the collation-sensitive behaviour of REPLACE and friends turns up in SQL Server string functions. String comparisons and sorting follow the column's collation and not the server's, so a case-sensitive join against a case-insensitive column does something other than what the code implies. Comparison behaviour is the other consequence, and it gets its own section below under precedence.

Date and time types

Six of them, and two are legacy:

  • date, 3 bytes, 0001-01-01 to 9999-12-31. Use it whenever there is no time component. A birth date is not a timestamp.
  • time(n), 3 to 5 bytes depending on the fractional scale.
  • datetime2(n), 6 to 8 bytes, scale 0 to 7. This is the default choice for a timestamp.
  • datetimeoffset(n), 8 to 10 bytes, carries a UTC offset. Use it when the offset is part of the fact you are recording.
  • datetime, 8 bytes, range starts at 1753, and it rounds to increments of .000, .003 and .007 seconds.
  • smalldatetime, 4 bytes, minute precision, 1900 to 2079.

Pick datetime2 over datetime for new columns. It is more precise and its range is sane. At scale 0 to 2 it is smaller too. I use datetime2(0) for order timestamps and datetime2(3) where milliseconds carry meaning. Scale 7 is 100-nanosecond resolution and nothing in a business application needs it. One detail that follows from all this: GETDATE() returns datetime while SYSDATETIME() returns datetime2(7), so a column defaulted to GETDATE() caps its own precision at what datetime can hold. SQL Server date functions goes further into which function returns which type, and the choice matters more than it looks when you start comparing values across columns.

bit and uniqueidentifier

bit holds 0, 1 or NULL. SQL Server packs up to eight bit columns from the same row into a single byte, so a dozen flags on one table cost two bytes between them. Strings convert on assignment: 'TRUE' becomes 1, 'FALSE' becomes 0 and a string like 'yes' fails to convert. Converting any nonzero number gives 1. A nullable bit is three-state, which is occasionally what you want and more often a modelling mistake dressed up as a flag.

uniqueidentifier is 16 bytes holding a GUID. It solves a real problem, generating keys on disconnected clients without a round trip, and it costs four times the width of an int in every index. NEWID() as a default on a clustered primary key is the bigger issue. The values are random, so inserts land in the middle of the index. Page splits and fragmentation start in the first week. NEWSEQUENTIALID() exists for that case and only works as a column default. Where client-side generation is not needed, use an identity int and put the GUID in a separate indexed column for external references.

Your idea deserves better hosting

24/7 support 30-day money-back guaranteeRefund covers the VPS plan on your first order. Cancel anytime
Verrechnungszyklus

VPS.S1

$5.99 Save  17 %
$4.99 Monatlich
  • 2 vCPU AMD EPYC
  • 2 GB RAMRAM
  • 30 GB NVMeSPEICHER
  • Unbegrenzter Traffic
  • IPv4 & IPv6IPv6-Support ist derzeit nicht verfügbar in Frankreich, Finnland oder den Niederlanden. inklusive

VPS.S3

$14.99 Save  33 %
$9.99 Monatlich
  • 4 vCPU AMD EPYC
  • 6 GB RAMRAM
  • 70 GB NVMeSPEICHER
  • Unbegrenzter Traffic
  • IPv4 & IPv6IPv6-Support ist derzeit nicht verfügbar in Frankreich, Finnland oder den Niederlanden. inklusive

VPS.S4

$19.99 Save  20 %
$15.99 Monatlich
  • 6 vCPU AMD EPYC
  • 8 GB RAMRAM
  • 80 GB NVMeSPEICHER
  • Unbegrenzter Traffic
  • IPv4 & IPv6IPv6-Support ist derzeit nicht verfügbar in Frankreich, Finnland oder den Niederlanden. inklusive

EPYC VPS.P1

$8.99 Save  22 %
$6.99 Monatlich
  • 2 vCPU AMD EPYC
  • 4 GB RAMRAM
  • 40 GB NVMeSPEICHER
  • Unbegrenzter Traffic
  • IPv4 & IPv6IPv6-Support ist derzeit nicht verfügbar in Frankreich, Finnland oder den Niederlanden. inklusive
  • Kostenloses Auto-BackupEnthält einen Backup-Slot, den du auf täglich, wöchentlich oder monatlich einstellen kannst.

EPYC VPS.P2

$16.99 Save  24 %
$12.99 Monatlich
  • 2 vCPU AMD EPYC
  • 8 GB RAMRAM
  • 80 GB NVMeSPEICHER
  • Unbegrenzter Traffic
  • IPv4 & IPv6IPv6-Support ist derzeit nicht verfügbar in Frankreich, Finnland oder den Niederlanden. inklusive
  • Kostenloses Auto-BackupEnthält einen Backup-Slot, den du auf täglich, wöchentlich oder monatlich einstellen kannst.

EPYC VPS.P4

$29.99 Save  23 %
$22.99 Monatlich
  • 4 vCPU AMD EPYC
  • 16 GB RAMRAM
  • 160 GB NVMeSPEICHER
  • Unbegrenzter Traffic
  • IPv4 & IPv6IPv6-Support ist derzeit nicht verfügbar in Frankreich, Finnland oder den Niederlanden. inklusive
  • Kostenloses Auto-BackupEnthält einen Backup-Slot, den du auf täglich, wöchentlich oder monatlich einstellen kannst.

EPYC VPS.P5

$39.99 Save  25 %
$29.99 Monatlich
  • 8 vCPU AMD EPYC
  • 16 GB RAMRAM
  • 180 GB NVMeSPEICHER
  • Unbegrenzter Traffic
  • IPv4 & IPv6IPv6-Support ist derzeit nicht verfügbar in Frankreich, Finnland oder den Niederlanden. inklusive
  • Kostenloses Auto-BackupEnthält einen Backup-Slot, den du auf täglich, wöchentlich oder monatlich einstellen kannst.

EPYC VPS.P6

$59.99 Save  25 %
$44.99 Monatlich
  • 8 vCPU AMD EPYC
  • 32 GB RAMRAM
  • 200 GB NVMeSPEICHER
  • Unbegrenzter Traffic
  • IPv4 & IPv6IPv6-Support ist derzeit nicht verfügbar in Frankreich, Finnland oder den Niederlanden. inklusive
  • Kostenloses Auto-BackupEnthält einen Backup-Slot, den du auf täglich, wöchentlich oder monatlich einstellen kannst.

EPYC VPS.P7

$69.99 Save  29 %
$49.99 Monatlich
  • 16 vCPU AMD EPYC
  • 32 GB RAMRAM
  • 240 GB NVMeSPEICHER
  • Unbegrenzter Traffic
  • IPv4 & IPv6IPv6-Support ist derzeit nicht verfügbar in Frankreich, Finnland oder den Niederlanden. inklusive
  • Kostenloses Auto-BackupEnthält einen Backup-Slot, den du auf täglich, wöchentlich oder monatlich einstellen kannst.

Genoa VPS.G2

$24.99 Save  20 %
$19.99 Monatlich
  • 2 vCPUAMD EPYC Genoa 4. Generation 9xx4 mit 3,25 GHz oder ähnlich, auf Zen 4-Architektur. AMD EPYC G4
  • 4 GB DDR5RAM
  • 50 GB NVMeSPEICHER
  • Unbegrenzter Traffic
  • IPv4 & IPv6IPv6-Support ist derzeit nicht verfügbar in Frankreich, Finnland oder den Niederlanden. inklusive
  • Kostenloses Auto-BackupEnthält einen Backup-Slot, den du auf täglich, wöchentlich oder monatlich einstellen kannst.

Genoa VPS.G4

$44.99 Save  22 %
$34.99 Monatlich
  • 4 vCPUAMD EPYC Prozessor mit dedizierten vCPU Kernen, auf Enterprise Serverhardware. AMD EPYC G4
  • 8 GB DDR5RAM
  • 100 GB NVMeSPEICHER
  • Unbegrenzter Traffic
  • IPv4 & IPv6IPv6-Support ist derzeit nicht verfügbar in Frankreich, Finnland oder den Niederlanden. inklusive
  • Kostenloses Auto-BackupEnthält einen Backup-Slot, den du auf täglich, wöchentlich oder monatlich einstellen kannst.

Genoa VPS.G6

$89.99 Save  22 %
$69.99 Monatlich
  • 8 vCPUAMD EPYC Prozessor mit dedizierten vCPU Kernen, auf Enterprise Serverhardware. AMD EPYC G4
  • 16 GB DDR5RAM
  • 200 GB NVMeSPEICHER
  • Unbegrenzter Traffic
  • IPv4 & IPv6IPv6-Support ist derzeit nicht verfügbar in Frankreich, Finnland oder den Niederlanden. inklusive
  • Kostenloses Auto-BackupEnthält einen Backup-Slot, den du auf täglich, wöchentlich oder monatlich einstellen kannst.

Genoa VPS.G7

$159.99 Save  22 %
$124.99 Monatlich
  • 8 vCPUAMD EPYC Prozessor mit dedizierten vCPU Kernen, auf Enterprise Serverhardware. AMD EPYC G4
  • 32 GB DDR5RAM
  • 250 GB NVMeSPEICHER
  • Unbegrenzter Traffic
  • IPv4 & IPv6IPv6-Support ist derzeit nicht verfügbar in Frankreich, Finnland oder den Niederlanden. inklusive
  • Kostenloses Auto-BackupEnthält einen Backup-Slot, den du auf täglich, wöchentlich oder monatlich einstellen kannst.

AMD Ryzen VPS.R1

$16.99 Save  18 %
$13.99 Monatlich
  • 1 dedizierter CPU AMD Ryzen 9 7950X mit 4,5 GHz oder ähnlich, auf Zen 4-Architektur. vCPU
  • 4 GB DDR5RAM
  • 50 GB NVMeSPEICHER
  • Unbegrenzter Traffic
  • IPv4 & IPv6 inklusive IPv6-Support ist derzeit nicht verfügbar in Frankreich, Finnland oder den Niederlanden.
  • Auto-Backup inklusive

AMD Ryzen VPS.R2

$29.99 Save  17 %
$24.99 Monatlich
  • 2 dedizierte CPUs AMD Ryzen 9 7950X mit 4,5 GHz oder ähnlich, auf Zen 4-Architektur. vCPU
  • 8 GB DDR5RAM
  • 100 GB NVMeSPEICHER
  • Unbegrenzter Traffic
  • IPv4 & IPv6 inklusive IPv6-Support ist derzeit nicht verfügbar in Frankreich, Finnland oder den Niederlanden.
  • Auto-Backup inklusive

AMD Ryzen VPS.R4

$109.99 Save  18 %
$89.99 Monatlich
  • 8 dedizierte CPUs AMD Ryzen 9 7950X mit 4,5 GHz oder ähnlich, auf Zen 4-Architektur. vCPU
  • 32 GB DDR5RAM
  • 400 GB NVMeSPEICHER
  • Unbegrenzter Traffic
  • IPv4 & IPv6 inklusive IPv6-Support ist derzeit nicht verfügbar in Frankreich, Finnland oder den Niederlanden.
  • Auto-Backup inklusive

*VAT excluded.

INCLUDED WITH EVERY PLAN

✓ No setup fees ✓1 Gbps network
✓ Full root access on KVM ✓ Free server monitoring ✓ Firewall management ✓24/7 support

Data type precedence in SQL Server

The data type precedence reference carries the whole order, and Microsoft's rule for reading it is one sentence, verbatim: "When an operator combines expressions of different data types, the data type with the lower precedence is first converted to the data type with the higher precedence." The lower-precedence side is the one that moves. Highest first:

json
sql_variant
xml
user-defined types
datetimeoffset
datetime2
datetime
smalldatetime
date
time
float
real
decimal
money
smallmoney
bigint
int
smallint
tinyint
bit
ntext
text
image
timestamp
uniqueidentifier
nvarchar
nchar
varchar
char
vector
varbinary
binary

json at the top and vector near the bottom are recent, added with the native JSON and vector types in SQL Server 2025. Very little else in that list has moved in twenty years, which is either a compliment to whoever designed it or a sign that nobody dares touch an ordering this load-bearing.

Three positions in it cause almost all the trouble, and a fourth turns up inside CASE. ISNULL, COALESCE and CASE has that one with its error message attached, since a CASE expression returns its highest-precedence branch and one stray integer takes the whole thing with it. The three below are the ones you meet in a WHERE clause.

int outranks every string type

Look at where int sits. Above nvarchar, above varchar, above all of them. Comparing a string column to a numeric literal therefore converts the column and not the literal.

-- product_sku is varchar(30)
SELECT * FROM dbo.order_items WHERE product_sku = 12345;
Msg 245, Level 16, State 1
Conversion failed when converting the varchar value 'SKU-9981' to data type int.

Note what the error names. Not the value you searched for. Some other row's SKU, which happened to be the first non-numeric string the engine tried to convert. An error naming a value that appears nowhere in your query is the tell for this whole class of bug. The query works fine in a test database where every SKU is numeric and fails the moment real data arrives. Quote the literal as = '12345' and it seeks properly.

float outranks decimal

float and real both sit above decimal, so mixing them promotes your exact number to an approximate one. No warning is issued.

DECLARE @price decimal(19,4) = 19.9900;
DECLARE @rate  float         = 0.2;

SELECT SQL_VARIANT_PROPERTY(@price * @rate, 'BaseType') AS result_type;

That returns float. One float variable in a tax calculation and every downstream sum is approximate, which surfaces months later as a total off by a penny in a reconciliation nobody can explain. SQL_VARIANT_PROPERTY with 'BaseType' is the fastest way to interrogate what an expression really produces, and I use it constantly when a result looks slightly wrong.

nvarchar outranks varchar and can turn a seek into a scan

This is the expensive one, because it does not error. It gets slow.

DECLARE @sku nvarchar(30) = N'SKU-9981';

SELECT order_item_id, order_id, quantity
FROM dbo.order_items
WHERE product_sku = @sku;      -- product_sku is varchar(30), indexed

nvarchar outranks varchar, so the column gets converted, every row of it. Applying a function to an indexed column makes the predicate non-sargable, the seek can become a scan, and a query that touched three pages can end up touching every page in the index. Everything about predicate shape and index usage is in writing WHERE clauses that can seek, and the plan shows this one plainly in the predicate:

CONVERT_IMPLICIT(nvarchar(30),[dbo].[order_items].[product_sku],0)=[@sku]

You almost never write this by hand. Your ORM does. Entity Framework and a lot of drivers send string parameters as nvarchar by default regardless of the column type, which is why a query that is instant in SSMS crawls from the application with identical text. Fix it at the parameter by declaring it varchar(30), or in the driver configuration, where one flag usually controls it.

Changing the column to nvarchar fixes it too. Altering an indexed column comes with restrictions, listed in ALTER TABLE and column changes, and those are what make this the bigger of the two conversations.

CAST against CONVERT

They do the same job. CAST is ANSI standard and portable. CONVERT is T-SQL only and takes a third argument, the style code, which is the entire reason to use it.

SELECT CAST(o.order_date AS date)                    AS as_date,
       CAST(o.total_amount AS varchar(20))           AS as_text,
       CONVERT(varchar(10), o.order_date, 23)        AS iso_date,
       CONVERT(varchar(8),  o.order_date, 112)       AS compact_date,
       CONVERT(varchar(30), o.order_date, 126)       AS iso8601
FROM dbo.orders AS o;

The styles to memorise, from the CAST and CONVERT reference: 23 gives yyyy-mm-dd, 103 gives dd/mm/yyyy, 112 gives yyyymmdd, 120 gives yyyy-mm-dd hh:mi:ss, 121 adds milliseconds and 126 is ISO 8601 with the T separator. Those are the ones you reach for when you MSSQL CONVERT datetime values into strings for an export or a filename.

One subtlety decides which of them you pick. When one side is datetime or smalldatetime, every style up to 100 is nondeterministic except 20 and 21, so on those types an expression using style 23 cannot go into a persisted computed column or an indexed view. Style 112 and style 126 are the deterministic ISO options. Those are the ones for a string going anywhere structural.

So: CAST unless you need a style code, then CONVERT. Numeric conversions carry their own rounding rules. Numeric to int truncates. Numeric to numeric rounds. Money to int rounds. Same expression, three different answers depending on the types involved.

TRY_CAST, TRY_CONVERT, PARSE and TRY_PARSE

All four arrived in SQL Server 2012. The TRY_ pair returns NULL when a conversion fails and raises nothing. That is what you want when cleaning imported data:

SELECT TRY_CAST('2026-02-30' AS date)    AS bad_date,      -- NULL
       TRY_CAST('42' AS int)             AS good_int,      -- 42
       TRY_CONVERT(decimal(10,2), '1,299.00') AS bad_money; -- NULL

Here is the caveat that catches people who treat TRY_CAST as blanket protection. It returns NULL for a conversion that is permitted but failed. For a conversion that is not permitted at all, it still throws:

SELECT TRY_CAST(4 AS XML);
Msg 529, Level 16, State 2
Explicit conversion from data type int to xml is not allowed.

Int to XML is not a legal conversion, so there is nothing to try. Wrapping a query in TRY_CAST does not make it safe to run against unknown types. I have watched an ETL job fall over on exactly this after somebody added a defensive cast.

PARSE and TRY_PARSE are a different tool. They are CLR dependent, they cannot be remoted, and they only target numeric and date/time types. What they do have is a culture argument:

SELECT PARSE('12/31/2026' AS date USING 'en-US')     AS us_style,
       TRY_PARSE('31/12/2026' AS date USING 'en-GB')  AS uk_style;

Microsoft's own guidance is to keep them narrow: "Use PARSE only for converting from string to date/time and number types. For general type conversions, continue to use CAST or CONVERT." That is the right advice. CLR-dependent functions are slower than the built-in conversions and they add a dependency you would rather not carry in a hot loop. I use TRY_PARSE when importing a CSV from a locale I do not control, and nowhere else.

Spotting implicit conversion in an execution plan

Turn on the actual execution plan, run the query, then look in two places.

Hover any index or table operator and read the Predicate or Seek Predicates property. The string CONVERT_IMPLICIT anywhere in there means a conversion is being applied per row, and when it wraps a column, the index can lose its seek. Wrapped around a parameter it does no harm. The second place is the root SELECT operator. It carries a yellow warning triangle when a type conversion may affect the cardinality estimate, with text naming the expression involved. That warning is the cheapest performance win in the whole tool and it is under-used, because people read the operators and never click the root node.

Compare estimated against actual row counts while you are in there. A large gap next to a CONVERT_IMPLICIT is usually the same problem showing up twice.

The modulo operator in SQL Server, integer division, ROUND, FLOOR and CEILING

There is no MOD() function in T-SQL. The remainder operator is %, and it works on integer, monetary and numeric types.

SELECT 38 / 5 AS int_division,
       38 % 5 AS remainder;
int_division   remainder
------------   ---------
7              3

Both operands are integers, so the division truncates. 7, not 7.6. Cast one operand to get a real answer:

SELECT 38 / 5.0                        AS as_decimal,       -- 7.600000
       CAST(38 AS decimal(10,2)) / 5   AS explicit_cast,    -- 7.6000000000000
       38 / CAST(5 AS float)           AS as_float;         -- 7.6

The literal 5.0 is decimal, so precedence promotes the whole expression. This matters most in percentage calculations, where (shipped_count / total_count) * 100 returns 0 for every row that is not 100%. I shipped that column once, and the person who caught it asked why every product had sold either none of its stock or all of it.

Bucketing rows for parallel processing with WHERE order_id % 4 = 0. Testing evenness. Sampling every hundredth row. Rounding a timestamp down to a fixed interval by dividing then multiplying back:

SELECT o.order_id,
       DATEADD(minute,
               (DATEDIFF(minute, '20000101', o.order_date) / 15) * 15,
               CAST('20000101' AS datetime2(0))) AS quarter_hour_bucket
FROM dbo.orders AS o;

Integer division doing the truncation is the whole trick there. Microsoft's modulo operator reference runs to a few paragraphs, the supported types among them. What it does not document is the sign behaviour when either operand is negative, which is worth saying out loud, because plenty of blog posts state a rule with total confidence and no source. Run SELECT -7 % 3, 7 % -3; on your own instance before you build logic on it. For a guaranteed non-negative result, see the FAQ below.

ROUND, FLOOR and CEILING with negative numbers

SELECT ROUND(-2.4, 0) AS rounded,   -- -2
       FLOOR(-2.4)    AS floored,   -- -3
       CEILING(-2.4)  AS ceiled;    -- -2

SELECT ROUND(2.4, 0)  AS rounded,   -- 2
       FLOOR(2.4)     AS floored,   -- 2
       CEILING(2.4)   AS ceiled;    -- 3

FLOOR goes toward negative infinity, so FLOOR(-2.4) is -3. ROUND goes to the nearest value and breaks ties away from zero. On positive numbers the two get conflated happily for years, until somebody hands a refund calculation a negative amount. ROUND also keeps the scale of its input, so the result may display as -2.0; wrap it in a CAST for an integer type out. A non-zero third argument makes ROUND truncate instead, so ROUND(2.678, 2, 1) gives 2.67 and not 2.68.

While we are on operators, use <> and not !=. Both work. Microsoft's comparison operators reference labels != as "Not equal to (not ISO standard)" along with !< and !>. Small consistency thing. It costs nothing to be on the standard side of it.

Choosing data types for a new table

Here is the shape I write by default, which settles most of the arguments above before anyone has them:

CREATE TABLE dbo.orders
(
    order_id      int IDENTITY(1,1)  NOT NULL CONSTRAINT pk_orders PRIMARY KEY,
    customer_id   int                NOT NULL,
    order_date    datetime2(0)       NOT NULL CONSTRAINT df_orders_date DEFAULT SYSUTCDATETIME(),
    status        varchar(20)        NOT NULL,
    total_amount  decimal(19,4)      NOT NULL,
    shipped_date  datetime2(0)       NULL,
    notes         nvarchar(1000)     NULL
);

Integer surrogate key. Exact decimal for money. datetime2(0) defaulted to UTC. varchar for the machine-generated status values, nvarchar for the field a human types into, and NOT NULL on everything that has a meaning for every row. Declaring nullability deliberately is easy to skip and the optimiser reads it. Getting literals into that table with matching types is its own small discipline, and the same precedence rules run in the other direction on the way in. That side of it belongs to INSERT INTO in SQL Server.

Then the thing that saves the most time later: put the same types on your parameters and variables that the columns carry. Half the implicit conversions in a production database were introduced by a DECLARE and not by a schema.

Questions?

How do I get a positive remainder from a negative number?

Take the remainder, add the divisor and take the remainder again: SELECT ((@value % @divisor) + @divisor) % @divisor;. For a positive divisor the result always lands between 0 and the divisor minus 1, whichever sign the inner % gives a negative value, so the logic never rests on behaviour Microsoft's modulo page leaves undocumented. That makes it safe for spreading ids or offsets across a fixed number of buckets. The idiom assumes the divisor is positive, so check it first if it can come from input.

About the author

Oliver P. Redmond

I studied history and, for a while, expected to end up in archives (or academic publishing). Instead I took a copy-editing job with a small specialist publisher, learnt rather more about XML than the job description had implied and gradually found myself editing software manuals. Naturally, technical writing followed from there.

What I like about infrastructure is the accumulated quality. A modern service may have an elegant interface sitting on top of conventions and protocols that are sometimes older than the person using it. And so, understanding a system often means following those layers backwards. I write mostly about Linux systems, virtual machines and storage. I live in Bristol and still use a fountain pen for notes.

Deploy your next project in seconds

Bring your winning ideas online faster, with modern hardware and unmetered bandwidth. Join a European cloud trusted by thousands of developers and businesses worldwide.