2010-09-15 21 views
12

Tengo un nvarchar bastante grande que deseo pasar a la función HashBytes. me sale el error:SQL Server 2008 y HashBytes

"String or binary would be truncated. Cannot insert the value NULL into column 'colname', tbale 'table'; column does not allow nulls. UPDATE fails. The statement has been terminated."

Ser siempre ingenioso, descubrí que esto era debido a la función HashBytes tener un límite máximo de 8000 bytes. Además me searching mostró una 'solución' donde mi gran varchar se dividiría y hash por separado y luego se combina con esta función definida por el usuario:

function [dbo].[udfLargeHashTable] (@algorithm nvarchar(4), @InputDataString varchar(MAX)) 
RETURNS varbinary(MAX) 
AS 
BEGIN 
DECLARE 
    @Index int, 
    @InputDataLength int, 
    @ReturnSum varbinary(max), 
    @InputData varbinary(max) 

SET @ReturnSum = 0 
SET @Index = 1 
SET @InputData = convert(binary,@InputDataString) 
SET @InputDataLength = DATALENGTH(@InputData) 

WHILE @Index <= @InputDataLength 
BEGIN 
    SET @ReturnSum = @ReturnSum + HASHBYTES(@algorithm, SUBSTRING(@InputData, @Index, 8000)) 
    SET @Index = @Index + 8000 
END 
RETURN @ReturnSum 
END 

que llamo con:

set @ReportDefinitionHash=convert(int,dbo.[udfLargeHashTable]('SHA1',@ReportDefinitionForLookup)) 

Dónde @ReportDefinitionHash es int, y @ReportDefinitionForLookup es el varchar

Pasar un simple carácter como 'prueba' produce una int diferente con mi UDF que una llamada normal a HashBytes produciría.

¿Algún consejo sobre este tema?

+0

Básicamente, usted no desea agregar su cadena de hash y por lo que el tipo de retorno debe ser varbinary (20). Luego, intenta ejecutar lo siguiente: 'seleccionar hashbytes ('sha1', 'test'), hashbytes ('sha1', N'test ')' (te espera una gran sorpresa) :) –

Respuesta

9

sólo tiene que utilizar esta función (tomado de Hashing large data strings with a User Defined Function):

create function dbo.fn_hashbytesMAX 
    (@string nvarchar(max) 
    , @Algo varchar(10) 
    ) 
    returns varbinary(20) 
as 
/************************************************************ 
* 
* Author:  Brandon Galderisi 
* Last modified: 15-SEP-2009 (by Denis) 
* Purpose:  uses the system function hashbytes as well 
*     as sys.fn_varbintohexstr to split an 
*     nvarchar(max) string and hash in 8000 byte 
*     chunks hashing each 8000 byte chunk,, 
*     getting the 40 byte output, streaming each 
*     40 byte output into a string then hashing 
*     that string. 
* 
*************************************************************/ 
begin 
    declare @concat  nvarchar(max) 
       ,@NumHash  int 
       ,@HASH   varbinary(20) 
    set @NumHash = ceiling((datalength(@string)/2)/(4000.0)) 
    /* HashBytes only supports 8000 bytes so split the string if it is larger */ 
    if @NumHash>1 
    begin 
                 -- # * 4000 character strings 
      ;with a as (select 1 as n union all select 1) -- 2 
       ,b as (select 1 as n from a ,a a1)  -- 4 
       ,c as (select 1 as n from b ,b b1)  -- 16 
       ,d as (select 1 as n from c ,c c1)  -- 256 
       ,e as (select 1 as n from d ,d d1)  -- 65,536 
       ,f as (select 1 as n from e ,e e1)  -- 4,294,967,296 = 17+ TRILLION characters 
       ,factored as (select row_number() over (order by n) rn from f) 
       ,factors as (select rn,(rn*4000)+1 factor from factored) 

      select @concat = cast((
      select right(sys.fn_varbintohexstr 
         (
         hashbytes(@Algo, substring(@string, factor - 4000, 4000)) 
         ) 
         , 40) + '' 
      from Factors 
      where rn <= @NumHash 
      for xml path('') 
     ) as nvarchar(max)) 


      set @HASH = dbo.fn_hashbytesMAX(@concat ,@Algo) 
    end 
    else 
    begin 
      set @HASH = convert(varbinary(20), hashbytes(@Algo, @string)) 
    end 

return @HASH 
end 

Y los resultados son los siguientes:

select 
hashbytes('sha1', N'test') --native function with nvarchar input 
,hashbytes('sha1', 'test') --native function with varchar input 
,dbo.fn_hashbytesMAX('test', 'sha1') --Galderisi's function which casts to nvarchar input 
,dbo.fnGetHash('sha1', 'test') --your function 

Salida:

0x87F8ED9157125FFC4DA9E06A7B8011AD80A53FE1 
0xA94A8FE5CCB19BA61C4C0873D391E987982FBBD3 
0x87F8ED9157125FFC4DA9E06A7B8011AD80A53FE1 
0x00000000AE6DBA4E0F767D06A97038B0C24ED720662ED9F1 
+0

