Por que a pesquisa de LIKE N '% %' corresponde a qualquer caractere Unicode e = N' 'corresponde a muitos?

20
DECLARE @T TABLE(
  Col NCHAR(1));

INSERT INTO @T
VALUES      (N'A'),
            (N'B'),
            (N'C'),
            (N'Ƕ'),
            (N'Ƿ'),
            (N'Ǹ');

SELECT *
FROM   @T
WHERE  Col LIKE N'%�%'

Devoluções

Col
A
B
C
Ƕ
Ƿ
Ǹ

SELECT *
FROM   @T
WHERE  Col = N'�' 

Devoluções

Col
Ƕ
Ƿ
Ǹ

A geração de todos os "caracteres" possíveis de dois bytes com o abaixo mostra que a =versão corresponde a 21.229 e a LIKE N'%�%'versão (tentei alguns agrupamentos não binários com o mesmo resultado).

WITH T(I, N)
AS 
(
SELECT TOP 65536 ROW_NUMBER() OVER (ORDER BY @@SPID),
                 NCHAR(ROW_NUMBER() OVER (ORDER BY @@SPID))
FROM master..spt_values v1, 
     master..spt_values v2
)
SELECT I, N 
FROM T
WHERE N = N'�'  

Alguém capaz de lançar alguma luz sobre o que está acontecendo aqui?

O uso COLLATE Latin1_General_BINentão corresponde ao caractere único NCHAR(65533)- mas a questão é entender quais regras ele usa no outro caso. O que há de especial nesses 21.229 caracteres que correspondem ao =e por que tudo corresponde ao curinga? Presumo que exista alguma razão por trás disso que estou perdendo.

nchar(65534)[e 21k outros] funcionam tão bem quanto nchar(65533). A pergunta poderia ter sido formulada usando -se da mesma nchar(502) forma que - se comporta da mesma forma que LIKE N'%Ƕ%'(corresponde a tudo) e no =caso. Essa é provavelmente uma grande pista.

Alterar a SELECTúltima consulta para SELECT I, N, RANK() OVER(ORDER BY N)mostra que o SQL Server não pode classificar os caracteres. Parece que qualquer personagem não tratado pelo agrupamento é considerado equivalente.

Um banco de dados com um Latin1_General_100_CS_ASagrupamento produz 5840 correspondências. Latin1_General_100_CS_ASreduz =consideravelmente as partidas, mas não muda o LIKEcomportamento. Parece que há um pote de caracteres que diminuiu em agrupamentos posteriores, todos comparados da mesma forma e são ignorados nas LIKEpesquisas de caracteres curinga .

Estou usando o SQL Server 2016. O símbolo é o caractere de substituição Unicode, mas os únicos caracteres inválidos na codificação UCS-2 são 55296 - 57343 AFAIK e estão claramente combinando pontos de código perfeitamente válidos, como os N'Ԛ'que não estão nesse intervalo.

Todos esses caracteres se comportam como a string vazia para LIKEe =. Eles até avaliam como equivalente. N'' = N'�'é verdade e você pode descartá-lo em uma LIKEcomparação de espaços únicos LIKE '_' + nchar(65533) + '_'sem efeito. LENcomparações produzem resultados diferentes, portanto, provavelmente são apenas algumas funções de string.

Eu acho que o LIKEcomportamento é correto para este caso; ele se comporta como um valor desconhecido (que pode ser qualquer coisa). Isso acontece também para esses outros personagens:

  • nchar(11217) (Sinal de incerteza)
  • nchar(65532) (Caractere de substituição de objeto)
  • nchar(65533) (Personagem de substituição)
  • nchar(65534) (Não é um personagem)

Portanto, se eu quiser encontrar todos os caracteres que representam incerteza com o sinal de igual, usaria um agrupamento que suporte caracteres adicionais como Latin1_General_100_CI_AS_SC.

Eu acho que esse é o grupo de "caracteres não ponderados" mencionados na documentação, Collation e Suporte Unicode .

Martin Smith
fonte

Respostas:

8

A comparação de um "caractere" (que pode ser composto de vários pontos de código: pares substitutos, caracteres combinados etc.) com outro baseia-se em um conjunto de regras bastante complexo. É tão complexo devido à necessidade de considerar todas as várias regras (e às vezes "malucas") encontradas em todos os idiomas representados na especificação Unicode . Este sistema se aplica a agrupamentos não binários para todos os NVARCHARdados e para os VARCHARdados que estão usando um agrupamento do Windows e não um agrupamento do SQL Server (um começando com SQL_). Este sistema não se aplica aos VARCHARdados usando um agrupamento do SQL Server, pois eles usam mapeamentos simples.

