Microsoft SQL Server – Todo sobre la administración y el mantenimiento de Servidores de datos

A connection was successfully established with the server, but then an error occurred during the pre-login handshake

Trabajas con Docker, MSSQL y recibes este error

Para ser mas claro el error que me estuvo volviendo loco fue el siguiente:

A connection was successfully established with the server, but then an error occurred during the pre-login handshake. (provider: TCP Provider, error: 35 – An internal exception was caught

Es difícil de entender y descifrar así te cuento un poco como era mi escenario

Escenario:

Persistencia: MS SQL Server 2018 Express Edition

Aplicación Web: Net 6.0

Contenedor: Docker

Al principio pensé que era una cuestión de conexión al motor, por lo que suele tener las siguientes precauciones:

En mi imagen base suele habilitar algunas herramientas para facilitar las pruebas: Por ejemplo iputils

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base

WORKDIR /app
#si el motor es SQL 2018
RUN sed -i 's/TLSv1.2/TLSv1/g' /etc/ssl/openssl.cnf 
EXPOSE 80
EXPOSE 443
USER root
RUN apt-get update
RUN apt-get install -y iputils-ping
RUN apt-get install -y nano
RUN apt-get install -y tzdata
RUN apt-get install -y locales
ENV LANG es_AR.UTF-8  
ENV LANGUAGE es_AR:es  
ENV LC_ALL es_AR.UTF-8  
ENV TZ America/Mendoza

Por otro lado en mi aplicación web bajo NET 6.0 suele habilitar la migración por párametros y habilitarlas al lanzamiento de la misma:

Mi Program.cs quería así

....
bool applymigrations = Configuration.GetValue<bool>("Migrations:ApplyMigrations");
string cone_rw6 = Configuration.GetValue<string>("ConnectionStrings:RW6Connection");
string cone_beje = Configuration.GetValue<string>("ConnectionStrings:BejermanConnection");

Console.WriteLine($"BD: Migraciones {applymigrations}, Conection: {cone_rw6} ");
Console.WriteLine($"BD: Migraciones {applymigrations}, Conection: {cone_beje} ");

var scope = app.Services.CreateScope();
if (applymigrations) await Migrations(scope.ServiceProvider);

Console.WriteLine("******************Finalizado configuración de middlewares *******************");

/* ========  Run  =======*/
app.Run();

Y mi función de Migraciones así



async Task Migrations(IServiceProvider services)
{
    //Cadena de conexión de variable externa



    var context_identity = services.GetRequiredService<AppIdentityContext>();
    var context_appdb = services.GetRequiredService<AppDbContext>();
    var conn_appidentity = context_identity.Database.GetDbConnection();
    var conn_appdb = context_appdb.Database.GetDbConnection();

    Console.WriteLine($"Conexión Actual Identity: {conn_appdb.ToString()}  {Environment.NewLine}  {conn_appdb.ConnectionString}");
    Console.WriteLine($"Conexión Actual AppDB: {conn_appdb.ToString()}  {Environment.NewLine}  {conn_appdb.ConnectionString}");
    Console.WriteLine("****************** Probando acceso  *******************");
    try
    {
        Console.WriteLine("Base Disponible de Identity:" + context_identity.Database.CanConnect());
        Console.WriteLine("Base Disponible de AppDb:" + context_appdb.Database.CanConnect());
    }
    catch (Exception ex)
    {
 Console.WriteLine($"------ !!! ERROR connectando: {ex.Message}");
    }

    Console.WriteLine("****************** Aplica Migraciones:  *******************");
    Console.WriteLine(applymigrations);

    if (applymigrations)

    {
        //Aplico la migracion
        Console.WriteLine("******************Aplicando migración*******************");
        Console.WriteLine("Migrando Identity:");
        context_identity.Database.Migrate();
        Console.WriteLine("Migrando Context:");
        context_appdb.Database.Migrate();

        Console.WriteLine("******************Cargando Usuarios:*******************");
        var userManager = services.GetRequiredService<UserManager<MyAUser_Models>>();
        var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();

        await AppIdentity_StartUp.CargarUsuarios(userManager, roleManager);
    }
}

Pero a pesar de todo esto no había caso. La aplicación se conectaba, pero luego la misma se caía.

Por lo que me puse a analizar el error completo:

Microsoft.Data.SqlClient.SqlException (0x80131904): 
A connection was successfully established with the server, 
but then an error occurred during the pre-login handshake. 
(provider: TCP Provider, error: 35 - An internal exception was caught)
 ---> System.Security.Authentication.AuthenticationException: Authentication failed, see inner exception.
 ---> Interop+OpenSsl+SslException: SSL Handshake failed with OpenSSL error - SSL_ERROR_SSL.

Y venía de la mano de SSL. Ya sabiendo por donde orientar mi búsqueda, fue mas sencillo llegar al causar y a la solución:

Para OpenSSL, la mínima versión con la que opera la imagen de .NET es TLS v1.2 en adelante, mientras que el MS SQL Server 2018 express edition, solo soporta hasta TLS v1.0

Solo era cuestión de probar, si la teoría era correcta. Para ello ingresé a la imagen y modifique la configuración del openssl para correr en una versión inferior:

#docker exec -it <name> sh

#cat /etc/ssl/openssl.cnf

.......
[system_default_sect] 
MinProtocol = TLSv1.2 
CipherString = DEFAULT@SECLEVEL=2

Por lo que procedí a modificar mi imagen, y no tener que cambiarlo manualmente en la implementación:

Para ello agregué

RUN sed -i 's/TLSv1.2/TLSv1/g' /etc/ssl/openssl.cnf

Mi primer parte del dockerfile quedó así

#################################################################################################
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base

WORKDIR /app
#si el motor es SQL 2018
RUN sed -i 's/TLSv1.2/TLSv1/g' /etc/ssl/openssl.cnf 
EXPOSE 80
EXPOSE 443
USER root
RUN apt-get update
RUN apt-get install -y iputils-ping
RUN apt-get install -y nano
RUN apt-get install -y tzdata
RUN apt-get install -y locales
ENV LANG es_AR.UTF-8  
ENV LANGUAGE es_AR:es  
ENV LC_ALL es_AR.UTF-8  
ENV TZ America/Mendoza

Cambiar el puerto del MS SQL Server

Primero que nada recordemos que el motor de Microsoft corre bajo los siguientes puertos:

TCP:1433

UDP:1433

Seguido a este te dejamos este post donde te contamos como funciona la seguridad de MS SQL (https://maurobernal.com.ar/blog/consejos-para-asegurar-tu-ms-sql-server/)

Entendiendo esta ya podemos empezar a cambiar la configuración de nuestro motor, para cambiar su puerto predeterminado. Lo primero que debes hacer es buscar el “SQL Server Configuration Manager” (o administrador de configuración de SQL Server) y buscar la opción de “SQL Server Network Configuration” (Configuración de Red) \ Protocols for MOTOR ,

y una vez seleccionado buscas la configuración de TCP/IP y le das doble clic. En la nueva ventana emergente ya puedes cambiar el puerto como te mostramos en la imagen.

Con esto sería suficiente. Solo toca ahora reiniciar el servicio. Puedes hacerlo también desde la misma aplicación.:

PD: No te olvides de abrir el puerto en tu firewall. Te dejo una ayuda para hacerlo desde CMD. Por ejemplo si elegiste el 11433:

netsh advfirewall firewall add rule name="SQL Puerto alternativo" dir=in localport="11433" protocol=tcp action=allow

Si quieres verificar, puedes usar tu SQL Management Studio ( https://maurobernal.com.ar/blog/mssql/como-especificar-el-puerto-en-el-sql-server-management-studio)

Deja tus comentarios si te son útiles estos consejos…

Funciones Analíticas con SQL Server

En los siguientes artículos que iré creando resumiremos el uso de las siguientes funciones analíticas incorporadas a partir de SQL Server 2012

 

Estas son:

CUME_DIST (Transact-SQL) LEAD (Transact-SQL)
FIRST_VALUE (Transact-SQL) PERCENTILE_CONT (Transact-SQL)
LAG (Transact-SQL) PERCENTILE_DISC (Transact-SQL)
LAST_VALUE (Transact-SQL) PERCENT_RANK (Transact-SQL)

Uso de LAG en SQL Server

Dos nuevas funciones analíticas: LAG y LEAD

A partir de la versión 2012 de SQL Server se incorporaron entre otras cosas dos funciones nuevas, que permiten acceder a la fila anterior, o posterior de una consulta.

Algo que anteriormente era imposible, y tocaba solucionar con Tablas Pivot, o usando Self Join

LAG y LEAD

Primero entendamos que hace cada una:

LAG: devuelve el valor (de una columna especificada) que estuvo en la fila anterior.

LEAD: devuelve el valor (de una columna especificada) que estuvo en la fila siguiente.

Mas info:

https://docs.microsoft.com/en-us/sql/t-sql/functions/lag-transact-sql

https://docs.microsoft.com/en-us/sql/t-sql/functions/lag-transact-sql

 

 

 

 

Realizar backup con TSQL

Realizar backup de tus bases con TSQL en MS SQL Server

Hoy veremos una manera simple de realizar un respaldo de todas nuestras bases de datos mediante un pequeño script.

Script para Backup

--------------------------------------------------
--Variables
DECLARE @Base_Datos VARCHAR(50) -- Nombre de la Base de Datos  
DECLARE @Ruta_Archivos VARCHAR(256) -- Ruta para los archivos 
DECLARE @Nombre_A VARCHAR(256) -- Nombre del Backup  (1era Parte)
DECLARE @Nombre_B VARCHAR(20) --  Nombre del Backup (2da Parte)
 
-- 1-Ubicación de los backups
SET @Ruta_Archivos = 'C:\Temp\'  
 
-- Nombre del Archivo (2da Parte) _YYYYMMDD
SELECT @Nombre_B = (SELECT '_'+CONVERT(VARCHAR(20),GETDATE(),112) +'.BAK') 
 
DECLARE Cursor_Backup CURSOR READ_ONLY FOR  
SELECT name 
FROM master.dbo.sysdatabases 
WHERE name NOT IN ('master','model','msdb','tempdb')  -- exclude these databases
 
OPEN Cursor_Backup   
FETCH NEXT FROM Cursor_Backup INTO @Base_Datos   
 
WHILE @@FETCH_STATUS = 0   
BEGIN   
   SET @Nombre_A = @Ruta_Archivos + @Base_Datos +  @Nombre_B   
   BACKUP DATABASE @Base_Datos TO DISK = @Nombre_A  
 
   FETCH NEXT FROM Cursor_Backup INTO @Base_Datos   
END   

 
CLOSE Cursor_Backup   
DEALLOCATE Cursor_Backup

Resultado de correr el script

realizar backup con tsqlQue sigue…

Se puede optimizar el script para agregar nuevas opciones de backups, como así también programar el mismo.

Crear indices faltantes

Como buscar y crear índices faltantes en nuestra base de datos

Navegando por la web me tope con esta conjunto de scripts recomendado por Microsoft que aseguran ser útil para cualquier DBA

https://gallery.technet.microsoft.com/Some-random-collection-of-b607bf1b

Como siempre son para MS SQL Server.

Entre ellos encontré uno muy útil que analiza las estadísticas de una base en producción y trate de generarte el TSQL para crear los índices recomendados faltantes

Veamos el código

/**********************************************
--Missing Index Script
**********************************************/

SELECT TOP 25
dm_mid.database_id AS DatabaseID,
dm_migs.avg_user_impact*(dm_migs.user_seeks+dm_migs.user_scans) Avg_Estimated_Impact,
dm_migs.last_user_seek AS Last_User_Seek,
OBJECT_NAME(dm_mid.OBJECT_ID,dm_mid.database_id) AS [TableName],
'CREATE INDEX [IX_' + OBJECT_NAME(dm_mid.OBJECT_ID,dm_mid.database_id) + '_'
+ REPLACE(REPLACE(REPLACE(ISNULL(dm_mid.equality_columns,''),', ','_'),'[',''),']','') +
CASE
WHEN dm_mid.equality_columns IS NOT NULL AND dm_mid.inequality_columns IS NOT NULL THEN '_'
ELSE ''
END
+ REPLACE(REPLACE(REPLACE(ISNULL(dm_mid.inequality_columns,''),', ','_'),'[',''),']','')
+ ']'
+ ' ON ' + dm_mid.statement
+ ' (' + ISNULL (dm_mid.equality_columns,'')
+ CASE WHEN dm_mid.equality_columns IS NOT NULL AND dm_mid.inequality_columns IS NOT NULL THEN ',' ELSE
'' END
+ ISNULL (dm_mid.inequality_columns, '')
+ ')'
+ ISNULL (' INCLUDE (' + dm_mid.included_columns + ')', '') AS Create_Statement
FROM sys.dm_db_missing_index_groups dm_mig
INNER JOIN sys.dm_db_missing_index_group_stats dm_migs
ON dm_migs.group_handle = dm_mig.index_group_handle
INNER JOIN sys.dm_db_missing_index_details dm_mid
ON dm_mig.index_handle = dm_mid.index_handle
WHERE dm_mid.database_ID = DB_ID()
ORDER BY Avg_Estimated_Impact DESC
GO

Resultado de correr el script

Luego de unos segundos veremos algo de este estilo:

buscando indices faltantes con tsqlPodremos ver el tiempo promedio estimado de mejora al realizar el índice, como el TSQL para crearlo.

(*) Un error que tiene el script es el nombre del índice que genera, que no debe superar los 128 caracteres, y en el mismo no lo contempla.

Por lo demás promete cumplir lo que ofrece….

Actualmente lo estoy probando en mis bases de producción como alternativa a la herramienta de análisis de rendimiento de MS SQL Server

Obtener el ultimo día del mes con SQL Server

Como obtener el último día del mes con TSQL en SQL Server

Anteriormente habíamos visto como podíamos obtener:

Primer y último día de Mes Anterior, Actual y Siguiente.

Si te interesa ( y trabajas con una versión anterior a MS SQL Server 2012), les dejo el tutorial anterior:

https://maurobernal.com.ar/blog/t-sql/primer-y-ultimo-dia-del-mes-con-sql-server-y-otros/

Pero en esta oportunidad veremos otra variante para obtener el último día del mes: actual, mes anterior y mes siguiente.

Usando la funtión EOMONTH()

A partir de Microsoft SQL Server 2012 se agregó la función EOMONTH()

https://docs.microsoft.com/en-us/sql/t-sql/functions/eomonth-transact-sql

La misma permite obtener el último día del mes

Veamos un ejemplo:

--GetDate() -> 14 Nov 2017

--Último día del mes Actual
select EOMONTH(getdate()) as UltDiaMesActual

--Último día del mes Anterior
select EOMONTH(getdate(),-1) as UltDiaMesAnterior

--Último día del mes Próximo
select EOMONTH(getdate(),1) as UltDiaMesProximo

Esto nos dará como resultado:

usando_eomonth_ejemploCon un poco de ingenio podemos obtener un poco más

A partir del código anterior, con un poco de ingenio, y con ayuda de la función DateAdd()

https://docs.microsoft.com/en-us/sql/t-sql/functions/dateadd-transact-sql

Si le agregamos un día podemos obtener el primer día del mes anterior, mes actual y mes próximo

--GetDate() -> 14 Nov 2017

--Primer día del Mes Anterior
select dateadd(d,1,EOMONTH(getdate(),-2)) as PrimerDiaMesAnterior

--Primer día del Mes Actual
select dateadd(d,1,EOMONTH(getdate(),-1)) as PrimerDiaMesActual

--Primer Dia del Mes Siguiente
select dateadd(d,1,EOMONTH(getdate())) as PrimerDiaMesSiguiente

El resultado sería el siguiente:

eomoth para primer dia del mes

 

No se olviden de comentar si les sirvió, !!! Y cualquier cosa que necesiten hacer es posible con un poco de paciencia…

Tutorial Rank y Dense_Rank

Aprende a usar Rank() y Dense_Rank()

Vamos primero con la documentación de estas dos funciones:

Rank:

https://docs.microsoft.com/en-us/sql/t-sql/functions/rank-transact-sql

Dense_Rank

https://docs.microsoft.com/en-us/sql/t-sql/functions/dense-rank-transact-sql

 

Entendiendo con un ejemplo

Primero crearemos un pequeña base de datos, que incluye un listado de vendedores, con sus ventas

IF EXISTS(SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('myTable'))
BEGIN;
    DROP TABLE [myTable];
END;
GO

CREATE TABLE [myTable] (
    [myTableID] INTEGER NOT NULL IDENTITY(1, 1),
    [Vendedor] VARCHAR(255) NULL,
    [Ventas] VARCHAR(100) NULL,
    PRIMARY KEY ([myTableID])
);
GO

INSERT INTO myTable([Vendedor],[Ventas]) VALUES('Bowen','$1.60'),('Becker','$31.71'),('Stanton','$95.81'),('Weaver','$60.26'),('Park','$65.37'),('Cook','$80.55'),('Pruitt','$60.42'),('Roberson','$20.23'),('Gilbert','$81.98'),('Emerson','$44.32');
INSERT INTO myTable([Vendedor],[Ventas]) VALUES('Harper','$1.60'),('Patrick','$98.08'),('Andrews','$35.71'),('Steele','$92.19'),('Moody','$1.90'),('Strong','$68.97'),('Rowland','$23.67'),('Barron','$79.18'),('Schultz','$80.94'),('Andrews','$1.92');
INSERT INTO myTable([Vendedor],[Ventas]) VALUES('Cleveland','$58.03'),('Head','$46.60'),('Hickman','$25.67'),('Cooke','$7.19'),('Farrell','$25.46'),('Sutton','$29.80'),('Dominguez','$85.95'),('Meadows','$70.26'),('Horne','$32.67'),('Cantu','$49.44');
INSERT INTO myTable([Vendedor],[Ventas]) VALUES('Mosley','$95.29'),('Gomez','$8.53'),('Noble','$81.25'),('Shaw','$47.67'),('Harper','$23.01'),('Reese','$2.46'),('Brown','$36.73'),('Hardin','$0.65'),('Marshall','$7.98'),('Combs','$60.59');
INSERT INTO myTable([Vendedor],[Ventas]) VALUES('Hicks','$2.78'),('Cote','$15.58'),('Knight','$91.91'),('Gilliam','$75.10'),('Leblanc','$11.16'),('Franklin','$13.92'),('Powers','$1.45'),('Clark','$36.26'),('Rose','$22.70'),('Hoover','$78.61');
INSERT INTO myTable([Vendedor],[Ventas]) VALUES('Hines','$45.97'),('Lowery','$29.76'),('Le','$63.35'),('Allison','$96.55'),('Fulton','$34.28'),('Cleveland','$46.02'),('Brewer','$1.60'),('Howe','$1.60'),('Sims','$26.47'),('Fox','$55.71');
INSERT INTO myTable([Vendedor],[Ventas]) VALUES('Bentley','$28.30'),('Hahn','$81.10'),('Colon','$62.66'),('Le','$3.42'),('House','$90.24'),('Kemp','$93.13'),('Cooper','$85.59'),('Mosley','$46.40'),('Jacobs','$1.58'),('Dickson','$76.56');
INSERT INTO myTable([Vendedor],[Ventas]) VALUES('Jacobs','$46.47'),('Moon','$77.90'),('Palmer','$52.45'),('Bray','$99.29'),('Franco','$50.64'),('Erickson','$21.77'),('Joyce','$32.19'),('Kline','$4.15'),('Barrera','$71.70'),('Jacobs','$58.56');
INSERT INTO myTable([Vendedor],[Ventas]) VALUES('Mercer','$95.18'),('Horn','$43.12'),('Mendoza','$42.59'),('Mays','$59.19'),('Shaw','$33.63'),('Sawyer','$58.80'),('Chen','$43.88'),('Dorsey','$32.79'),('Howard','$50.02'),('Marshall','$81.84');
INSERT INTO myTable([Vendedor],[Ventas]) VALUES('Casey','$2.67'),('Boyer','$39.07'),('Richards','$31.92'),('Larson','$64.99'),('Drake','$42.46'),('Holmes','$28.37'),('Hicks','$21.46'),('Richardson','$89.33'),('Evans','$8.90'),('Crawford','$11.28');

 

Contenido de esta tabla

Esta tabla desordenada contiene el listado de vendedores, con sus respectivas Ventas

select * from myTable

Base para ejemplo de Dense Rank

Uso de Rank y Dense_Rank para armar ranking

La idea de estas dos funciones es armar un ranking.

Semejanza: Ambas, en el caso de que empaten le ponen el mismo número de ranking

Diferencia: Al caso siguiente luego del empate Rank no le pone el inmediato superior, si no que mantiene la cuenta de la cantidad de elementos que lleva hasta el momento y le pone el siguiente. A diferencia de Dense_Rank que continúa con el siguiente correlativo al del empate

 

Con Rank

select *, RANK() over (Order by Ventas) as Ranking from myTable

Uso de Rank

Con Dense_Rank

select *, DENSE_RANK() over ( Order by Ventas) as Ranking from myTable

Uso de dense_rankts

Validar CBU Argentina

Validar – CBU Número de Cuenta – Argentina

Siguiendo con estas funciones de TSQL para MS SQL Server, en esta ocasión es para validar los 22 dígitos del CBU.

La Clave Bancaria Uniforme (CBU) se compone de 22 dígitos

https://es.wikipedia.org/wiki/Clave_Bancaria_Uniforme

CBU Clave Bancaria Uniforme

 

CREATE FUNCTION [dbo].[ValidarCBUdigito] 
( 
-- Add the parameters for the function here 
@block VARCHAR(100) 
) 
RETURNS TINYINT 
AS 
BEGIN 
DECLARE @subblock VARCHAR(100) 
DECLARE @posicion INTEGER 
DECLARE @digito CHAR(1) 
DECLARE @Pond CHAR(4) 
DECLARE @largo INTEGER 
DECLARE @suma INTEGER 

SELECT @suma = 0 
SELECT @largo = LEN ( @block ) 
SELECT @digito = SUBSTRING ( @block , @largo , 1 ) 
SELECT @subblock = SUBSTRING ( @block , 1 , @largo - 1 ) 
SELECT @Pond = '9713' 
SELECT @posicion = 1 

WHILE @posicion <= @largo - 1 
BEGIN 
SELECT @suma = @suma + CONVERT( INTEGER , SUBSTRING ( @subblock , @largo - @posicion , 1 )) * CONVERT( INTEGER , SUBSTRING ( @pond ,CASE (@posicion % 4) WHEN 1 THEN 4 WHEN 2 THEN 3 WHEN 3 THEN 2 WHEN 0 THEN 1 END , 1 ) ) 
SELECT @posicion = @posicion + 1 
END 


if @digito = RIGHT ( convert( varchar, 10 - ( @suma % 10 )) , 1 ) 
RETURN 1 

RETURN 0 
END

 

Validar Tarjeta de Crédito con TSQL

Validar Tarjeta de crédito

Verificar y validar el número de una Tarjeta de Crédito o Débito
La mayoría de los sellos de tarjetas de crédito (Visa, Master, Dinners, etc) usan el algoritmo de Luhn, el cual mediante un digito verificador corrobora si el resto de los números son correctos.
https://es.wikipedia.org/wiki/Algoritmo_de_Luhn

Existen diferentes formas de implementar el algoritmo. En esta ocasión les comparto el script de Derek Colley

Veamos la función para validar con TSQL

CREATE FUNCTION dbo.usp_LuhnsAlgorithm_New ( @inputString VARCHAR(20) )
RETURNS TINYINT 
AS BEGIN 
-------------------------------------------------------------------------------
-- Function to calculate whether a number is valid according to the 'MOD 10'
-- check, a.k.a. Luhn's Algorithm.
-- Author:  Derek Colley, August 2014
-- Parameters:  @inputString VARCHAR(20) 
-- Outputs:  TINYINT:    2 = an error occurred, validity undetermined
--       0 = number is not valid 
--       1 = number is valid
-------------------------------------------------------------------------------

-- first a quick check to ensure we have at least 3 numbers in the string - 
-- you can change this to any arbitrary amount, i.e. if you are just 
-- checking credit card numbers, make it 13 digits

DECLARE @result TINYINT

IF @inputString NOT LIKE ('%[0-9]%[0-9]%[0-9]%') 
 RETURN 2

-- set up our table for algorithm calculation

DECLARE @charTable TABLE ( 
 Position INT NOT NULL, 
 ThisChar CHAR(1) NOT NULL, 
 Doubled TINYINT, 
 Summed TINYINT ) 

-- convert the @inputString to a fixed width char datatype 
-- we can then process the string as a set with a known number of elements
-- this avoids RBAR substringing each char to a table in a cursor

SET @inputString = CAST(@inputString AS CHAR(20))
INSERT INTO @charTable(Position, ThisChar) 
 SELECT 1, SUBSTRING(@inputString, 1, 1) UNION ALL 
 SELECT 2, SUBSTRING(@inputString, 2, 1) UNION ALL 
 SELECT 3, SUBSTRING(@inputString, 3, 1) UNION ALL 
 SELECT 4, SUBSTRING(@inputString, 4, 1) UNION ALL 
 SELECT 5, SUBSTRING(@inputString, 5, 1) UNION ALL 
 SELECT 6, SUBSTRING(@inputString, 6, 1) UNION ALL 
 SELECT 7, SUBSTRING(@inputString, 7, 1) UNION ALL 
 SELECT 8, SUBSTRING(@inputString, 8, 1) UNION ALL 
 SELECT 9, SUBSTRING(@inputString, 9, 1) UNION ALL 
 SELECT 10, SUBSTRING(@inputString, 10, 1) UNION ALL 
 SELECT 11, SUBSTRING(@inputString, 11, 1) UNION ALL 
 SELECT 12, SUBSTRING(@inputString, 12, 1) UNION ALL 
 SELECT 13, SUBSTRING(@inputString, 13, 1) UNION ALL 
 SELECT 14, SUBSTRING(@inputString, 14, 1) UNION ALL 
 SELECT 15, SUBSTRING(@inputString, 15, 1) UNION ALL 
 SELECT 16, SUBSTRING(@inputString, 16, 1) UNION ALL 
 SELECT 17, SUBSTRING(@inputString, 17, 1) UNION ALL 
 SELECT 18, SUBSTRING(@inputString, 18, 1) UNION ALL 
 SELECT 19, SUBSTRING(@inputString, 19, 1) UNION ALL 
 SELECT 20, SUBSTRING(@inputString, 20, 1)


-- remove non-numerics inc. whitespace from the string 
DELETE FROM @charTable
WHERE  ThisChar NOT LIKE('[0-9]') 


-- unfortunately this messes up the Position indicator, 
-- so let's 'reset' this like so... 
DECLARE @tempTable TABLE ( 
 NewPosition INT IDENTITY(1,1), 
 OldPosition INT ) 
INSERT INTO @tempTable (OldPosition)
 SELECT Position 
 FROM @charTable 
 ORDER BY Position ASC 

UPDATE  @charTable
SET   Position = t2.NewPosition 
FROM  @charTable t1 
INNER JOIN  @tempTable t2 ON t1.Position = t2.OldPosition 

-- now for every 2nd digit from the right of the numeric, 
-- double it and store the result in the Doubled column 

IF ( SELECT MAX(Position) % 2 FROM @charTable ) = 0 -- evens 
BEGIN 
 UPDATE @charTable
 SET  Doubled = CAST(ThisChar AS TINYINT) * 2 
 WHERE Position % 2 <> 0 
END
ELSE BEGIN -- odds
 UPDATE @charTable 
 SET  Doubled = CAST(ThisChar AS TINYINT) * 2 
 WHERE Position % 2 = 0 
END 


-- now if the doubled digit is > 9, sum the digits, else carry forward
-- to the Summed column.  This goes for non-doubled digits too.
UPDATE @charTable
SET  Summed = 
   CASE WHEN Doubled IS NULL 
     THEN CAST(ThisChar AS TINYINT) 
     WHEN Doubled IS NOT NULL AND Doubled <= 9 
     THEN Doubled 
     WHEN Doubled IS NOT NULL AND Doubled >= 10 
     -- sum the digits.  Luckily SQL Server butchers int division...
     THEN (Doubled / 10) + (Doubled - 10) 
   END      


-- finally, sum the Summed column and if the result % 10 = 0, it's valid 
IF ( SELECT SUM(Summed) % 10 FROM @charTable ) = 0
 SET @result = 1
ELSE 
 SET @result = 0

RETURN @result 
END

Para probar:

SELECT '371449635398431' [Test String], [dbo].[usp_LuhnsAlgorithm_New]('371449635398431') [Valid Card?] UNION ALL
SELECT '3714 4963 5398 431' [Test String], [dbo].[usp_LuhnsAlgorithm_New]('3714 4963 5398 431') [Valid Card?] UNION ALL
SELECT '37XX XXXX 5398431' [Test String], [dbo].[usp_LuhnsAlgorithm_New]('37XX XXXX 5398431') [Valid Card?] UNION ALL
SELECT 'This is not a valid string' [Test String], [dbo].[usp_LuhnsAlgorithm_New]('This is not a valid string' ) [Valid Card?] UNION ALL
SELECT '1234123412341234' [Test String], [dbo].[usp_LuhnsAlgorithm_New]('1234123412341234') [Valid Card?]

Fuente:

https://www.mssqltips.com/sqlservertip/3319/implementing-luhns-algorithm-in-tsql-to-validate-credit-card-numbers/

Validar tarjeta de credito con tsql