1. Qué es
Si vienes de PostgreSQL y te toca trabajar en SQL Server, vas a escribir consultas que funcionan en los dos motores, no dan ningún error y devuelven números distintos.
Este es el informe más sencillo posible: stock medio y precio medio por categoría, sobre la misma Northwind cargada en los dos motores.
SELECT category_id,
avg(units_in_stock) AS stock_medio,
CAST(avg(unit_price) AS int) AS precio_entero
FROM products
GROUP BY category_id
ORDER BY category_id;
category_id |
PostgreSQL stock_medio |
SQL Server stock_medio |
PostgreSQL precio_entero |
SQL Server precio_entero |
|---|---|---|---|---|
| 1 | 46.5833333333333333 | 46 | 38 | 37 |
| 2 | 42.2500000000000000 | 42 | 23 | 22 |
| 3 | 29.6923076923076923 | 29 | 25 | 25 |
| 4 | 39.3000000000000000 | 39 | 29 | 28 |
| 5 | 44.0000000000000000 | 44 | 20 | 20 |
| 6 | 27.5000000000000000 | 27 | 54 | 54 |
| 7 | 20.0000000000000000 | 20 | 32 | 32 |
| 8 | 58.4166666666666667 | 58 | 21 | 20 |
Misma consulta, mismos datos, ningún error. El stock medio pierde los decimales en 6 de las 8 categorías y el precio sale distinto en 4. Dos comportamientos lo explican, y los dos se ven en este tema:
- En SQL Server, el promedio de enteros es un entero:
avgcorta los decimales. - En SQL Server, convertir a
intcorta los decimales: no redondea.
Todas las salidas de este tema se ejecutaron en PostgreSQL 17 (Neon) y en Azure SQL Database.
2. Equivalencias de tipos
| Necesidad | PostgreSQL | SQL Server | Ojo |
|---|---|---|---|
| Entero | integer |
int |
Igual |
| Decimal exacto | numeric(p,s) |
decimal(p,s) / numeric(p,s) |
Sin (p,s) se comportan al revés (sección 3.2) |
| Coma flotante 4 bytes | real |
real |
Igual |
| Coma flotante 8 bytes | double precision |
float |
float a secas es 8 bytes en los dos |
| Texto sin tope | text |
varchar(max) / nvarchar(max) |
|
| Texto con tope | varchar(n) |
varchar(n) / nvarchar(n) |
Sin (n) se comportan muy distinto (sección 3.6) |
| Sí/no | boolean |
bit (0/1) |
bit no es un booleano de verdad (sección 3.7) |
| Fecha y hora | timestamp |
datetime2 |
Evita datetime (sección 3.8) |
| Fecha con zona | timestamptz |
datetimeoffset |
|
| Dinero | numeric(12,2) |
decimal(12,2) |
money existe en los dos y en los dos hay que evitarlo |
3. Ejemplos lado a lado
3.1 Convertir a entero: uno redondea, el otro corta
SELECT CAST(3.7 AS int), CAST(2.5 AS int), CAST(-3.7 AS int);
| Expresión | PostgreSQL | SQL Server |
|---|---|---|
CAST(3.7 AS int) |
4 |
3 |
CAST(2.5 AS int) |
3 |
2 |
CAST(-3.7 AS int) |
-4 |
-3 |
CAST(CAST(2.5 AS float) AS int) |
2 |
2 |
PostgreSQL redondea; SQL Server trunca: tira los decimales sin mirarlos. Con float coinciden, pero por motivos distintos: PostgreSQL redondea al par y SQL Server corta.
Es la diferencia que más dinero mueve, porque no avisa. Si una consulta convierte precios a entero, en SQL Server cada línea pierde hasta casi una unidad, siempre hacia abajo.
Cómo escribirlo para que dé lo mismo en los dos: redondea primero y convierte después.
CAST(ROUND(avg(unit_price), 0) AS int)
En SQL Server eso devuelve 38, 23, 25, 29, 20, 54, 32, 21, igual que PostgreSQL.
3.2 decimal sin (p,s): todos los decimales o ninguno
SELECT CAST('2.4445' AS decimal);
| PostgreSQL | SQL Server | |
|---|---|---|
| Resultado | 2.4445 |
2 |
| Por qué | numeric sin paréntesis no tiene límite: guarda los decimales que traiga el valor |
decimal sin paréntesis es decimal(18,0): cero decimales |
En PostgreSQL, CAST(x AS numeric) es la conversión más segura que hay. En SQL Server es la que más destruye. Lo que en tipos-de-datos-en-postgresql (sección 3.2: convertir texto a número conserva todos los decimales) es una buena práctica, en SQL Server es un error.
Regla para SQL Server: decimal siempre con (p,s).
decimal(10) sin escala sí coincide en los dos: escala 0, así que CAST(2.7 AS decimal(10)) da 3 en ambos.
3.3 ROUND no es la misma función
| Expresión | PostgreSQL | SQL Server |
|---|---|---|
round(2.4445, 2) |
2.44 |
2.4400: mantiene los decimales del original, rellenos con ceros |
round(2.5) |
3 |
❌ Error: necesita siempre dos argumentos. ROUND(2.5, 0) → 3.0 |
round(-2.5) / ROUND(-2.5, 0) |
-3 |
-3.0 |
round(CAST(2.4445 AS float), 2) |
❌ Error: no existe round(double precision, integer) |
2.44 |
round(CAST(2.5 AS float)) / ROUND(..., 0) |
2 |
3 |
round(999.99, 0) |
1000 |
❌ Error de desbordamiento |
ROUND(2.4485, 2, 1) |
No existe | 2.4400: el tercer argumento distinto de 0 corta en vez de redondear |
El desbordamiento es el más traicionero. 999.99 es un decimal(5,2), y SQL Server intenta guardar el resultado en ese mismo tipo. 1000.00 necesita 6 cifras y no cabe:
Msg 8115, Level 16, State 2
Arithmetic overflow error converting expression to data type numeric.
Pasa solo cuando el redondeo añade una cifra a la parte entera, así que puede funcionar meses y romperse con un dato nuevo. La salida es darle sitio antes de redondear: ROUND(CAST(999.99 AS decimal(7,2)), 0) → 1000.00.
3.4 Texto a número
| Caso | PostgreSQL | SQL Server |
|---|---|---|
CAST(' 10 ' AS int) |
10 |
10 |
CAST('3.7' AS int) |
❌ Error | ❌ Error |
CAST('1e3' AS numeric(18,4)) |
1000.0000 |
❌ Error |
'2,7', 'N/A', '', '$2.70' a decimal |
❌ Error | ❌ Error |
'100' + 5 |
105 |
105 |
'100' + '5' |
❌ Error: no sabe qué tipo son | 1005: el + concatena texto |
Para revisar valores sin que falle la consulta, cada motor tiene su herramienta, y no hacen lo mismo:
| PostgreSQL | SQL Server | |
|---|---|---|
| Función | pg_input_is_valid(x, 'numeric') |
TRY_CAST(x AS decimal(18,4)) |
| Devuelve | true / false |
El número convertido, o NULL si no puede |
| Uso típico | Encontrar las filas malas | Convertir y dejar en NULL las malas |
-- SQL Server
SELECT x AS texto, TRY_CAST(x AS decimal(18,4)) AS convertido
FROM (VALUES ('2.7'), ('2.4445'), ('2,7834'), ('N/A'), (''), ('$2.70')) t(x);
texto convertido
2.7 2.7000
2.4445 2.4445
2,7834 NULL
N/A NULL
NULL
$2.70 NULL
Cuidado con TRY_CAST: convierte los datos malos en NULL sin avisar, y sum ignora los NULL. Si lo usas en una carga, cuenta cuántos NULL salieron antes de dar el total por bueno.
3.5 División y promedios
| Expresión | PostgreSQL | SQL Server |
|---|---|---|
7/2 |
3 |
3 |
7.0/2 |
3.5000000000000000 |
3.500000 |
1.0/3 |
0.33333333333333333333 |
0.333333 |
avg de (1), (2) como enteros |
1.5000000000000000 |
1 |
avg(quantity) en order_details |
23.8129930394431555 |
23 |
La división entera es igual en los dos. El promedio no. En PostgreSQL, avg de enteros devuelve numeric; en SQL Server devuelve un entero, así que corta los decimales. Es la causa de la columna stock_medio de la sección 1.
Cómo escribirlo para que dé lo mismo: convierte antes de promediar.
avg(CAST(units_in_stock AS decimal(10,2)))
En SQL Server devuelve 46.583333, 42.250000, 29.692307…. Los 6 decimales son cosa de SQL Server; la parte importante, que no se pierda el promedio, ya coincide.
3.6 Texto
| Caso | PostgreSQL | SQL Server |
|---|---|---|
'abc' = 'abc ' |
distintos |
iguales: ignora los espacios finales al comparar |
'ABC' = 'abc' |
distintos |
iguales con la configuración habitual |
CAST('…36 caracteres…' AS varchar) |
Los 36 | Los 30 primeros |
DECLARE @v varchar = 'hola' |
(no aplica) | h: 1 solo carácter |
'a' + NULL / 'a' \|\| NULL |
NULL |
NULL |
concat('a', NULL) |
a |
a |
Tres notas:
- Mayúsculas: que
'ABC' = 'abc'déigualesdepende de la collation (las reglas de comparación de texto) de la base. La nuestra en Azure esSQL_Latin1_General_CP1_CI_AS, dondeCI= case insensitive. Es la habitual en SQL Server, así que unWHEREque en PostgreSQL filtra, en SQL Server puede devolver filas de más. varcharsin longitud no da error en SQL Server: recorta sin avisar, a 30 caracteres en unCASTy a 1 en una variable. En PostgreSQLvarcharsin longitud no tiene tope.- Concatenar con
NULLdaNULLen los dos.concat()trata elNULLcomo texto vacío en los dos.
3.7 Booleanos: bit no es boolean
| Caso | PostgreSQL | SQL Server |
|---|---|---|
| Tipo | boolean |
bit |
CAST(5 AS boolean) / CAST(5 AS bit) |
true |
1 |
CASE WHEN columna_booleana THEN … |
Funciona | ❌ Error: An expression of non-boolean type specified in a context where a condition is expected |
En SQL Server, bit es un número que solo vale 0 o 1. No se puede usar como condición: hay que compararlo. WHERE activo en PostgreSQL es WHERE activo = 1 en SQL Server.
3.8 Fechas
| Caso | PostgreSQL | SQL Server |
|---|---|---|
'2026-10-04 12:34:56.789' como timestamp / datetime |
12:34:56.789 |
12:34:56.790 |
'… 12:34:56.7891234' como timestamp / datetime2 |
12:34:56.789123 (6 decimales) |
12:34:56.7891234 (7 decimales) |
| Días entre dos fechas | fecha_fin - fecha_inicio |
DATEDIFF(day, fecha_inicio, fecha_fin) |
'04/10/2026' como fecha |
2026-04-10 |
2026-04-10 |
datetimeno guarda milisegundos exactos. Su precisión es de unos 3 milisegundos y redondea:.789se convierte en.790. Por eso en SQL Server se usadatetime2.- La fecha ambigua dio 10 de abril en los dos, pero depende de la configuración de cada servidor (idioma y formato de fecha). Escribe siempre las fechas como
'YYYY-MM-DD', que se lee igual en cualquier configuración.
3.9 Lo que sí coincide
No todo cambia. En estos casos los dos motores se comportan igual:
- Desbordamiento de enteros:
2147483647 + 1da error en los dos (integer out of range/Arithmetic overflow error). - División entera:
7/2 = 3. - Coma flotante:
0.1 + 0.2en 8 bytes da0.30000000000000004en los dos. Enrealcambia solo cómo se muestra: PostgreSQL enseña0.3y SQL Server0.300000011920. Es el mismo valor aproximado. money: se recomienda evitarlo en los dos.
4. Errores comunes al pasar de PostgreSQL a SQL Server
CAST(x AS int)esperando que redondee. En SQL Server trunca. EscribeCAST(ROUND(x, 0) AS int).CAST(x AS decimal)sin(p,s). En SQL Server son cero decimales.avgsobre una columna entera. En SQL Server devuelve un entero. Convierte antes de promediar.ROUNDsin pensar en el tamaño del tipo. Puede desbordar cuando el redondeo añade una cifra.varcharsin longitud. Recorta a 30 o a 1 sin avisar.- Comparar texto esperando que distinga mayúsculas. Con la collation habitual no las distingue.
- Dar por bueno un total hecho con
TRY_CAST. Los datos malos se volvieronNULLy no suman. datetimepara registrar momentos exactos. Usadatetime2.
5. Chuleta de traducción
| Quieres | PostgreSQL | SQL Server |
|---|---|---|
| Entero redondeado | CAST(x AS int) o round(x)::int |
CAST(ROUND(x, 0) AS int) |
| Entero truncado | trunc(x)::int |
CAST(x AS int) |
| Decimal sin perder nada | CAST(x AS numeric) |
CAST(x AS decimal(p,s)) con (p,s) suficientes |
| Redondear a 2 decimales | round(x, 2) |
ROUND(x, 2) (devuelve la escala original) |
| Texto a decimal redondeado | round(CAST(x AS numeric), 2) |
ROUND(CAST(x AS decimal(18,4)), 2): el (p,s) es obligatorio y debe dejar sitio al redondeo |
| Promedio con decimales | avg(x) |
avg(CAST(x AS decimal(10,2))) |
| Convertir sin romper | pg_input_is_valid + CAST |
TRY_CAST |
| Condición booleana | WHERE activo |
WHERE activo = 1 |
| Días entre fechas | fin - inicio |
DATEDIFF(day, inicio, fin) |
6. Ejercicios
- Sin ejecutarla, di qué devuelve cada motor:
SELECT CAST(-3.7 AS int); - En SQL Server,
SELECT avg(quantity) FROM order_details;devuelve23. ¿Por qué no23.81, y cómo lo arreglas? - Esta consulta funciona en SQL Server con casi todos los precios y falla con
999.99. ¿Por qué, y cómo la arreglas?sql SELECT ROUND(999.99, 0); - Una API manda el precio
'2,7834'. Conviértelo a decimal en SQL Server sin que la consulta falle y sin perder el valor.
7. Pregunta de negocio
Te piden migrar a SQL Server el informe de la sección 1 (stock medio y precio medio entero por categoría), que hoy corre en PostgreSQL. El responsable de compras va a comparar las dos versiones. Reescríbelo para que SQL Server devuelva los mismos números y explica qué dos cambios hiciste y por qué.