Stockage d'adresses IP - varchar (45) vs varbinary (16)

11

Je vais créer une table avec deux champs - IDcomme BIGINTet IPAddresscomme varchar(45)ou varbinary(16). L'idée est de stocker toutes les adresses IP uniques et d'utiliser une référence à la IDplace de la réelle IP addressdans d'autres tableaux.

Généralement, je vais créer une procédure stockée qui renvoie le IDpour donné IP addressou (si l'adresse n'a pas été trouvée) insérer l'adresse et renvoyer le généré ID.

Je m'attends à avoir de nombreux enregistrements (je ne peux pas dire exactement combien), mais j'ai besoin que la procédure stockée ci-dessus soit exécutée le plus rapidement possible. Donc, je me demande comment stocker l'adresse IP réelle - au format texte ou octets. Qu'est-ce qui va être mieux?

J'ai déjà écrit des SQL CLRfonctions pour transformer les octets d'adresse IP en chaîne et inversement, donc la transformation n'est pas un problème (travailler avec les deux IPv4et IPv6).

Je suppose que je dois créer un index pour optimiser la recherche, mais je ne suis pas sûr de devoir inclure le IP addresschamp à l'index clusterisé, ou de créer un index séparé et avec quel type la recherche sera plus rapide?

gotqn
la source
2
Pour IPv4 au moins, pourquoi pas 4 minuscules? Ensuite, ils sont en fait lisibles par l'homme et vous n'avez pas à effectuer de conversions. Vous pouvez également créer toutes sortes de colonnes calculées persistantes pour représenter des types spécifiques de recherches (correspondance exacte, sous-réseau, etc.).
Aaron Bertrand
Si c'était le cas, IPv4je suppose que je convertirais l'adresse INTet utiliserais le champ comme clé d'index. Mais car IPv6j'ai besoin d'utiliser deux BIGINTchamps et je préfère stocker la valeur dans un champ - me semble plus naturel.
gotqn
1
Vous ne comprenez toujours pas pourquoi INT au lieu de 4 TINYINTs? Même stockage, débogage plus facile, moins de bêtises, à mon humble avis. Si vous avez deux types complètement différents avec une validation et une signification différentes, pourquoi ont-ils besoin d'utiliser la même colonne? Si vous pariez qu'une seule colonne est plus simple, pourquoi ne pas simplement utiliser SQL_VARIANT, alors vous n'avez à vous soucier de rien. Vous pouvez stocker des dates, des chaînes et des nombres et tout le monde peut avoir une grande fête dans une colonne gigantesque et inutile ...
Aaron Bertrand
D'où proviennent les adresses IP? Vont-ils jamais inclure le masque / sous-réseau (par exemple 10.10.10.1/124)? J'ai vu cela à travers les journaux du serveur Web et ne se traduit pas facilement en BIGINT (INT ne fonctionnera pas car le calcul nécessite un INT non signé, sauf si bien sûr, vous incorporez cette normalisation pour supposer que 0 est vraiment -2,14xxxx milliards). Je suppose que le masque de sous-réseau pourrait simplement être un champ TINYINT supplémentaire. Mais je comprends que je souhaite stocker en tant que BIGINT si je veux faire correspondre cela à une base de données de latitude / longitude pour les cartographier. Mais comme Aaron l'a mentionné, cela peut être un col calculé persistant.
Solomon Rutzky

Réponses:

12

comment stocker l'adresse IP réelle - au format texte ou octets. Qu'est-ce qui va être mieux?

Puisque "texte" fait référence ici VARCHAR(45)et "octets" fait référence VARBINARY(16), je dirais: ni l'un ni l'autre .

Compte tenu des informations suivantes (tirées d'un article de Wikipédia sur IPv6 ):

Représentation des adresses
Les 128 bits d'une adresse IPv6 sont représentés en 8 groupes de 16 bits chacun. Chaque groupe est écrit en 4 chiffres hexadécimaux et les groupes sont séparés par des deux-points (:). L'adresse 2001: 0db8: 0000: 0000: 0000: ff00: 0042: 8329 est un exemple de cette représentation.

Pour plus de commodité, une adresse IPv6 peut être abrégée en notations plus courtes en appliquant les règles suivantes, si possible.

  • Un ou plusieurs zéros de tête de n'importe quel groupe de chiffres hexadécimaux sont supprimés; cela est généralement fait pour tous ou aucun des zéros non significatifs. Par exemple, le groupe 0042 est converti en 42.
  • Les sections consécutives de zéros sont remplacées par des deux-points (: :). Le double signe deux-points ne peut être utilisé qu'une seule fois dans une adresse, car une utilisation multiple rendrait l'adresse indéterminée. La RFC 5952 recommande de ne pas utiliser deux points deux-points pour désigner une seule section omise de zéros. [41]

Un exemple d'application de ces règles:

        Adresse initiale: 2001: 0db8: 0000: 0000: 0000: ff00: 0042: 8329
        Après avoir supprimé tous les zéros de tête de chaque groupe: 2001: db8: 0: 0: 0: ff00: 42: 8329
        Après avoir omis des sections consécutives de zéros: 2001 : db8 :: ff00: 42: 8329

Je commencerais par utiliser 8 VARBINARY(2)champs pour représenter les 8 groupes. Les champs des groupes 5 à 8 doivent être NULLtels qu'ils ne seront utilisés que pour les adresses IPv6. Les champs des groupes 1 à 4 doivent être NOT NULLtels qu'ils seront utilisés pour les adresses IPv4 et IPv6.

En gardant chaque groupe indépendant (au lieu de les combiner dans un VARCHAR(45)ou un VARBINARY(16)ou même deux BIGINTdomaines), vous obtenez deux avantages principaux:

  1. Il est beaucoup plus facile de reconstruire l'adresse en n'importe quelle représentation particulière. Sinon, afin de remplacer des groupes consécutifs de zéros par (: :), vous devrez l'analyser. Les garder séparés permet des instructions simples IF/ IIF/ CASEpour faciliter cela.
  2. Vous économiserez une tonne d'espace sur les adresses IPv6 en activant ROW COMPRESSIONou PAGE COMPRESSION. Étant donné que les deux types de COMPRESSION permettront des champs qui doivent 0x00prendre 0 octets, tous ces groupes de zéros ne vous coûteront plus rien. D'un autre côté, si vous stockez l'exemple d'adresse ci-dessus (dans la citation de Wikipedia), alors les 3 ensembles de tous les zéros au milieu prendraient leur pleine quantité d'espace (à moins que vous ne le fassiez et que vous alliez VARCHAR(45)avec la notation réduite , mais cela pourrait ne pas fonctionner correctement pour l'indexation et nécessiterait une analyse spéciale pour le reconstruire au format complet, supposons donc que ce n'est pas une option ;-).

SI vous avez besoin de capturer le réseau, créez un TINYINTchamp pour celui appelé, euh, [Network]:-)