A maioria das regras é definida no Algoritmo de Collation Unicode (UCA) . Algumas dessas regras, e outras não cobertas pela UCA, são:

  1. A ordem / peso padrão fornecidos no allkeys.txtarquivo (anotado abaixo)
  2. Quais sensibilidades e opções estão sendo usadas (por exemplo, é sensível a maiúsculas ou minúsculas? E, se for sensível, é maiúscula primeiro ou minúscula primeiro?)
  3. Qualquer substituição baseada em código de idioma.
  4. A versão do padrão Unicode está sendo usada.
  5. O fator "humano" (ou seja, Unicode é uma especificação, não um software e, portanto, fica a critério de cada fornecedor implementá-lo)

Enfatizei que o ponto final em relação ao fator humano deve deixar claro que não se deve esperar que o SQL Server sempre se comporte 100% de acordo com a especificação.

O fator primordial aqui é a ponderação atribuída a cada ponto de código e o fato de que vários pontos de código podem compartilhar a mesma especificação de peso. Você pode encontrar os pesos básicos (sem substituições específicas do código do idioma) aqui (acredito que a 100série de Collations é Unicode v 5.0 - confirmação informal nos comentários no item do Microsoft Connect ):

http://www.unicode.org/Public/UCA/5.0.0/allkeys.txt

O ponto de código em questão - U + FFFD - é definido como:

FFFD  ; [*0F12.0020.0002.FFFD] # REPLACEMENT CHARACTER

Essa notação é definida na seção 9.1 Formato de arquivo Allkeys do UCA:

<entry>       := <charList> ';' <collElement>+ <eol>
<charList>    := <char>+
<collElement> := "[" <alt> <weight> "." <weight> "." <weight> ("." <weight>)? "]"
<alt>         := "*" | "."

Collation elements marked with a "*" are variable.

Essa última linha é importante, pois o Code Point que estamos vendo possui uma especificação que de fato começa com "*". Na seção 3.6 Ponderação variável, existem quatro comportamentos possíveis definidos, com base nos valores de configuração do Collation aos quais não temos acesso direto (eles são codificados na implementação da Microsoft de cada Collation, como se diferencia maiúsculas de minúsculas ou maiúscula primeiro, uma propriedade que é diferente entre os VARCHARdados usando SQL_Collations e todas as outras variações).