Creo que hay un error aquí. Llamar a 'dbo.fn_hashbytesMAX()' con valores grandes da como resultado el mismo hash. Me parece que el tipo de parámetro '@ string' debe ser' nvarchar (max) 'en lugar de' varchar (max) ', de lo contrario, la mitad del resultado' datalength() 'no tiene sentido. Tal como está, 'datalength (@string)/2' significa que solo está procesando la mitad de subcadenas como debería. – Rory

+0

Veo originalmente que la función proporcionada era para la entrada 'nvarchar (max)' y se modificó. Cualquiera que use esto debería cambiar el tipo de datos '@ string' a' nvarchar (max) 'o cambiar el código para que funcione correctamente (lo que probablemente signifique cambiar otro nvarchar a varchar y eliminar el'/2', pero querría probar) – Rory

+0

He editado la respuesta según mis comentarios anteriores: ahora toma y calcula usando nvarchar. No generará el mismo valor que hashbytes() si se pasa un valor varchar ya que el argumento se lanza primero a nvarchar. Se cambió para que varbinary vuelva, por lo que llamar con el algoritmo md5 devuelve la longitud correcta. – Rory

1

Se podría escribir un SQL CLR función:

[Microsoft.SqlServer.Server.SqlFunction] 
public static SqlBinary BigHashBytes(SqlString algorithm, SqlString data) 
{ 
    var algo = HashAlgorithm.Create(algorithm.Value); 

    var bytes = Encoding.UTF8.GetBytes(data.Value); 

    return new SqlBinary(algo.ComputeHash(bytes)); 
} 

Y entonces se le puede llamar en SQL como esto:

--these return the same value 
select HASHBYTES('md5', 'test stuff') 
select dbo.BigHashBytes('md5', 'test stuff') 

El BigHashBytes sólo es necesario si la longitud sería de más de 8k.

+0

1) Los datos de cadena en SQL Server se almacenan como UTF-16 Little Endian, que equivale a "Unicode" en .NET. 2) No necesita meterse con 'Codificación. ' ya que SqlString puede darle el byte de Unicode [] a través de [SqlString.GetUnicodeBytes] (https://msdn.microsoft.com/en-us/library /system.data.sqltypes.sqlstring.getunicodebytes.aspx). –

14

Si no puede crear una función y tienen que utilizar algo que ya existe en la base de datos:

sys.fn_repl_hash_binary 

se pueden hacer para trabajar con la sintaxis:

sys.fn_repl_hash_binary(cast('some really long string' as varbinary(max))) 

Tomado de: http://www.sqlnotes.info/2012/01/16/generate-md5-value-from-big-data/

+0

NB: solo disponible desde SQL Server 2008 en adelante – Rory

+0

No funcionará si tiene datos de utf-8 - 'NVARCHAR' cadena – gotqn

+1

SQL Server no utiliza cadenas utf-8. No tuve problemas con las cadenas NVARCHAR. –

0

Esto puede ser utilizado como cuerpo de la función, también:

DECLARE @A NVARCHAR(MAX) = N'test' 

DECLARE @res VARBINARY(MAX) = 0x 
DECLARE @position INT = 1 
     ,@len INT = DATALENGTH(@A) 

WHILE 1 = 1 
BEGIN 
    SET @res = @res + HASHBYTES('SHA2_256', SUBSTRING(@A, @position, 4000)) 
    SET @position = @position+4000 
    IF @Position > @len 
     BREAK 
END 

SELECT HASHBYTES('SHA2_256',@res) 

La idea si a HASH cada 4000 parte de la cadena NVARCHAR(MAX) y concatanate los resultados. Luego a HASH el último resultado.

1

probado y trabajando seleccione master.sys.fn_repl_hash_binary (someVarbinaryMaxValue) por otra parte no complicada :)

0

Parece la solución más fácil es escribir un algoritmo de hash recursiva que analiza el valor de texto de entrada en sub varchar(8000) segmentos. arbitrariamente elegí para cortar la cadena de entrada en segmentos 7500 caracteres El algoritmo de cálculo devuelve un varbinary(20) que se puede convertir fácilmente en un varchar(20)

ALTER FUNCTION [dbo].[BigHash] 
( 
    @TextValue nvarchar(max) 
) 

RETURNS varbinary(20) 

AS 
BEGIN 

    if @TextValue = null 
     return hashbytes('SHA1', 'null') 


    Declare @FirstPart as varchar(7500) 
    Declare @Remainder as varchar(max) 

    Declare @RemainderHash as varbinary(20) 
    Declare @BinaryValue as varbinary(20) 

    Declare @TextLength as integer 


    Set @TextLength = len(@TextValue) 

    if @TextLength > 7500 
     Begin 
      Set @FirstPart = substring(@TextValue, 1, 7500)   

      Set @Remainder = substring(@TextValue, 7501, @TextLength - 7500)   

      Set @RemainderHash = dbo.BigHash(@Remainder) 

      Set @BinaryValue = hashbytes('SHA1', @FirstPart + convert(varchar(20), @RemainderHash, 2)) 

      return @BinaryValue 

     End 
    else 
     Begin 
      Set @FirstPart = substring(@TextValue, 1, @TextLength)      
      Set @BinaryValue = hashbytes('SHA1', @FirstPart) 

      return @BinaryValue 
     End 


    return null 

