Armazenando endereços IP - varchar (45) vs varbinary (16)

11

Vou criar uma tabela com dois campos - IDcomo BIGINTe IPAddresscomo ou varchar(45)ou varbinary(16). A idéia é armazenar todos os endereços IP exclusivos e usar uma referência em IDvez do real IP addressem outras tabelas.

Geralmente, eu vou criar um procedimento armazenado que está retornando o IDdado IP addressou (se o endereço não foi encontrado) insira o endereço e retorne o gerado ID.

Espero ter muitos registros (não sei exatamente quantos), mas preciso que o procedimento armazenado acima seja executado o mais rápido possível. Então, eu estou querendo saber como armazenar o endereço IP real - no formato de texto ou bytes. Qual vai ser melhor?

Eu já escrevi SQL CLRfunções para transformar bytes de endereços IP em string e o inverso, portanto, a transformação não é um problema (trabalhando com ambos IPv4e IPv6).

Acho que preciso criar um índice para otimizar a pesquisa, mas não tenho certeza se devo incluir o IP addresscampo no índice clusterizado ou criar um índice separado e com qual tipo a pesquisa será mais rápida?

gotqn
fonte
2
Para IPv4, pelo menos, por que não 4 tinyints? Eles são legíveis por humanos e você não precisa realizar nenhuma conversão. Você também pode criar todos os tipos de colunas computadas persistentes para representar tipos específicos de pesquisas (correspondência exata, sub-rede, etc.).
Aaron Bertrand
Se fosse o caso, IPv4acho que eu converteria o endereço INTe usaria o campo como chave de índice. Mas, como IPv6preciso usar dois BIGINTcampos e prefiro armazenar o valor em um campo - me parece mais natural.
gotqn
1
Ainda não entendo por que INT em vez de 4 TINYINTs? Mesmo armazenamento, depuração mais fácil, menos absurdo, IMHO. Se você tem dois tipos completamente diferentes com validação e significado diferentes, por que eles precisam usar a mesma coluna? Se você está apostando que uma única coluna é mais simples, por que não usar SQL_VARIANT, não precisa se preocupar com nada. Você pode armazenar datas, seqüências de caracteres e números, e todos podem fazer uma grande festa em uma coluna gigantesca e inútil ...
Aaron Bertrand
De onde vêm os endereços IP? Eles incluirão a máscara / sub-rede (ou seja, 10.10.10.1/124)? Eu já vi isso acontecer nos logs do servidor da Web e não se traduz facilmente para o BIGINT (o INT não funcionará, pois o cálculo exige um INT não assinado, a menos que, é claro, você incorpore a normalização para assumir que 0 é realmente -2,14xxxx bilhões). Eu acho que a máscara de sub-rede poderia ser apenas um campo TINYINT adicional. Mas eu entendo o desejo de armazenar como BIGINT se quiser corresponder a um DB de latitude / longitude para mapeá-los. Mas, como Aaron mencionou, isso pode ser uma coluna computada persistente.
Solomon Rutzky

Respostas:

12

como armazenar o endereço IP real - em formato de texto ou bytes. Qual vai ser melhor?

Como "texto" aqui se refere VARCHAR(45)e "bytes" se refere VARBINARY(16), eu diria: nenhum .

Dadas as seguintes informações (do artigo da Wikipedia sobre IPv6 ):

Representação de endereço
Os 128 bits de um endereço IPv6 são representados em 8 grupos de 16 bits cada. Cada grupo é escrito como 4 dígitos hexadecimais e os grupos são separados por dois pontos (:). O endereço 2001: 0db8: 0000: 0000: 0000: ff00: 0042: 8329 é um exemplo dessa representação.

Por conveniência, um endereço IPv6 pode ser abreviado para notações mais curtas aplicando as regras a seguir, sempre que possível.

  • Um ou mais zeros à esquerda de qualquer grupo de dígitos hexadecimais são removidos; isso geralmente é feito para todos ou nenhum dos zeros iniciais. Por exemplo, o grupo 0042 é convertido em 42.
  • Seções consecutivas de zeros são substituídas por dois pontos duplos (: :). Os dois pontos duplos podem ser usados ​​apenas uma vez em um endereço, pois o uso múltiplo tornaria o endereço indeterminado. A RFC 5952 recomenda que um cólon duplo não deve ser usado para indicar uma única seção omitida de zeros. [41]

Um exemplo de aplicação destas regras:

        Endereço inicial: 2001: 0db8: 0000: 0000: 0000: ff00: 0042: 8329
        Após remover todos os zeros à esquerda em cada grupo: 2001: db8: 0: 0: 0: ff00: 42: 8329
        Após omitir seções consecutivas de zeros: 2001 : db8 :: ff00: 42: 8329

