Curso SQL · Northwind
Tema 14 de 64
Bloque 3 · Transformar valores PostgreSQL 17 y SQL Server (Azure SQL Database) northwind

Tipos de datos: PostgreSQL contra SQL Server

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:

  1. En SQL Server, el promedio de enteros es un entero: avg corta los decimales.
  2. En SQL Server, convertir a int corta 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é iguales depende de la collation (las reglas de comparación de texto) de la base. La nuestra en Azure es SQL_Latin1_General_CP1_CI_AS, donde CI = case insensitive. Es la habitual en SQL Server, así que un WHERE que en PostgreSQL filtra, en SQL Server puede devolver filas de más.
  • varchar sin longitud no da error en SQL Server: recorta sin avisar, a 30 caracteres en un CAST y a 1 en una variable. En PostgreSQL varchar sin longitud no tiene tope.
  • Concatenar con NULL da NULL en los dos. concat() trata el NULL como 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
  • datetime no guarda milisegundos exactos. Su precisión es de unos 3 milisegundos y redondea: .789 se convierte en .790. Por eso en SQL Server se usa datetime2.
  • 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 + 1 da error en los dos (integer out of range / Arithmetic overflow error).
  • División entera: 7/2 = 3.
  • Coma flotante: 0.1 + 0.2 en 8 bytes da 0.30000000000000004 en los dos. En real cambia solo cómo se muestra: PostgreSQL enseña 0.3 y SQL Server 0.300000011920. Es el mismo valor aproximado.
  • money: se recomienda evitarlo en los dos.

4. Errores comunes al pasar de PostgreSQL a SQL Server

  1. CAST(x AS int) esperando que redondee. En SQL Server trunca. Escribe CAST(ROUND(x, 0) AS int).
  2. CAST(x AS decimal) sin (p,s). En SQL Server son cero decimales.
  3. avg sobre una columna entera. En SQL Server devuelve un entero. Convierte antes de promediar.
  4. ROUND sin pensar en el tamaño del tipo. Puede desbordar cuando el redondeo añade una cifra.
  5. varchar sin longitud. Recorta a 30 o a 1 sin avisar.
  6. Comparar texto esperando que distinga mayúsculas. Con la collation habitual no las distingue.
  7. Dar por bueno un total hecho con TRY_CAST. Los datos malos se volvieron NULL y no suman.
  8. datetime para registrar momentos exactos. Usa datetime2.

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

  1. Sin ejecutarla, di qué devuelve cada motor: SELECT CAST(-3.7 AS int);
  2. En SQL Server, SELECT avg(quantity) FROM order_details; devuelve 23. ¿Por qué no 23.81, y cómo lo arreglas?
  3. 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);
  4. 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é.

? RespuestaRespóndela antes de abrir
SELECT category_id,
       avg(CAST(units_in_stock AS decimal(10,2))) AS stock_medio,
       CAST(ROUND(avg(unit_price), 0) AS int)    AS precio_entero
FROM products
GROUP BY category_id
ORDER BY category_id;
category_id stock_medio precio_entero
1           46.583333   38
2           42.250000   23
3           29.692307   25
4           39.300000   29
5           44.000000   20
6           27.500000   54
7           20.000000   32
8           58.416666   21

Los precios coinciden con PostgreSQL en las 8 categorías, y el stock medio ya no pierde los decimales. Los dos cambios:

  1. CAST(units_in_stock AS decimal(10,2)) dentro del avg. En SQL Server, el promedio de un entero es un entero. Sin esto, la categoría 1 da 46 en vez de 46.58.
  2. ROUND(..., 0) antes del CAST a int. SQL Server trunca al convertir a entero. Sin esto, la categoría 1 da 37 en vez de 38.

Lo que hay que decirle al responsable de compras: el stock medio sale con 6 decimales en SQL Server y con 16 en PostgreSQL. Es la forma de presentar de cada motor, no una diferencia en los datos. Si se compara a 2 decimales, coincide.

✓ SolucionesResuélvelos antes de abrir

1. PostgreSQL devuelve -4 (redondea, alejándose del cero). SQL Server devuelve -3 (trunca: tira los decimales).

2. quantity es smallint, y en SQL Server avg de enteros devuelve un entero: corta los decimales. Se arregla convirtiendo antes de promediar:

SELECT avg(CAST(quantity AS decimal(10,2))) FROM order_details;

avg(x * 1.0) también funciona, porque 1.0 es decimal y obliga a SQL Server a promediar con decimales; pero el CAST deja más clara la intención.

3. 999.99 es un decimal(5,2). ROUND devuelve el mismo tipo, y 1000.00 necesita 6 cifras: no cabe y da Arithmetic overflow error. Con 123.45 funciona porque el resultado (123.00) sigue cabiendo. Se arregla dando sitio antes de redondear:

SELECT ROUND(CAST(999.99 AS decimal(7,2)), 0);   -- 1000.00

4.

SELECT TRY_CAST(REPLACE('2,7834', ',', '.') AS decimal(18,4));   -- 2.7834

REPLACE cambia la coma por punto y TRY_CAST devuelve NULL en vez de romper si aun así queda algo inválido. decimal(18,4) lleva (p,s) explícitos: con decimal a secas devolvería 3.