Pour plus d'informations sur la valeur du réseau, voici quelques informations d'un autre article Wikipedia sur l'adresse IPv6 :

Réseaux

Un réseau IPv6 utilise un bloc d'adresses qui est un groupe contigu d'adresses IPv6 d'une taille qui est une puissance de deux. Le premier ensemble de bits des adresses est identique pour tous les hôtes d'un réseau donné et est appelé l'adresse du réseau ou le préfixe de routage .

Les plages d'adresses réseau sont écrites en notation CIDR. Un réseau est indiqué par la première adresse du bloc (se terminant par tous les zéros), une barre oblique (/) et une valeur décimale égale à la taille en bits du préfixe. Par exemple, le réseau écrit comme 2001: db8: 1234 :: / 48 commence à l'adresse 2001: db8: 1234: 0000: 0000: 0000: 0000: 0000 et se termine à 2001: db8: 1234: ffff: ffff: ffff: ffff : ffff.

Le préfixe de routage d'une adresse d'interface peut être directement indiqué avec l'adresse par notation CIDR. Par exemple, la configuration d'une interface avec l'adresse 2001: db8: a :: 123 connectée au sous-réseau 2001: db8: a :: / 64 est écrite comme 2001: db8: a :: 123/64.


Pour l'indexation, je dirais créer un index non clusterisé sur les 8 champs Groupe, et éventuellement le champ Réseau si vous décidez de l'inclure.


Le résultat final devrait ressembler à ceci:

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

Remarques:

  • Je reconnais que vous prévoyez d'utiliser BIGINTpour le champ ID, mais vous attendez-vous vraiment à capturer plus de 4 294 967 295 valeurs uniques? Si c'est le cas, changez simplement le champ en BIGINT et vous pouvez même changer la valeur de départ en 0. Mais sinon, vous feriez mieux d'utiliser INT et de commencer avec la valeur minimale afin de pouvoir utiliser toute la plage de ce type de données .
  • Si vous le souhaitez, vous pouvez ajouter une ou plusieurs colonnes calculées NON torsadées à ce tableau pour renvoyer des représentations textuelles de l'adresse IP.
  • Les champs Groupe * sont organisés de manière intentionnelle en descendant , de 8 à 1, dans le tableau afin que faire SELECT *renvoie les champs dans l'ordre attendu. Mais l'indice les fait monter , de 1 à 8, car c'est ainsi qu'ils sont remplis.
  • Un exemple (inachevé) d'une colonne calculée pour représenter les valeurs sous forme de texte est:

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

    Tester:

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

    Résultat:

    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
la source
Pour SQL Server 2005, la définition des colonnes serait-elle VARDECIMALterminée, VARBINARYcar elle DATA_COMPRESSIONn'est pas disponible?
Matt
@SolomonRutzky Merci pour l'explication détaillée. Je suis curieux, comment rechercher entre des plages d'adresses? Par exemple, j'ai un fournisseur de données fournissant des données de géolocalisation IP sous la forme d'une adresse IP de début et de fin. Je dois trouver dans quelle plage se situe une adresse IP donnée.
J Weezy
@JWeezy Vous êtes les bienvenus :). Comment sont stockées les adresses IP de début et de fin? Utilisez-vous des adresses IPv4 ou v6?
Solomon Rutzky
@SolomonRutzky Both. IPv4 n'est pas un problème car je peux le stocker sous forme d'entier. Malheureusement, il n'y a pas de type de données 128 bits entier ou lié au nombre dans SQL Server assez grand pour le gérer. Donc, pour IPv6, je le stocke dans VARBINARY (16) puis j'utilise l'opérateur BETWEEN pour rechercher entre les plages. Mais, j'obtiens plusieurs résultats sur les plages IP, qui ne me semblent pas correctes. J'aimerais utiliser le même type de données pour IPv4 et IPv6 si possible.
J Weezy
@JWeezy j'allais suggérer BINARY(16);-). Pouvez-vous me donner un exemple avec une plage de début / fin et au moins deux lignes que vous récupérez, une valide et au moins une invalide? Il se peut que VARbinary raccourcisse certaines valeurs.
Solomon Rutzky
1

Plus petit sera toujours plus rapide. Avec des valeurs plus petites, vous pouvez en insérer plus sur une seule page, donc moins d'E / S, des arbres B potentiellement moins profonds, etc.

Toutes autres choses (surcharge de traduction, lisibilité, compatibilité, charge CPU, sargabilité d'index, etc.) étant égales, bien sûr.

Michael Green
la source