sql >> Base de Datos >  >> RDS >> Database

Complejidades NULL - Parte 2

Este artículo es el segundo de una serie sobre las complejidades de NULL. El mes pasado presenté NULL como marcador de SQL para cualquier tipo de valor faltante. Le expliqué que SQL no le brinda la capacidad de distinguir entre faltante y aplicable (valores A) y ausente e inaplicable (valores I) marcadores. También expliqué cómo funcionan las comparaciones que involucran valores NULL con constantes, variables, parámetros y columnas. Este mes continúo la discusión cubriendo las inconsistencias del tratamiento NULL en diferentes elementos de T-SQL.

Continuaré usando la base de datos de muestra TSQLV5 como el mes pasado en algunos de mis ejemplos. Puede encontrar el script que crea y completa esta base de datos aquí, y su diagrama ER aquí.

Inconsistencias de tratamiento NULL

Como ya se ha dado cuenta, el tratamiento NULL no es trivial. Parte de la confusión y la complejidad tiene que ver con el hecho de que el tratamiento de NULL puede ser inconsistente entre diferentes elementos de T-SQL para operaciones similares. En las próximas secciones, describo el manejo de NULL en cálculos lineales versus agregados, cláusulas ON/WHERE/HAVING, restricción CHECK versus opción CHECK, elementos IF/WHILE/CASE, la declaración MERGE, distinción y agrupación, así como ordenación y singularidad.

Cálculos lineales versus agregados

T-SQL, y lo mismo ocurre con SQL estándar, utiliza una lógica de manejo NULL diferente cuando aplica una función agregada real como SUM, MIN y MAX en filas en comparación con cuando aplica el mismo cálculo que uno lineal en columnas. Para demostrar esta diferencia, usaré dos tablas de muestra llamadas #T1 y #T2 que usted crea y completa ejecutando el siguiente código:

DROP TABLE IF EXISTS #T1, #T2;
 
SELECT * INTO #T1 FROM ( VALUES(10, 5, NULL) ) AS D(col1, col2, col3);
 
SELECT * INTO #T2 FROM ( VALUES(10),(5),(NULL) ) AS D(col1);

La tabla #T1 tiene tres columnas llamadas col1, col2 y col3. Actualmente tiene una fila con los valores de columna 10, 5 y NULL, respectivamente:

SELECT * FROM #T1;
col1        col2        col3
----------- ----------- -----------
10          5           NULL

La tabla #T2 tiene una columna llamada col1. Actualmente tiene tres filas con los valores 10, 5 y NULL en col1:

SELECT * FROM #T2;
col1
-----------
10
5
NULL

Al aplicar lo que en última instancia es un cálculo agregado, como una suma lineal entre columnas, la presencia de cualquier entrada NULL produce un resultado NULL. La siguiente consulta demuestra este comportamiento:

SELECT col1 + col2 + col3 AS total
FROM #T1;

Esta consulta genera el siguiente resultado:

total
-----------
NULL

Por el contrario, las funciones agregadas reales, que se aplican a través de las filas, están diseñadas para ignorar las entradas NULL. La siguiente consulta demuestra este comportamiento usando la función SUM:

SELECT SUM(col1) AS total
FROM #T2;

Esta consulta genera el siguiente resultado:

total
-----------
15

Warning: Null value is eliminated by an aggregate or other SET operation.

Observe la advertencia exigida por el estándar SQL que indica la presencia de entradas NULL que se ignoraron. Puede suprimir dichas advertencias desactivando la opción de sesión ANSI_WARNINGS.

De manera similar, cuando se aplica a una expresión de entrada, la función COUNT cuenta el número de filas con valores de entrada que no son NULL (a diferencia de COUNT (*) que simplemente cuenta el número de filas). Por ejemplo, reemplazar SUM(col1) con COUNT(col1) en la consulta anterior devuelve el recuento de 2.

Curiosamente, si aplica un agregado COUNT a una columna que está definida como que no permite valores NULL, el optimizador convierte la expresión COUNT() a COUNT(*). Esto permite el uso de cualquier índice con el propósito de contar en lugar de requerir el uso de un índice que contenga la columna en cuestión. Esa es una razón más además de garantizar la consistencia e integridad de sus datos que debería alentarlo a aplicar restricciones como NOT NULL y otras. Tales restricciones permiten al optimizador más flexibilidad para considerar alternativas más óptimas y evitar trabajo innecesario.