Eu começaria usando 8 VARBINARY(2)campos para representar os 8 grupos. Os campos dos Grupos 5 a 8 devem ser NULLcomo eles serão usados ​​apenas para endereços IPv6. Os campos dos Grupos 1 a 4 devem ser os NOT NULLque serão usados ​​para os endereços IPv4 e IPv6.

Ao manter cada grupo independente (em vez de combiná-los em VARCHAR(45)um VARBINARY(16)ou mais BIGINTcampos), você obtém dois benefícios principais:

  1. É muito mais fácil reconstruir o endereço em qualquer representação específica. Caso contrário, para substituir grupos consecutivos de zeros por (: :), você precisará analisá-lo. Mantê-los separados permite a simples IF/ IIF/ CASEdeclarações para facilitar este processo.
  2. Você economizará muito espaço nos endereços IPv6 ativando ROW COMPRESSIONou PAGE COMPRESSION. Como os dois tipos de COMPRESSION permitirão campos que 0x00ocupam 0 bytes, todos esses grupos de zeros agora não custarão nada. Por outro lado, se você armazenasse o endereço de exemplo acima (na citação da Wikipedia), os três conjuntos de todos os zeros no meio ocupariam toda a sua quantidade de espaço (a menos que você estivesse fazendo o mesmo VARCHAR(45)e seguisse a notação reduzida) , mas isso pode não funcionar bem para a indexação e exigiria uma análise especial para reconstruí-la para o formato completo, então vamos supor que essa não seja uma opção ;-)

Se você precisar capturar a rede, crie um TINYINTcampo para o chamado, hum, [Network]:-)

Para obter mais informações sobre o valor da rede, veja algumas informações de outro artigo da Wikipedia sobre o endereço IPv6 :

Redes

Uma rede IPv6 usa um bloco de endereços que é um grupo contíguo de endereços IPv6 de um tamanho que é uma potência de dois. O conjunto principal de bits dos endereços é idêntico para todos os hosts em uma determinada rede e é chamado de endereço ou prefixo de roteamento da rede .

Os intervalos de endereços de rede são gravados na notação CIDR. Uma rede é indicada pelo primeiro endereço no bloco (terminando em todos os zeros), uma barra (/) e um valor decimal igual ao tamanho em bits do prefixo. Por exemplo, a rede gravada como 2001: db8: 1234 :: / 48 inicia no endereço 2001: db8: 1234: 0000: 0000: 0000: 0000: 0000 e termina em 2001: db8: 1234: ffff: ffff: ffff: ffff : ffff.

O prefixo de roteamento de um endereço de interface pode ser indicado diretamente com o endereço pela notação CIDR. Por exemplo, a configuração de uma interface com o endereço 2001: db8: a :: 123 conectado à sub-rede 2001: db8: a :: / 64 é gravada como 2001: db8: a :: 123/64.


Para indexação, eu diria que criar um índice não clusterizado nos 8 campos do grupo e, possivelmente, o campo rede, se você decidir incluir isso.


O resultado final deve ser algo como o seguinte:

CREATE TABLE [IPAddress]
(
  IPAddressID INT          NOT NULL IDENTITY(-2147483648, 1),
  Group8      VARBINARY(2) NULL, -- IPv6 only, NULL for IPv4
  Group7      VARBINARY(2) NULL, -- IPv6 only, NULL for IPv4
  Group6      VARBINARY(2) NULL, -- IPv6 only, NULL for IPv4
  Group5      VARBINARY(2) NULL, -- IPv6 only, NULL for IPv4
  Group4      VARBINARY(2) NOT NULL, -- both
  Group3      VARBINARY(2) NOT NULL, -- both
  Group2      VARBINARY(2) NOT NULL, -- both
  Group1      VARBINARY(2) NOT NULL, -- both
  Network     TINYINT      NULL
);

ALTER TABLE [IPAddress]
  ADD CONSTRAINT [PK_IPAddress]
  PRIMARY KEY CLUSTERED
  (IPAddressID ASC)
  WITH (FILLFACTOR = 100, DATA_COMPRESSION = PAGE);

CREATE NONCLUSTERED INDEX [IX_IPAddress_Groups]
  ON [IPAddress] (Group1 ASC, Group2 ASC, Group3 ASC, Group4 ASC,
         Group5 ASC, Group6 ASC, Group7 ASC, Group8 ASC, Network ASC)
  WITH (FILLFACTOR = 100, DATA_COMPRESSION = PAGE);

