Almacenamiento de direcciones IP: varchar (45) frente a varbinary (16)


11

Voy a crear una tabla con dos campos: IDcomo BIGINTy IPAddresscomo varchar(45)o varbinary(16). La idea es almacenar todas las direcciones IP únicas y utilizar una referencia en IDlugar de la real IP addressen otras tablas.

En general, voy a crear un procedimiento almacenado que devuelve el IDdado IP addresso (si no se encontró la dirección) insertar la dirección y devolver la generada ID.

Espero tener muchos registros (no puedo decir exactamente cuántos), pero necesito que el procedimiento almacenado anterior se ejecute lo más rápido posible. Entonces, me pregunto cómo almacenar la dirección IP real, en formato de texto o bytes. ¿Cuál va a ser mejor?

Ya he escrito SQL CLRfunciones para transformar los bytes de la dirección IP en cadena y viceversa, por lo que la transformación no es un problema (trabajar con ambos IPv4y IPv6).

Supongo que necesito crear un índice para optimizar la búsqueda, pero no estoy seguro de si debería incluir el IP addresscampo en el índice agrupado, o crear un índice separado y con qué tipo la búsqueda será más rápida.


2
Al menos para IPv4, ¿por qué no 4 tinyints? Entonces, en realidad, son legibles por humanos y no tiene que realizar ninguna conversión. También puede crear todo tipo de columnas calculadas persistentes para representar tipos específicos de búsquedas (coincidencia exacta, subred, etc.).
Aaron Bertrand

Si el caso fuera solo para IPv4, supongo que convertiría la dirección INTy usaría el campo como clave de índice. Pero porque IPv6necesito usar dos BIGINTcampos y prefiero almacenar el valor en un campo, me parece más natural.
gotqn

1
¿Todavía no entiendo por qué INT en lugar de 4 TINYINT? Mismo almacenamiento, depuración más fácil, menos tonterías, en mi humilde opinión. Si tiene dos tipos completamente diferentes con diferente validación y significado, ¿por qué necesitan usar la misma columna? Si está apostando a que una sola columna es más simple, ¿por qué no solo usa SQL_VARIANT? No tiene que preocuparse por nada. Puede almacenar fechas, cadenas y números y todos pueden tener una gran fiesta en una columna gigantesca e inútil ...
Aaron Bertrand

¿De dónde provienen las direcciones IP? ¿Alguna vez incluirán la máscara / subred (es decir, 10.10.10.1/124)? He visto esto venir de los registros del servidor web y no se traduce fácilmente a BIGINT (INT no funcionará ya que el cálculo requiere un INT sin firmar, a menos que, por supuesto, incorpore esa normalización para asumir que 0 es realmente -2.14xxxx mil millones). Supongo que la máscara de subred podría ser un campo TINYINT adicional. Pero entiendo querer almacenar como BIGINT si quiero hacer coincidir eso con un DB de latitud / longitud para mapearlos. Pero como Aaron mencionó, eso puede ser una columna computada persistente.
Solomon Rutzky

Respuestas:


12

cómo almacenar la dirección IP real, en formato de texto o bytes. ¿Cuál va a ser mejor?

Como "texto" aquí se refiere VARCHAR(45)y "bytes" se refiere VARBINARY(16), yo diría: ninguno .

Dada la siguiente información (del artículo de Wikipedia sobre IPv6 ):

Representación de dirección
Los 128 bits de una dirección IPv6 se representan en 8 grupos de 16 bits cada uno. Cada grupo se escribe como 4 dígitos hexadecimales y los grupos están separados por dos puntos (:). La dirección 2001: 0db8: 0000: 0000: 0000: ff00: 0042: 8329 es un ejemplo de esta representación.

Por conveniencia, una dirección IPv6 se puede abreviar a anotaciones más cortas mediante la aplicación de las siguientes reglas, cuando sea posible.

  • Se eliminan uno o más ceros a la izquierda de cualquier grupo de dígitos hexadecimales; esto generalmente se hace para todos o ninguno de los ceros iniciales. Por ejemplo, el grupo 0042 se convierte a 42.
  • Las secciones consecutivas de ceros se reemplazan con dos puntos dobles (: :). Los dos puntos dobles solo se pueden usar una vez en una dirección, ya que el uso múltiple haría que la dirección sea indeterminada. RFC 5952 recomienda que no se usen dos puntos dobles para denotar una sola sección omitida de ceros. [41]

Un ejemplo de aplicación de estas reglas:

        Dirección inicial: 2001: 0db8: 0000: 0000: 0000: ff00: 0042: 8329
        Después de eliminar todos los ceros iniciales en cada grupo: 2001: db8: 0: 0: 0: ff00: 42: 8329
        Después de omitir secciones consecutivas de ceros: 2001 : db8 :: ff00: 42: 8329

Comenzaría usando 8 VARBINARY(2)campos para representar los 8 grupos. Los campos para los Grupos 5 - 8 deben ser NULLya que solo se usarán para direcciones IPv6. Los campos para los Grupos 1 a 4 deben ser NOT NULLcomo se usarán para las direcciones IPv4 e IPv6.

