web-dev-qa-db-fra.com

Stockage des adresses IP - varchar (45) vs varbinary (16)

Je vais créer une table avec deux champs - ID comme BIGINT et IPAddress comme varchar(45) ou varbinary(16). L'idée est de stocker toutes les adresses IP uniques et d'utiliser une référence ID à la place du IP address Réel dans d'autres tableaux.

Généralement, je vais créer une procédure stockée qui renvoie le ID pour un IP address Donné ou (si l'adresse n'est pas trouvée) insérer l'adresse et renvoyer le ID généré .

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 fonctions SQL CLR Pour transformer les octets d'adresse IP en chaîne et inversement, donc la transformation n'est pas un problème (travailler avec à la fois IPv4 Et 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 champ IP address À l'index clusterisé, ou de créer un index séparé et avec quel type la recherche sera plus rapide?

11
gotqn

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

Puisque "texte" se réfère ici à VARCHAR(45) et "octets" à VARBINARY(16), je dirais: ni .

Étant donné les informations suivantes (de article Wikipedia sur IPv6 ):

Représentation d'adresse
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 un double signe 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 de deux-points pour indiquer 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 champs VARBINARY(2) pour représenter les 8 groupes. Les champs des groupes 5 à 8 doivent être NULL car ils ne seront utilisés que pour les adresses IPv6. Les champs des groupes 1 à 4 doivent être NOT NULL Car ils seront utilisés pour les adresses IPv4 et IPv6.

En gardant chaque groupe indépendant (au lieu de les combiner en une VARCHAR(45) ou une VARBINARY(16) ou même deux BIGINT champs), 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 de simples instructions IF/IIF/CASE pour faciliter cela.
  2. Vous économiserez une tonne d'espace sur les adresses IPv6 en activant ROW COMPRESSION Ou PAGE COMPRESSION. Étant donné que les deux types de COMPRESSION permettront aux champs qui sont 0x00 De prendre 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 fassiez la VARCHAR(45) et est allé avec la notation réduite, mais cela pourrait ne pas bien fonctionner pour l'indexation et nécessiterait une analyse spéciale pour la reconstruire au format complet, supposons donc que ce n'est pas une option ;-).

SI vous devez capturer le réseau, créez un champ TINYINT 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 BIGINT pour 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 par 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 persistant à ce tableau pour renvoyer des représentations textuelles de l'adresse IP.
  • Les champs Groupe * sont organisés de manière intentionnelle vers le bas , de 8 à 1, dans le tableau afin que faire SELECT * Renvoie les champs dans l'ordre attendu. Mais l'index les fait monter vers le haut , 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
    
13
Solomon Rutzky

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 bien entendu égales.

1
Michael Green