Según esta lógica, la función AVG divide la suma de los valores que no son NULL por el recuento de los valores que no son NULL. Considere la siguiente consulta como ejemplo:

SELECT AVG(1.0 * col1) AS avgall
FROM #T2;

Aquí, la suma de los valores col1 no NULL 15 se divide por el recuento de valores no NULL 2. Multiplica col1 por el literal numérico 1.0 para forzar la conversión implícita de los valores de entrada enteros a numéricos para obtener una división numérica y no un entero división. Esta consulta genera el siguiente resultado:

avgall
---------
7.500000

De manera similar, los agregados MIN y MAX ignoran las entradas NULL. Considere la siguiente consulta:

SELECT MIN(col1) AS mincol1, MAX(col1) AS maxcol1
FROM #T2;

Esta consulta genera el siguiente resultado:

mincol1     maxcol1
----------- -----------
5           10

Intentar aplicar cálculos lineales pero emular la semántica de funciones agregadas (ignorar NULL) no es agradable. Emular SUM, COUNT y AVG no es demasiado complejo, pero requiere que verifique cada entrada en busca de NULL, así:

SELECT col1, col2, col3,
  CASE
    WHEN COALESCE(col1, col2, col3) IS NULL THEN NULL
    ELSE COALESCE(col1, 0) + COALESCE(col2, 0) + COALESCE(col3, 0)
  END AS sumall,
  CASE WHEN col1 IS NOT NULL THEN 1 ELSE 0 END
    + CASE WHEN col2 IS NOT NULL THEN 1 ELSE 0 END
    + CASE WHEN col3 IS NOT NULL THEN 1 ELSE 0 END AS cntall,
  CASE
    WHEN COALESCE(col1, col2, col3) IS NULL THEN NULL
    ELSE 1.0 * (COALESCE(col1, 0) + COALESCE(col2, 0) + COALESCE(col3, 0))
           / (CASE WHEN col1 IS NOT NULL THEN 1 ELSE 0 END
                + CASE WHEN col2 IS NOT NULL THEN 1 ELSE 0 END
                + CASE WHEN col3 IS NOT NULL THEN 1 ELSE 0 END)
  END AS avgall
FROM #T1;

Esta consulta genera el siguiente resultado:

col1        col2        col3        sumall      cntall      avgall
----------- ----------- ----------- ----------- ----------- ---------------
10          5           NULL        15          2           7.500000000000

Intentar aplicar un mínimo o un máximo como un cálculo lineal a más de dos columnas de entrada es bastante complicado, incluso antes de agregar la lógica para ignorar NULL, ya que implica anidar varias expresiones CASE, ya sea directa o indirectamente (cuando reutiliza alias de columna). Por ejemplo, aquí hay una consulta que calcula el máximo entre col1, col2 y col3 en #T1, sin la parte que ignora los valores NULL:

SELECT col1, col2, col3, 
  CASE WHEN col1 IS NULL OR col2 IS NULL OR col3 IS NULL THEN NULL ELSE max2 END AS maxall
FROM #T1
  CROSS APPLY (VALUES(CASE WHEN col1 >= col2 THEN col1 ELSE col2 END)) AS A1(max1)
  CROSS APPLY (VALUES(CASE WHEN max1 >= col3 THEN max1 ELSE col3 END)) AS A2(max2);

Esta consulta genera el siguiente resultado:

col1        col2        col3        maxall
----------- ----------- ----------- -----------
10          5           NULL        NULL

Si examina el plan de consulta, encontrará la siguiente expresión expandida calculando el resultado final:

[Expr1005] = Scalar Operator(CASE WHEN CASE WHEN [#T1].[col1] IS NOT NULL THEN [#T1].[col1] ELSE 
  CASE WHEN [#T1].[col2] IS NOT NULL THEN [#T1].[col2] 
    ELSE [#T1].[col3] END END IS NULL THEN NULL ELSE 
  CASE WHEN CASE WHEN [#T1].[col1]>=[#T1].[col2] THEN [#T1].[col1] 
    ELSE [#T1].[col2] END>=[#T1].[col3] THEN 
  CASE WHEN [#T1].[col1]>=[#T1].[col2] THEN [#T1].[col1] 
    ELSE [#T1].[col2] END ELSE [#T1].[col3] END END)

Y ahí es cuando solo hay tres columnas involucradas. ¡Imagina tener una docena de columnas involucradas!

Ahora agregue a esto la lógica para ignorar NULL:

SELECT col1, col2, col3, max2 AS maxall
FROM #T1
  CROSS APPLY (VALUES(CASE WHEN col1 >= col2 OR col2 IS NULL THEN col1 ELSE col2 END)) AS A1(max1)
  CROSS APPLY (VALUES(CASE WHEN max1 >= col3 OR col3 IS NULL THEN max1 ELSE col3 END)) AS A2(max2);

Esta consulta genera el siguiente resultado:

col1        col2        col3        maxall
----------- ----------- ----------- -----------
10          5           NULL        10

Oracle tiene un par de funciones denominadas MAYOR y MENOR que aplican cálculos mínimos y máximos, respectivamente, como cálculos lineales a los valores de entrada. Estas funciones devuelven un NULL dada cualquier entrada NULL como lo hacen la mayoría de los cálculos lineales. Hubo un elemento de comentarios abierto que solicitaba obtener funciones similares en T-SQL, pero esta solicitud no se transfirió en su último cambio en el sitio de comentarios. Si Microsoft agrega tales funciones a T-SQL, sería genial tener una opción que controle si se ignoran los NULL o no.

Mientras tanto, existe una técnica mucho más elegante en comparación con las mencionadas anteriormente que calcula cualquier tipo de agregado como uno lineal entre columnas utilizando la semántica de función agregada real que ignora los valores NULL. Utiliza una combinación del operador CROSS APPLY y una consulta de tabla derivada contra un constructor de valores de tabla que gira columnas a filas y aplica el agregado como una función de agregado real. Aquí hay un ejemplo que demuestra los cálculos MIN y MAX, pero puede usar esta técnica con cualquier función agregada que desee:

SELECT col1, col2, col3, maxall, minall
FROM #T1 CROSS APPLY
  (SELECT MAX(mycol), MIN(mycol)
   FROM (VALUES(col1),(col2),(col3)) AS D1(mycol)) AS D2(maxall, minall);

Esta consulta genera el siguiente resultado:

col1        col2        col3        maxall      minall
----------- ----------- ----------- ----------- -----------
10          5           NULL        10          5

¿Y si quieres lo contrario? ¿Qué pasa si necesita calcular un agregado en filas, pero produce un NULL si hay alguna entrada NULL? Por ejemplo, suponga que necesita sumar todos los valores col1 de #T1, pero devolver NULL si alguna de las entradas es NULL. Esto se puede lograr con la siguiente técnica:

SELECT SUM(col1) * NULLIF(MIN(CASE WHEN col1 IS NULL THEN 0 ELSE 1 END), 0) AS sumall
FROM #T2;

Aplica un agregado MIN a una expresión CASE que devuelve ceros para entradas NULL y unos para entradas que no son NULL. Si hay alguna entrada NULL, el resultado de la función MIN es 0; de lo contrario, es 1. Luego, al usar la función NULLIF, convierte un resultado 0 en NULL. Luego multiplica el resultado de la función NULLIF por la suma original. Si hay alguna entrada NULL, multiplica la suma original por un NULL que produce un NULL. Si no hay una entrada NULL, multiplica el resultado de la suma original por 1, obteniendo la suma original.

Volviendo a los cálculos lineales que arrojan NULL para cualquier entrada NULL, la misma lógica se aplica a la concatenación de cadenas usando el operador +, como lo demuestra la siguiente consulta:

USE TSQLV5;
 
SELECT empid, country, region, city,
  country + N',' + region + N',' + city AS emplocation
FROM HR.Employees;

Esta consulta genera el siguiente resultado:

empid       country         region          city            emplocation
----------- --------------- --------------- --------------- ----------------
1           USA             WA              Seattle         USA,WA,Seattle
2           USA             WA              Tacoma          USA,WA,Tacoma
3           USA             WA              Kirkland        USA,WA,Kirkland
4           USA             WA              Redmond         USA,WA,Redmond
5           UK              NULL            London          NULL
6           UK              NULL            London          NULL
7           UK              NULL            London          NULL
8           USA             WA              Seattle         USA,WA,Seattle
9           UK              NULL            London          NULL

Desea concatenar las partes de ubicación de los empleados en una cadena, usando una coma como separador. Pero desea ignorar las entradas NULL. En cambio, cuando cualquiera de las entradas es NULL, obtienes NULL como resultado. Algunos desactivan la opción de sesión CONCAT_NULL_YIELDS_NULL, que hace que una entrada NULL se convierta en una cadena vacía con fines de concatenación, pero esta opción no se recomienda porque aplica un comportamiento no estándar. Además, se quedará con múltiples separadores consecutivos cuando haya entradas NULL, que normalmente no es el comportamiento deseado. Otra opción es reemplazar explícitamente las entradas NULL con una cadena vacía usando las funciones ISNULL o COALESCE, pero esto generalmente da como resultado un código largo y detallado. Una opción mucho más elegante es usar la función CONCAT_WS, que se introdujo en SQL Server 2017. Esta función concatena las entradas, ignorando los NULL, usando el separador provisto como la primera entrada. Aquí está la consulta de solución usando esta función:

SELECT empid, country, region, city,
  CONCAT_WS(N',', country, region, city) AS emplocation
FROM HR.Employees;

Esta consulta genera el siguiente resultado:

empid       country         region          city            emplocation
----------- --------------- --------------- --------------- ----------------
1           USA             WA              Seattle         USA,WA,Seattle
2           USA             WA              Tacoma          USA,WA,Tacoma
3           USA             WA              Kirkland        USA,WA,Kirkland
4           USA             WA              Redmond         USA,WA,Redmond
5           UK              NULL            London          UK,London
6           UK              NULL            London          UK,London
7           UK              NULL            London          UK,London
8           USA             WA              Seattle         USA,WA,Seattle
9           UK              NULL            London          UK,London

EN/DONDE/TENIENDO

Cuando se utilizan las cláusulas de consulta WHERE, HAVING y ON con fines de filtrado/coincidencia, es importante recordar que utilizan una lógica de predicado de tres valores. Cuando tiene una lógica de tres valores involucrada, desea identificar con precisión cómo la cláusula maneja los casos VERDADERO, FALSO y DESCONOCIDO. Estas tres cláusulas están diseñadas para aceptar casos VERDADEROS y rechazar casos FALSO y DESCONOCIDO.

Para demostrar este comportamiento, usaré una tabla llamada Contactos que usted crea y completa ejecutando el siguiente código:.

DROP TABLE IF EXISTS dbo.Contacts;
GO
 
CREATE TABLE dbo.Contacts
(
  id INT NOT NULL 
    CONSTRAINT PK_Contacts PRIMARY KEY,
  name VARCHAR(10) NOT NULL,
  hourlyrate NUMERIC(12, 2) NULL
    CONSTRAINT CHK_Contacts_hourlyrate CHECK(hourlyrate > 0.00)
);
 
INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES
  (1, 'A', 100.00),(2, 'B', 200.00),(3, 'C', NULL);

Tenga en cuenta que los contactos 1 y 2 tienen tarifas por hora aplicables y el contacto 3 no, por lo que su tarifa por hora se establece en NULL. Considere la siguiente consulta en busca de contactos con una tarifa por hora positiva:

SELECT id, name, hourlyrate
FROM dbo.Contacts
WHERE hourlyrate > 0.00;

Este predicado se evalúa como VERDADERO para los contactos 1 y 2, y como DESCONOCIDO para el contacto 3, por lo tanto, la salida contiene solo los contactos 1 y 2:

id          name       hourlyrate
----------- ---------- -----------
1           A          100.00
2           B          200.00

El pensamiento aquí es que cuando está seguro de que el predicado es verdadero, desea devolver la fila; de lo contrario, desea descartarla. Esto puede parecer trivial al principio, hasta que te das cuenta de que algunos elementos del lenguaje que también usan predicados funcionan de manera diferente.

Restricción CHECK versus opción CHECK

Una restricción CHECK es una herramienta que utiliza para imponer la integridad en una tabla en función de un predicado. El predicado se evalúa cuando intenta insertar o actualizar filas en la tabla. A diferencia de las cláusulas de coincidencia y filtrado de consultas que aceptan los casos VERDADEROS y rechazan los casos FALSO y DESCONOCIDO, una restricción CHECK está diseñada para aceptar los casos VERDADERO y DESCONOCIDO y rechazar los casos FALSO. El pensamiento aquí es que cuando está seguro de que el predicado es falso, desea rechazar el intento de cambio; de lo contrario, desea permitirlo.

Si examina la definición de nuestra tabla de contactos, notará que tiene la siguiente restricción CHECK, rechazando contactos con tarifas por hora no positivas:

CONSTRAINT CHK_Contacts_hourlyrate CHECK(hourlyrate > 0.00)

Observe que la restricción usa el mismo predicado que usó en el filtro de consulta anterior.

Intente agregar un contacto con una tarifa por hora positiva:

INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (4, 'D', 150.00);

Este intento tiene éxito.

Intente agregar un contacto con una tarifa por hora NULA:

INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (5, 'E', NULL);

Este intento también tiene éxito, ya que una restricción CHECK está diseñada para aceptar casos VERDADERO y DESCONOCIDO. Ese es el caso en el que un filtro de consulta y una restricción CHECK están diseñados para funcionar de manera diferente.

Intente agregar un contacto con una tarifa por hora no positiva:

INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (6, 'F', -100.00);

Este intento falla con el siguiente error:

Mensaje 547, nivel 16, estado 0, línea 454
La declaración INSERT entró en conflicto con la restricción CHECK "CHK_Contacts_hourlyrate". El conflicto ocurrió en la base de datos "TSQLV5", tabla "dbo.Contacts", columna 'hourlyrate'.

T-SQL también le permite hacer cumplir la integridad de las modificaciones a través de vistas usando una opción CHECK. Algunos piensan que esta opción tiene un propósito similar a una restricción CHECK siempre que aplique la modificación a través de la vista. Por ejemplo, considere la siguiente vista, que usa un filtro basado en el predicado tarifa por hora> 0.00 y se define con la opción CHECK:

CREATE OR ALTER VIEW dbo.MyContacts
AS
SELECT id, name, hourlyrate
FROM dbo.Contacts
WHERE hourlyrate > 0.00
WITH CHECK OPTION;

Resulta que, a diferencia de una restricción CHECK, la opción view CHECK está diseñada para aceptar casos VERDADEROS y rechazar casos FALSO y DESCONOCIDO. Por lo tanto, en realidad está diseñado para comportarse más como lo hace normalmente el filtro de consulta, también con el propósito de hacer cumplir la integridad.

Intente insertar una fila con una tarifa por hora positiva a través de la vista:

INSERT INTO dbo.MyContacts(id, name, hourlyrate) VALUES (7, 'G', 300.00);

Este intento tiene éxito.

Intente insertar una fila con una tarifa por hora NULA a través de la vista:

INSERT INTO dbo.MyContacts(id, name, hourlyrate) VALUES (8, 'H', NULL);

Este intento falla con el siguiente error:

Mensaje 550, nivel 16, estado 1, línea 473
El intento de inserción o actualización falló porque la vista de destino especifica CON OPCIÓN DE COMPROBACIÓN o abarca una vista que especifica CON OPCIÓN DE COMPROBACIÓN y una o más filas resultantes de la operación no calificar bajo la restricción CHECK OPTION.

El pensamiento aquí es que una vez que agrega la opción CHECK a la vista, solo desea permitir modificaciones que den como resultado filas que devuelve la vista. Eso es un poco diferente a pensar con una restricción CHECK:rechazar los cambios para los que está seguro de que el predicado es falso. Esto puede ser un poco confuso. Si desea que la vista permita modificaciones que establezcan la tarifa por hora en NULL, necesita el filtro de consulta para permitirlas también agregando OR hourlyrate IS NULL. Solo debe darse cuenta de que una restricción CHECK y una opción CHECK están diseñadas para funcionar de manera diferente con respecto al caso UNKNOWN. El primero lo acepta mientras que el segundo lo rechaza.

Consulta la tabla de contactos después de todos los cambios anteriores:

SELECT id, name, hourlyrate
FROM dbo.Contacts;

Debería obtener el siguiente resultado en este punto:

id          name       hourlyrate
----------- ---------- -----------
1           A          100.00
2           B          200.00
3           C          NULL
4           D          150.00
5           E          NULL
7           G          300.00

SI/MIENTRAS/CASO

Los elementos de lenguaje IF, WHILE y CASE funcionan con predicados.

La instrucción IF está diseñada de la siguiente manera:

IF <predicate>
  <statement or BEGIN-END block when TRUE>
ELSE
  <statement or BEGIN-END block when FALSE or UNKNOWN>

Es intuitivo esperar tener un bloque VERDADERO después de la cláusula IF y un bloque FALSO después de la cláusula ELSE, pero debe darse cuenta de que la cláusula ELSE en realidad se activa cuando el predicado es FALSO o DESCONOCIDO. Teóricamente, un lenguaje de lógica de tres valores podría haber tenido una declaración IF con una separación de los tres casos. Algo como esto:

IF <predicate>
  WHEN TRUE
    <statement or BEGIN-END block when TRUE>
  WHEN FALSE
    <statement or BEGIN-END block when FALSE>
  WHEN UNKNOWN
    <statement or BEGIN-END block when UNKNOWN>

E incluso permitir combinaciones de resultados lógicos, de modo que si quisiera combinar FALSO y DESCONOCIDO en una sola sección, podría usar algo como esto:

IF <predicate>
  WHEN TRUE
    <statement or BEGIN-END block when TRUE>
  WHEN FALSE OR UNKNOWN
    <statement or BEGIN-END block when FALSE OR UNKNOWN>

Mientras tanto, puede emular dichas construcciones anidando declaraciones IF-ELSE y buscando explícitamente NULL en los operandos con el operador IS NULL.

La declaración WHILE solo tiene un bloque VERDADERO. Está diseñado de la siguiente manera:

WHILE <predicate>
  <statement or BEGIN-END block when TRUE>

La declaración o bloque BEGIN-END que forma el cuerpo del ciclo se activa mientras el predicado es TURE. Tan pronto como el predicado sea FALSO o DESCONOCIDO, el control pasa a la sentencia que sigue al bucle WHILE.

A diferencia de IF y WHILE, que son declaraciones que ejecutan código, CASE es una expresión que devuelve un valor. La sintaxis de un buscado La expresión CASE es la siguiente:

CASE
  WHEN <predicate 1> THEN <expression 1 when TRUE>
  WHEN <predicate 2> THEN <expression 2 when TRUE >
  ...
  WHEN <predicate n> THEN <expression n when TRUE >
  ELSE <else expression when all are FALSE or UNKNOWN>
END

Una expresión CASE está diseñada para devolver la expresión que sigue a la cláusula THEN que corresponde al primer predicado WHEN que se evalúa como TRUE. Si hay una cláusula ELSE, se activa si ningún predicado WHEN es VERDADERO (todos son FALSO o DESCONOCIDO). En ausencia de una cláusula ELSE explícita, se utiliza un ELSE NULL implícito. Si desea manejar un caso DESCONOCIDO por separado, puede buscar explícitamente NULL en los operandos del predicado utilizando el operador IS NULL.

Un sencillo La expresión CASE utiliza comparaciones implícitas basadas en la igualdad entre la expresión de origen y las expresiones comparadas:

CASE <source expression>
  WHEN <comp expression 1> THEN <result expression 1 when TRUE>
  WHEN <comp expression 2> THEN <result expression 2 when TRUE >
  ...
  WHEN <comp expression n> THEN <result expression n when TRUE >
  ELSE <else result expression when all are FALSE or UNKNOWN>
END

La expresión CASE simple está diseñada de manera similar a la expresión CASE buscada en términos del manejo de la lógica de tres valores, pero dado que las comparaciones usan una comparación implícita basada en la igualdad, no puede manejar el caso UNKNOWN por separado. Un intento de usar NULL en una de las expresiones comparadas en las cláusulas WHEN no tiene sentido ya que la comparación no dará como resultado TRUE incluso cuando la expresión de origen sea NULL. Considere el siguiente ejemplo:

DECLARE @input AS INT = NULL;
 
SELECT CASE @input WHEN NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

Esto se convierte implícitamente en lo siguiente:

DECLARE @input AS INT = NULL;
 
SELECT CASE WHEN @input = NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

En consecuencia, el resultado es:

La entrada no es NULL

Para detectar una entrada NULL, debe usar la sintaxis de la expresión CASE buscada y el operador IS NULL, así:

DECLARE @input AS INT = NULL;
 
SELECT CASE WHEN @input IS NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

Esta vez el resultado es:

La entrada es NULA

COMBINAR

La declaración MERGE se utiliza para fusionar datos de un origen en un destino. Utiliza un predicado de combinación para identificar los siguientes casos y aplicar una acción contra el objetivo:

  • Una fila de origen coincide con una fila de destino (se activa cuando se encuentra una coincidencia para la fila de origen donde el predicado de combinación es VERDADERO):aplique ACTUALIZAR o ELIMINAR contra el destino
  • Una fila de origen no coincide con una fila de destino (se activa cuando no se encuentran coincidencias para la fila de origen donde el predicado de combinación es VERDADERO, en lugar de que todos los predicados sean FALSO o DESCONOCIDO):aplicar un INSERTAR contra el destino
  • Una fila de destino no coincide con una fila de origen (se activa cuando no se encuentran coincidencias para la fila de destino donde el predicado de combinación es VERDADERO, en lugar de que todos los predicados sean FALSO o DESCONOCIDO):aplique ACTUALIZAR o ELIMINAR contra el destino

Los tres escenarios separan VERDADERO para un grupo y FALSO o DESCONOCIDO para otro. No obtiene secciones separadas para manejar casos VERDADERO, FALSO y DESCONOCIDO.

Para demostrar esto, usaré una tabla llamada T3 que usted crea y completa ejecutando el siguiente código:

DROP TABLE IF EXISTS dbo.T3;
GO
 
CREATE TABLE dbo.T3(col1 INT NULL, col2 INT NULL, CONSTRAINT UNQ_T3 UNIQUE(col1));
 
INSERT INTO dbo.T3(col1) VALUES(1),(2),(NULL);

Considere la siguiente instrucción MERGE:

MERGE INTO dbo.T3 AS TGT
USING (VALUES(1, 100), (3, 300)) AS SRC(col1, col2)
  ON SRC.col1 = TGT.col1
WHEN MATCHED THEN UPDATE
  SET TGT.col2 = SRC.col2
WHEN NOT MATCHED THEN INSERT(col1, col2) VALUES(SRC.col1, SRC.col2)
WHEN NOT MATCHED BY SOURCE THEN UPDATE
  SET col2 = -1;
 
SELECT col1, col2 FROM dbo.T3;

La fila de origen donde col1 es 1 coincide con la fila de destino donde col1 es 1 (el predicado es VERDADERO) y, por lo tanto, la fila de destino col2 se establece en 100.

La fila de origen donde col1 es 3 no coincide con ninguna fila de destino (porque todos los predicados son FALSO o DESCONOCIDO) y, por lo tanto, se inserta una nueva fila en T3 con 3 como valor de col1 y 300 como valor de col2.

Las filas de destino donde col1 es 2 y donde col1 es NULL no coinciden con ninguna fila de origen (para todas las filas, el predicado es FALSO o DESCONOCIDO) y, por lo tanto, en ambos casos col2 en las filas de destino se establece en -1.

La consulta contra T3 devuelve el siguiente resultado después de ejecutar la instrucción MERGE anterior:

col1        col2
----------- -----------
1           100
2           -1
NULL        -1
3           300

Mantenga la mesa T3 alrededor; se usa más tarde.

Distinción y agrupación

A diferencia de las comparaciones que se realizan utilizando operadores de igualdad y desigualdad, las comparaciones realizadas con fines de distinción y agrupación agrupan NULL. Se considera que un NULL no es distinto de otro NULL, pero se considera que un NULL es distinto de un valor que no es NULL. En consecuencia, la aplicación de una cláusula DISTINCT elimina las ocurrencias duplicadas de NULL. La siguiente consulta demuestra esto:

SELECT DISTINCT country, region FROM HR.Employees;

Esta consulta genera el siguiente resultado:

country         region
--------------- ---------------
UK              NULL
USA             WA

Hay varios empleados con el país EE. UU. y la región NULL, y después de eliminar los duplicados, el resultado muestra solo una aparición de la combinación.

Al igual que la distinción, la agrupación también agrupa los valores NULL, como demuestra la siguiente consulta:

SELECT country, region, COUNT(*) AS numemps
FROM HR.Employees
GROUP BY country, region;

Esta consulta genera el siguiente resultado:

country         region          numemps
--------------- --------------- -----------
UK              NULL            4
USA             WA              5

Nuevamente, los cuatro empleados con el país Reino Unido y la región NULL se agruparon.

Pedidos

La ordenación trata varios valores NULL como si tuvieran el mismo valor de ordenación. El estándar SQL deja que la implementación elija si ordenar los valores NULL primero o último en comparación con los valores que no son NULL. Microsoft optó por considerar que los NULL tienen valores de orden más bajos en comparación con los que no son NULL en SQL Server, por lo que cuando se usa la dirección de orden ascendente, T-SQL ordena primero los NULL. La siguiente consulta demuestra esto:

SELECT id, name, hourlyrate
FROM dbo.Contacts
ORDER BY hourlyrate;

Esta consulta genera el siguiente resultado:

id          name       hourlyrate
----------- ---------- -----------
3           C          NULL
5           E          NULL
1           A          100.00
4           D          150.00
2           B          200.00
7           G          300.00

El próximo mes agregaré más sobre este tema, discutiendo los elementos estándar que le brindan control sobre el comportamiento de orden NULL y las soluciones para esos elementos en T-SQL.

Singularidad

Al imponer la exclusividad en una columna NULLable mediante una restricción ÚNICA o un índice único, T-SQL trata los NULL como valores no NULL. Rechaza NULL duplicados como si un NULL no fuera único de otro NULL.

Recuerde que nuestra tabla T3 tiene una restricción ÚNICA definida en col1. Aquí está su definición:

CONSTRAINT UNQ_T3 UNIQUE(col1)

Consulta T3 para ver su contenido actual:

SELECT * FROM dbo.T3;

Si ejecutó todas las modificaciones en T3 de los ejemplos anteriores de este artículo, debería obtener el siguiente resultado:

col1        col2
----------- -----------
1           100
2           -1
NULL        -1
3           300

Intente agregar una segunda fila con un NULL en col1:

INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 400);

Obtiene el siguiente error:

Mensaje 2627, Nivel 14, Estado 1, Línea 558
Violación de la restricción CLAVE ÚNICA 'UNQ_T3'. No se puede insertar una clave duplicada en el objeto 'dbo.T3'. El valor de la clave duplicada es ().

Este comportamiento es en realidad no estándar. El próximo mes describiré la especificación estándar y cómo emularla en T-SQL.

Conclusión

En esta segunda parte de la serie sobre las complejidades de NULL, me centré en las inconsistencias en el tratamiento de NULL entre diferentes elementos de T-SQL. Cubrí los cálculos lineales frente a los agregados, las cláusulas de filtrado y coincidencia, la restricción CHECK frente a la opción CHECK, los elementos IF, WHILE y CASE, la declaración MERGE, la distinción y agrupación, el orden y la unicidad. Las inconsistencias que cubrí enfatizan aún más lo importante que es comprender correctamente el tratamiento de NULL en la plataforma que está utilizando, para asegurarse de que escribe un código correcto y sólido. El próximo mes continuaré la serie cubriendo las opciones de tratamiento NULL estándar de SQL que no están disponibles en T-SQL y proporcionaré soluciones que son compatibles con T-SQL.