Al mantener a cada grupo independiente (en lugar de combinarlos en uno VARCHAR(45)o uno VARBINARY(16)o incluso dos BIGINTcampos), obtiene dos beneficios principales:

  1. Es mucho más fácil reconstruir la dirección en una representación particular. De lo contrario, para reemplazar grupos consecutivos de ceros con (: :) tendrías que analizarlo. Mantenerlos separados permite declaraciones / IF/ simples para facilitar esto.IIFCASE
  2. Ahorrará una tonelada de espacio en las direcciones IPv6 activando ROW COMPRESSIONo PAGE COMPRESSION. Dado que ambos tipos de COMPRESIÓN permitirán que los campos 0x00ocupen 0 bytes, todos esos grupos de ceros ahora no le costarán nada. Por otro lado, si almacenó la dirección de ejemplo de arriba (en la cita de Wikipedia), entonces los 3 conjuntos de todos los ceros en el medio ocuparían su cantidad total de espacio (a menos que lo hiciera VARCHAR(45)y fuera con la notación reducida , pero eso podría no funcionar bien para la indexación y requeriría un análisis especial para reconstruirlo al formato completo, así que supongamos que no es una opción ;-).

SI necesita capturar la red, cree un TINYINTcampo para eso llamado, um, [Network]:-)

Para obtener más información sobre el valor de la red, aquí hay información de otro artículo de Wikipedia sobre la dirección IPv6 :

Redes

Una red IPv6 utiliza un bloque de direcciones que es un grupo contiguo de direcciones IPv6 de un tamaño que es una potencia de dos. El conjunto principal de bits de las direcciones es idéntico para todos los hosts en una red determinada y se denomina dirección de red o prefijo de enrutamiento .

Los rangos de direcciones de red se escriben en notación CIDR. Una red se denota por la primera dirección en el bloque (que termina en todos los ceros), una barra diagonal (/) y un valor decimal igual al tamaño en bits del prefijo. Por ejemplo, la red escrita como 2001: db8: 1234 :: / 48 comienza en la dirección 2001: db8: 1234: 0000: 0000: 0000: 0000: 0000 y finaliza en 2001: db8: 1234: ffff: ffff: ffff: ffff : ffff.

El prefijo de enrutamiento de una dirección de interfaz puede indicarse directamente con la dirección mediante notación CIDR. Por ejemplo, la configuración de una interfaz con la dirección 2001: db8: a :: 123 conectada a la subred 2001: db8: a :: / 64 se escribe como 2001: db8: a :: 123/64.


Para la indexación, yo diría que cree un índice no agrupado en los 8 campos de grupo, y posiblemente el campo de red si decide incluir eso.


El resultado final debería ser algo como lo siguiente:

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:

  • Reconozco que planea usar BIGINTpara el campo ID, pero ¿realmente espera capturar más de 4,294,967,295 valores únicos? Si es así, simplemente cambie el campo para que sea GRANDE e incluso puede cambiar el valor de inicialización a 0. Pero de lo contrario, será mejor que use INT y comience con el valor mínimo para que pueda usar todo el rango de ese tipo de datos .
  • Si lo desea, puede agregar una o más columnas calculadas NO persistentes a esta tabla para devolver representaciones de texto de la dirección IP.
  • Los campos del Grupo * se ordenan a propósito bajando , de 8 a 1, en la tabla para que al hacerlo SELECT *se devuelvan los campos en el orden esperado. Pero el índice los hace subir , de 1 a 8, ya que así es como se completan.
  • Un ejemplo (sin terminar) de una columna calculada para representar los valores en forma de texto es:

    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
    );
    

    Prueba:

    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
    

Para SQL Server 2005, que define las columnas que VARDECIMALsobre VARBINARYya DATA_COMPRESSIONno está disponible?
Matt

@SolomonRutzky Gracias por la explicación detallada. Tengo curiosidad, ¿cómo buscaría entre rangos de direcciones? Por ejemplo, tengo un proveedor de datos que proporciona datos de geolocalización IP en forma de una dirección IP inicial y final. Necesito encontrar en qué rango cae una IP determinada.
J Weezy

@JWeezy De nada :). ¿Cómo se almacenan las direcciones IP de inicio y fin? ¿Estás usando direcciones IPv4 o v6?
Solomon Rutzky

@SolomonRutzky Ambos. IPv4 no es un problema porque puedo almacenarlo como un entero. Desafortunadamente, no hay un número entero de 128 bits o un tipo de datos relacionado con el número en SQL Server lo suficientemente grande como para manejarlo. Entonces, para IPv6 lo estoy almacenando en VARBINARY (16) y luego uso el operador ENTRE para buscar entre rangos. Pero, estoy obteniendo múltiples resultados en los rangos de IP, que no creo que sean correctos. Me gustaría usar el mismo tipo de datos para IPv4 e IPv6 si es posible.
J Weezy

@JWeezy iba a sugerir BINARY(16);-). ¿Me puede dar un ejemplo con un rango inicial / final y al menos dos filas que recupere, una válida y al menos una inválida? Puede ser que VARbinary acorte algunos valores.
Solomon Rutzky

1

Más pequeño siempre será más rápido. Con valores más pequeños, puede colocar más de ellos en una sola página, por lo tanto, menos IO, B-Trees potencialmente menos profundos, etc.

Todas las demás cosas (sobrecarga de traducción, legibilidad, compatibilidad, carga de CPU, índice de sargabilidad, etc.) son iguales, por supuesto.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.