Não tenho tempo para fazer uma pesquisa completa sobre os caminhos a serem seguidos e inferir quais opções estão sendo usadas, de modo que uma prova mais sólida possa ser fornecida, mas é seguro dizer que, dentro de cada especificação do Code Point, algo ou não é considerado "igual" nem sempre usará a especificação completa. Nesse caso, temos "0F12.0020.0002.FFFD" e, provavelmente, são apenas os níveis 2 e 3 que estão sendo usados ​​(por exemplo, 0020.0002. ). Fazendo uma "contagem" no Notepad ++ para ".0020.0002". encontra 12.581 correspondências (incluindo caracteres suplementares com os quais ainda não lidamos). Fazer uma "contagem" em "[*" retorna 4049 correspondências. Fazendo um RegEx "Find" / "Count" usando um padrão de\[\*\d{4}\.0020\.0002retorna 832 correspondências. Portanto, em algum lugar dessa combinação, além de outras regras que não estou vendo, além de alguns detalhes de implementação específicos da Microsoft, está a explicação completa desse comportamento. E, para ficar claro, o comportamento é o mesmo para todos os caracteres correspondentes, pois todos combinam entre si, pois todos têm o mesmo peso após a aplicação das regras (ou seja, essa pergunta poderia ter sido feita sobre qualquer um deles, não necessariamente Sr. ).

Você pode ver com a consulta abaixo e alterar a COLLATEcláusula conforme os resultados abaixo da consulta como as várias sensibilidades funcionam nas duas versões dos Collations:

;WITH cte AS
(
  SELECT     TOP (65536) ROW_NUMBER() OVER (ORDER BY (SELECT 0)) - 1 AS [Num]
  FROM       [master].sys.columns col
  CROSS JOIN [master].sys.objects obj
)
SELECT cte.Num AS [Decimal],
       CONVERT(VARBINARY(2), cte.Num) AS [Hex],
       NCHAR(cte.Num) AS [Character]
FROM   cte
WHERE  NCHAR(cte.Num) = NCHAR(0xFFFD) COLLATE Latin1_General_100_CS_AS_WS --N'�'
ORDER BY cte.Num;

As várias contagens de caracteres correspondentes em diferentes agrupamentos estão abaixo.

Latin1_General_100_CS_AS_WS   =   5840
Latin1_General_100_CS_AS      =   5841 (The "extra" character is U+3000)
Latin1_General_100_CI_AS      =   5841
Latin1_General_100_CI_AI      =   6311

Latin1_General_CS_AS_WS       = 21,229
Latin1_General_CS_AS          = 21,230
Latin1_General_CI_AS          = 21,230
Latin1_General_CI_AI          = 21,537

Em todos os agrupamentos listados acima, N'' = N'�'também é avaliado como verdadeiro.

ATUALIZAR

Pude fazer um pouco mais de pesquisa e aqui está o que encontrei:

Como "provavelmente" deve funcionar

Usando a demonstração de agrupamento da ICU , defino o código do idioma como "en-US-u-va-posix", defino a força como "primário", marquei a opção "chaves de classificação" e colei os 4 caracteres a seguir que copiei do resultados da consulta acima (usando o Latin1_General_100_CI_AIagrupamento):

�
Ԩ
ԩ
Ԫ

e que retorna:

Ԫ
    60 2E 02 .
Ԩ
    60 7A .
ԩ
    60 7A .
�
    FF FD .

Em seguida, verifique as propriedades dos caracteres para " " em http://unicode.org/cldr/utility/character.jsp?a=fffd e veja se a chave de classificação de nível 1 (ou seja FF FD) corresponde à propriedade "uca". Clicar nessa propriedade "uca" leva a uma página de pesquisa - http://unicode.org/cldr/utility/list-unicodeset.jsp?a=%5B%3Auca%3DFFFD%3A%5D - mostrando apenas 1 correspondência. E, no arquivo allkeys.txt , o peso da classificação de nível 1 é mostrado como 0F12, e há apenas 1 correspondência para isso.

Para ter certeza de que estamos interpretando o comportamento corretamente, olhei para outro personagem: LETRA DE CAPITAL GREGA OMICRON WITH VARIA em http://unicode.org/cldr/utility/character.jsp?a=1FF8 que possui um "uca" ( ou seja, peso de classificação de nível 1 / elemento de classificação) de 5F30. Clicar nesse "5F30" nos leva a uma página de pesquisa - http://unicode.org/cldr/utility/list-unicodeset.jsp?a=%5B%3Auca%3D5F30%3A%5D - mostrando 30 correspondências, 20 de estando na faixa de 0 - 65535 (ou seja, U + 0000 - U + FFFF). Olhando no arquivo allkeys.txt para o Code Point 1FF8 , vemos um peso de classificação de nível 1 12E0. Fazendo uma "contagem" no Notepad ++ em12E0. mostra 30 correspondências (isso corresponde aos resultados do Unicode.org, embora não seja garantido, pois o arquivo é para Unicode v 5.0 e o site está usando dados Unicode v 9.0).

No SQL Server, a seguinte consulta retorna 20 correspondências, iguais à pesquisa Unicode.org ao remover os 10 caracteres suplementares:

;WITH cte AS
(
  SELECT TOP (65535) ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS [Num]
  FROM   [master].sys.columns col
  CROSS JOIN [master].sys.objects obj
)
SELECT cte.Num AS [Decimal],
       CONVERT(VARCHAR(50), CONVERT(VARBINARY(2), cte.Num), 2) AS [Hex],
       NCHAR(cte.Num) AS [Character]
FROM cte
WHERE NCHAR(cte.Num) = NCHAR(0x1FF8) COLLATE Latin1_General_100_CI_AI
ORDER BY cte.Num;

E, apenas para ter certeza, voltando à página de Demonstração de agrupamento da ICU e substituindo os caracteres na caixa "Entrada" pelos 3 caracteres a seguir, retirados da lista de 20 resultados do SQL Server:


𝜪

mostra que, de fato, todos têm o mesmo 5F 30peso de classificação de nível 1 (correspondendo ao campo "uca" na página de propriedades do personagem).

Portanto, certamente parece que esse personagem em particular não deve corresponder a mais nada.

Como ele realmente funciona (pelo menos no Microsoft land)

Diferentemente do SQL Server, o .NET tem um meio de mostrar a chave de classificação de uma string por meio do CompareInfo.GetSortKey Method . Usando esse método e passando apenas o caractere U + FFFD, ele retorna uma chave de classificação de 0x0101010100. Em seguida, iterou sobre todos os caracteres no intervalo de 0 a 65535 para ver quais tinham uma chave de classificação das 0x0101010100correspondências 4529 retornadas. Isso não corresponde exatamente ao 5840 retornado no SQL Server (ao usar o Latin1_General_100_CS_AS_WSCollation), mas é o mais próximo que podemos chegar (por enquanto), pois estou executando o Windows 10 e o .NET Framework versão 4.6.1, que usa Unicode v 6.3.0 de acordo com o gráfico da classe CharUnicodeInfo(em "Observação para os chamadores", na seção "Comentários"). No momento, estou usando uma função SQLCLR e, portanto, não posso alterar a versão do Framework de destino. Quando tiver a chance, criarei um aplicativo de console e utilizarei uma versão de destino do Framework 4.5, que usa Unicode v 5.0, que deve corresponder aos agrupamentos da série 100.

O que esse teste mostra é que, mesmo sem o mesmo número exato de correspondências entre o .NET e o SQL Server para U + FFFD, é bastante claro que esse não é um comportamento específico do SQL Server e que, intencionalmente ou supervisionado, com a implementação realizada pela Microsoft, o caractere U + FFFD realmente corresponde a alguns caracteres, mesmo que não corresponda à especificação Unicode. E, como esse caractere corresponde a U + 0000 (nulo), provavelmente é apenas uma questão de pesos ausentes.

ALÉM DISSO

Em relação à diferença de comportamento entre a =consulta e a LIKE N'%�%'consulta, ela tem a ver com os curingas e os pesos ausentes (presumo) para esses � Ƕ Ƿ Ǹcaracteres (ou seja ). Se a LIKEcondição for alterada para simplesmente LIKE N'�', ela retornará as mesmas 3 linhas da =condição. Se o problema com os curingas não for devido a pesos "ausentes" (não há 0x00uma chave de classificação retornada por CompareInfo.GetSortKey, btw), pode ser porque esses caracteres possuam uma propriedade que permita que a chave de classificação varie com base no contexto (ou seja, caracteres adjacentes )

Solomon Rutzky
fonte
Obrigado - no allkeys.txt vinculado, parece que não há mais nada com o mesmo peso que FFFD(procurar *0F12.0020.0002.FFFDapenas retorna um resultado). Pela observação de @ Forrest de que todos eles correspondem à sequência vazia e um pouco mais de leitura sobre o assunto, parece que o peso que eles compartilham nas várias ordenações não binárias é, na verdade, zero, creio.
Martin Smith
11
@MartinSmith Fiz algumas pesquisas usando o ICU Collation Demo , além de colocar � A a \u24D0algumas outras que estavam no conjunto de resultados das 5839 correspondências. Parece que você não pode pular o primeiro peso, e esse char de substituição é o único a começar 0F12. Muitos outros também tiveram um primeiro peso único e muitos estavam ausentes no arquivo allkeys. Portanto, isso pode ser um bug de implementação devido a erro humano. Eu vi esse caractere no grupo "não suportado" no site Unicode nos gráficos de agrupamentos. Vai parecer mais amanhã.
Solomon Rutzky,
O Rextester usa 4.5. Na verdade, vejo menos correspondências nessa versão (3385). Talvez eu esteja definindo alguma opção diferente para você? Rextester.com/JBWIN31407
Martin Smith
BTW essa chave tipo de 01 01 01 01 00é mencionado aqui archives.miloush.net/michkap/archive/2007/09/10/4847780.html (parece CompareInfo.InternalGetSortKeychamadas LCMapStringEx)
Martin Smith
@MartinSmith Eu brinquei com ele um pouco, mas não tenho certeza qual é a diferença ainda. O sistema operacional em que o .NET está sendo executado é um fator importante. Vou procurar mais amanhã, se tiver tempo. Independentemente do número de correspondências, isso parece confirmar pelo menos o motivo do comportamento, especialmente agora que temos algumas dicas sobre a estrutura da chave de classificação, graças ao blog ao qual você vinculou e a outros vinculados. A página CharUnicodeInfo I ligada a menciona as chamadas de agrupamento Subjacente, que é a base para a minha sugestão aqui: connect.microsoft.com/SQLServer/feedback/details/2932336 :-)
Solomon Rutzky