END 
6

Me he tomado la respuesta aceptada, y lo modificó un poco con el siguientes mejoras:

  1. ya no recursiva de la función
  2. ahora esquema unido
  3. ya no confiar en st indocumentado ored procedures
  4. dos versiones: una para nvarchar, una para varchar
  5. devuelve el mismo tamaño de datos que HASHBYTES, dejando que el usuario final lo convierta en más pequeño según el algoritmo utilizado. Esto permite que las funciones admitan futuros algoritmos con mayores retornos de datos.

Con estos cambios, las funciones que ahora se pueden utilizar en columnas calculadas persistió ya que ahora están marcados determinista cuando se creó.

CREATE FUNCTION dbo.fnHashBytesNVARCHARMAX 
(
    @Algorithm VARCHAR(10), 
    @Text NVARCHAR(MAX) 
) 
RETURNS VARBINARY(8000) 
WITH SCHEMABINDING 
AS 
BEGIN 
    DECLARE @NumHash INT; 
    DECLARE @HASH VARBINARY(8000); 
    SET @NumHash = CEILING(DATALENGTH(@Text)/(8000.0)); 
    /* HashBytes only supports 8000 bytes so split the string if it is larger */ 
    WHILE @NumHash > 1 
    BEGIN 
     -- # * 4000 character strings 
     WITH a AS 
     (SELECT 1 AS n UNION ALL SELECT 1), -- 2 
     b AS 
     (SELECT 1 AS n FROM a, a a1),  -- 4 
     c AS 
     (SELECT 1 AS n FROM b, b b1),  -- 16 
     d AS 
     (SELECT 1 AS n FROM c, c c1),  -- 256 
     e AS 
     (SELECT 1 AS n FROM d, d d1),  -- 65,536 
     f AS 
     (SELECT 1 AS n FROM e, e e1),  -- 4,294,967,296 = 17+ TRILLION characters 
     factored AS 
     (SELECT ROW_NUMBER() OVER (ORDER BY n) rn FROM f), 
     factors AS 
     (SELECT rn, (rn * 4000) + 1 factor FROM factored) 
     SELECT @Text = CAST 
      (
       (
        SELECT CONVERT(VARCHAR(MAX), HASHBYTES(@Algorithm, SUBSTRING(@Text, factor - 4000, 4000)), 1) 
        FROM factors 
        WHERE rn <= @NumHash 
        FOR XML PATH('') 
       ) AS NVARCHAR(MAX) 
      ); 

     SET @NumHash = CEILING(DATALENGTH(@Text)/(8000.0)); 
    END; 
    SET @HASH = CONVERT(VARBINARY(8000), HASHBYTES(@Algorithm, @Text)); 
    RETURN @HASH; 
END; 

CREATE FUNCTION dbo.fnHashBytesVARCHARMAX 
(
    @Algorithm VARCHAR(10), 
    @Text VARCHAR(MAX) 
) 
RETURNS VARBINARY(8000) 
WITH SCHEMABINDING 
AS 
BEGIN 
    DECLARE @NumHash INT; 
    DECLARE @HASH VARBINARY(8000); 
    SET @NumHash = CEILING(DATALENGTH(@Text)/(8000.0)); 
    /* HashBytes only supports 8000 bytes so split the string if it is larger */ 
    WHILE @NumHash > 1 
    BEGIN 
     -- # * 4000 character strings 
     WITH a AS 
     (SELECT 1 AS n UNION ALL SELECT 1), -- 2 
     b AS 
     (SELECT 1 AS n FROM a, a a1),  -- 4 
     c AS 
     (SELECT 1 AS n FROM b, b b1),  -- 16 
     d AS 
     (SELECT 1 AS n FROM c, c c1),  -- 256 
     e AS 
     (SELECT 1 AS n FROM d, d d1),  -- 65,536 
     f AS 
     (SELECT 1 AS n FROM e, e e1),  -- 4,294,967,296 = 17+ TRILLION characters 
     factored AS 
     (SELECT ROW_NUMBER() OVER (ORDER BY n) rn FROM f), 
     factors AS 
     (SELECT rn, (rn * 8000) + 1 factor FROM factored) 
     SELECT @Text = CAST 
     (
      (
       SELECT CONVERT(VARCHAR(MAX), HASHBYTES(@Algorithm, SUBSTRING(@Text, factor - 8000, 8000)), 1) 
       FROM factors 
       WHERE rn <= @NumHash 
       FOR XML PATH('') 
      ) AS NVARCHAR(MAX) 
     ); 

     SET @NumHash = CEILING(DATALENGTH(@Text)/(8000.0)); 
    END; 
    SET @HASH = CONVERT(VARBINARY(8000), HASHBYTES(@Algorithm, @Text)); 
    RETURN @HASH; 
END;