Notas:

  • Reconheço que você planeja usar BIGINTo campo ID, mas você realmente espera capturar mais de 4.294.967.295 valores exclusivos? Nesse caso, basta alterar o campo para BIGINT e você pode até alterar o valor inicial para 0. Mas, caso contrário, é melhor usar INT e começar com o valor mínimo para poder usar todo o intervalo desse tipo de dados. .
  • Se desejar, você pode adicionar uma ou mais colunas computadas não resistentes a esta tabela para retornar representações de texto do endereço IP.
  • Os campos do Grupo * são organizados propositadamente para baixo , de 8 para 1, na tabela, de forma que a ação SELECT *retorne os campos na ordem esperada. Mas o índice tem-los indo para cima , 1-8, como é assim que eles são preenchidos.
  • Um exemplo (inacabado) de uma coluna computada para representar os valores em forma de texto é:

    ALTER TABLE [IPAddress]
      ADD TextAddress AS (
    IIF([Group8] IS NULL,
        -- IPv4
        CONCAT(CONVERT(TINYINT, [Group4]), '.', CONVERT(TINYINT, [Group3]), '.',
          CONVERT(TINYINT, [Group2]), '.', CONVERT(TINYINT, [Group1]),
          IIF([Network] IS NOT NULL, CONCAT('/', [Network]), '')),
        -- IPv6
        LOWER(CONCAT(
          CONVERT(VARCHAR(4), [Group8], 2), ':', CONVERT(VARCHAR(4), [Group7], 2), ':',
          CONVERT(VARCHAR(4), [Group6], 2), ':', CONVERT(VARCHAR(4), [Group5], 2), ':',
          CONVERT(VARCHAR(4), [Group4], 2), ':', CONVERT(VARCHAR(4), [Group3], 2), ':',
          CONVERT(VARCHAR(4), [Group2], 2), ':', CONVERT(VARCHAR(4), [Group1], 2),
          IIF([Network] IS NOT NULL, CONCAT('/', [Network]), '')
         ))
       ) -- end of IIF
    );

    Teste:

    INSERT INTO IPAddress VALUES (127, 0, 0, 0, 4, 22, 222, 63, NULL); -- IPv6
    INSERT INTO IPAddress VALUES (27, 10, 1234, 0, 45673, 200, 1, 6363, 48); -- IPv6
    INSERT INTO IPAddress VALUES (NULL, NULL, NULL, NULL, 192, 168, 2, 63, NULL); -- v4
    INSERT INTO IPAddress VALUES (NULL, NULL, NULL, NULL, 192, 168, 137, 29, 16); -- v4
    
    SELECT [IPAddressID], [Group8], [Group1], [Network], [TextAddress]
    FROM IPAddress ORDER BY [IPAddressID];

    Resultado:

    IPAddressID   Group8   Group1   Network  TextAddress
    -----------   ------   ------   -------  ---------------------
    -2147483646   0x007F   0x003F   NULL     007f:0000:0000:0000:0004:0016:00de:003f
    -2147483645   0x001B   0x18DB   48       001b:000a:04d2:0000:b269:00c8:0001:18db/48
    -2147483644   NULL     0x003F   NULL     192.168.2.63
    -2147483643   NULL     0x001D   16       192.168.137.29/16
Solomon Rutzky
fonte
Para o SQL Server 2005, definir as colunas como VARDECIMALterminadas VARBINARYjá que DATA_COMPRESSIONnão está disponível?
Matt
@SolomonRutzky Obrigado pela explicação detalhada. Estou curioso, como eu pesquisaria entre os intervalos de endereços? Por exemplo, eu tenho um fornecedor de dados que fornece dados de geolocalização IP na forma de um endereço IP inicial e final. Eu preciso achar que variam de um determinado IP cai no.
J Weezy
@JWeezy De nada :). Como os endereços IP inicial e final estão sendo armazenados? Você está usando endereços IPv4 ou v6?
Solomon Rutzky,
@SolomonRutzky Both. O IPv4 não é um problema, porque eu posso armazená-lo como um número inteiro. Infelizmente, não há um número inteiro de 128 bits ou um tipo de dados relacionado ao número no SQL Server grande o suficiente para lidar com isso. Portanto, para o IPv6, estou armazenando-o no VARBINARY (16) e depois uso o operador BETWEEN para pesquisar entre os intervalos. Mas estou obtendo vários resultados em intervalos de IP, que não acho corretos. Gostaria de usar o mesmo tipo de dados para IPv4 e IPv6, se possível.
precisa
@JWeezy eu ia sugerir BINARY(16);-). Você pode me dar um exemplo com um intervalo de início / fim e pelo menos duas linhas retornadas, uma válida e pelo menos uma inválida? Pode ser que o VARbinary reduz alguns valores.
Solomon Rutzky,
1

Menor sempre será mais rápido. Com valores menores, você pode ajustar mais deles em uma única página, portanto menos IO, árvores B potencialmente mais rasas etc.

Todas as outras coisas (sobrecarga de tradução, legibilidade, compatibilidade, carga da CPU, capacidade de indexação etc.) são iguais, é claro.

Michael Green
fonte