Speichern von IP-Adressen - varchar (45) vs varbinary (16)

11

Ich werde eine Tabelle mit zwei Feldern erstellen - IDals BIGINTund IPAddressals entweder varchar(45)oder varbinary(16). Die Idee ist, alle eindeutigen IP-Adressen zu speichern und IDstattdessen eine Referenz zu verwenden, die IP addressin anderen Tabellen tatsächlich ist .

Im Allgemeinen werde ich eine gespeicherte Prozedur erstellen, die die IDangegebene zurückgibt IP addressoder (falls die Adresse nicht gefunden wurde) die Adresse einfügt und die generierte zurückgibt ID.

Ich erwarte viele Datensätze (ich kann nicht genau sagen, wie viele), aber ich muss die oben gespeicherte Prozedur so schnell wie möglich ausführen. Ich frage mich also, wie ich die tatsächliche IP-Adresse speichern soll - im Text- oder Byte-Format. Welches wird besser?

Ich habe bereits SQL CLRFunktionen zum Transformieren von IP-Adressbytes in Zeichenfolgen und umgekehrt geschrieben, sodass die Transformation kein Problem darstellt (Arbeiten mit beiden IPv4und IPv6).

Ich denke, ich muss einen Index erstellen, um die Suche zu optimieren, aber ich bin nicht sicher, ob ich das IP addressFeld in den Clustered-Index aufnehmen oder einen separaten Index erstellen soll und mit welchem ​​Typ die Suche schneller sein wird.

gotqn
quelle
2
Warum nicht mindestens für IPv4 4 winzige Punkte? Dann sind sie tatsächlich von Menschen lesbar und Sie müssen keine Konvertierungen durchführen. Sie können auch alle Arten von persistierten berechneten Spalten erstellen, um bestimmte Arten von Suchvorgängen darzustellen (genaue Übereinstimmung, Subnetz usw.).
Aaron Bertrand
Wenn dies nur der Fall IPv4wäre, würde ich die Adresse in konvertieren INTund das Feld als Indexschlüssel verwenden. Aber denn IPv6ich muss zwei BIGINTFelder verwenden und ich bevorzuge es, den Wert in einem Feld zu speichern - scheint mir natürlicher.
Gotqn
1
Sie verstehen immer noch nicht, warum INT statt 4 TINYINTs? Gleicher Speicher, einfacheres Debuggen, weniger Unsinn, IMHO. Wenn Sie zwei völlig unterschiedliche Typen mit unterschiedlicher Validierung und Bedeutung haben, warum müssen sie dieselbe Spalte verwenden? Wenn Sie darauf wetten, dass eine einzelne Spalte einfacher ist, warum nicht einfach SQL_VARIANT verwenden, müssen Sie sich um nichts kümmern. Sie können Daten, Zeichenfolgen und Zahlen speichern und jeder kann eine große Party in einer riesigen, nutzlosen Spalte
Aaron Bertrand
Woher kommen die IP-Adressen? Werden sie jemals die Maske / das Subnetz enthalten (dh 10.10.10.1/124)? Ich habe gesehen, dass dies aus Webserver-Protokollen stammt und sich nicht leicht in BIGINT übersetzen lässt (INT funktioniert nicht, da für die Berechnung ein nicht signiertes INT erforderlich ist, es sei denn, Sie berücksichtigen natürlich, dass die Normalisierung 0 0 -2,14xxxx Milliarden beträgt). Ich denke, die Subnetzmaske könnte nur ein zusätzliches TINYINT-Feld sein. Aber ich verstehe, dass ich als BIGINT speichern möchte, wenn ich das mit einer DB von Längen- und Breitengraden abgleichen möchte, um sie abzubilden. Aber wie Aaron erwähnte, kann das ein hartnäckiger berechneter Col sein.
Solomon Rutzky

Antworten:

12

Speichern der tatsächlichen IP-Adresse - im Text- oder Byteformat. Welches wird besser?

Da sich "Text" hier auf VARCHAR(45)und "Bytes" bezieht VARBINARY(16), würde ich sagen: weder .

Angesichts der folgenden Informationen (aus dem Wikipedia-Artikel zu IPv6 ):

Adressdarstellung
Die 128 Bits einer IPv6-Adresse werden in 8 Gruppen zu je 16 Bit dargestellt. Jede Gruppe besteht aus 4 hexadezimalen Ziffern und die Gruppen sind durch Doppelpunkte (:) getrennt. Die Adresse 2001: 0db8: 0000: 0000: 0000: ff00: 0042: 8329 ist ein Beispiel für diese Darstellung.

Der Einfachheit halber kann eine IPv6-Adresse nach Möglichkeit durch Anwendung der folgenden Regeln zu kürzeren Notationen abgekürzt werden.

  • Eine oder mehrere führende Nullen aus einer beliebigen Gruppe von hexadezimalen Ziffern werden entfernt. Dies wird normalerweise entweder für alle oder keine der führenden Nullen durchgeführt. Beispielsweise wird die Gruppe 0042 in 42 konvertiert.
  • Aufeinanderfolgende Abschnitte von Nullen werden durch einen Doppelpunkt (: :) ersetzt. Der Doppelpunkt darf nur einmal in einer Adresse verwendet werden, da eine Mehrfachverwendung die Adresse unbestimmt machen würde. RFC 5952 empfiehlt, dass kein Doppelpunkt verwendet werden darf, um einen ausgelassenen einzelnen Abschnitt von Nullen zu kennzeichnen. [41]

Ein Beispiel für die Anwendung dieser Regeln:

        Anfangsadresse: 2001: 0db8: 0000: 0000: 0000: ff00: 0042: 8329
        Nach dem Entfernen aller führenden Nullen in jeder Gruppe: 2001: db8: 0: 0: 0: ff00: 42: 8329
        Nach dem Weglassen aufeinanderfolgender Abschnitte von Nullen: 2001 : db8 :: ff00: 42: 8329

Ich würde damit beginnen, 8 VARBINARY(2)Felder zu verwenden, um die 8 Gruppen darzustellen. Die Felder für die Gruppen 5 bis 8 sollten so sein, dass NULLsie nur für IPv6-Adressen verwendet werden. Die Felder für die Gruppen 1 bis 4 sollten so sein, NOT NULLwie sie sowohl für IPv4- als auch für IPv6-Adressen verwendet werden.

Wenn Sie jede Gruppe unabhängig halten (anstatt sie in einem VARCHAR(45)oder einem VARBINARY(16)oder sogar zwei BIGINTFeldern zu kombinieren ), erhalten Sie zwei Hauptvorteile:

  1. Es ist viel einfacher, die Adresse in eine bestimmte Darstellung zu rekonstruieren. Andernfalls müssten Sie es analysieren, um aufeinanderfolgende Gruppen von Nullen durch (: :) zu ersetzen. Wenn Sie sie getrennt halten, können einfache IF/ IIF/ CASEAnweisungen verwendet werden, um dies zu erleichtern.
  2. Sie sparen eine Menge Speicherplatz auf IPv6-Adressen, indem Sie entweder ROW COMPRESSIONoder aktivieren PAGE COMPRESSION. Da beide Arten der KOMPRESSION Felder zulassen 0x00, die 0 Byte belegen sollen , kosten Sie all diese Nullengruppen jetzt nichts mehr. Wenn Sie dagegen die Beispieladresse von oben (im Wikipedia-Zitat) gespeichert haben, nehmen die 3 Sätze aller Nullen in der Mitte ihren vollen Platz ein (es sei denn, Sie haben das getan VARCHAR(45)und die reduzierte Notation verwendet , aber das funktioniert möglicherweise nicht gut für die Indizierung und erfordert eine spezielle Analyse, um es in das vollständige Format zu rekonstruieren. Nehmen wir also an, dass dies keine Option ist ;-).

Wenn Sie das Netzwerk erfassen müssen, erstellen Sie ein TINYINTFeld mit dem Namen, ähm, [Network]:-)

Weitere Informationen zum Netzwerkwert finden Sie in einem anderen Wikipedia-Artikel zur IPv6-Adresse :

Netzwerke

Ein IPv6-Netzwerk verwendet einen Adressblock, bei dem es sich um eine zusammenhängende Gruppe von IPv6-Adressen mit einer Zweierpotenz handelt. Der führende Satz von Bits der Adressen ist für alle Hosts in einem bestimmten Netzwerk identisch und wird als Netzwerkadresse oder Routing- Präfix bezeichnet .

Netzwerkadressbereiche werden in CIDR-Notation geschrieben. Ein Netzwerk wird durch die erste Adresse im Block (die mit allen Nullen endet), einen Schrägstrich (/) und einen Dezimalwert angegeben, der der Größe des Präfixes in Bits entspricht. Das als 2001: db8: 1234 :: / 48 geschriebene Netzwerk beginnt beispielsweise mit der Adresse 2001: db8: 1234: 0000: 0000: 0000: 0000: 0000 und endet mit 2001: db8: 1234: ffff: ffff: ffff: ffff : ffff.

Das Routing-Präfix einer Schnittstellenadresse kann direkt mit der Adresse durch CIDR-Notation angegeben werden. Beispielsweise wird die Konfiguration einer Schnittstelle mit der Adresse 2001: db8: a :: 123, die mit dem Subnetz 2001: db8: a :: / 64 verbunden ist, als 2001: db8: a :: 123/64 geschrieben.


Für die Indizierung würde ich sagen, dass Sie einen nicht gruppierten Index für die 8 Gruppenfelder und möglicherweise das Netzwerkfeld erstellen, wenn Sie dies einschließen möchten.


Das Endergebnis sollte ungefähr so ​​aussehen:

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

Anmerkungen:

  • Ich erkenne, dass Sie BIGINTfür das ID-Feld verwenden möchten, aber erwarten Sie wirklich, dass mehr als 4.294.967.295 eindeutige Werte erfasst werden? Wenn ja, ändern Sie einfach das Feld in BIGINT und Sie können sogar den Startwert in 0 ändern. Andernfalls ist es besser, INT zu verwenden und mit dem Mindestwert zu beginnen, damit Sie den gesamten Bereich dieses Datentyps nutzen können .
  • Falls gewünscht, können Sie dieser Tabelle eine oder mehrere nicht persistierte berechnete Spalten hinzufügen, um Textdarstellungen der IPAddress zurückzugeben.
  • Die Gruppe mit * gekennzeichneten Feldern angeordnet werden gezielt nach unten , 8-1, in der Tabelle , so dass tun SELECT *wird , die Felder in der erwarteten Reihenfolge zurück. Aber der Index gehen sie nach oben , von 1 bis 8, wie das ist , wie sie ausgefüllt sind.
  • Ein Beispiel (unvollendet) einer berechneten Spalte zur Darstellung der Werte in Textform ist:

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

    Prüfung:

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

    Ergebnis:

    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
quelle
Für SQL Server 2005, würde die Definition der Spalten , wie VARDECIMALüber VARBINARYda DATA_COMPRESSIONist nicht verfügbar?
Matt
@SolomonRutzky Vielen Dank für die ausführliche Erklärung. Ich bin gespannt, wie ich zwischen Adressbereichen suchen soll. Ich habe beispielsweise einen Datenanbieter, der IP-Geolocation-Daten in Form einer Start- und End-IP-Adresse bereitstellt. Ich muss herausfinden, in welchen Bereich eine bestimmte IP fällt.
J Weezy
@JWeezy Gern geschehen :). Wie werden die Start- und End-IP-Adressen gespeichert? Verwenden Sie IPv4- oder v6-Adressen?
Solomon Rutzky
@SolomonRutzky Beide. IPv4 ist kein Problem, da ich es als Ganzzahl speichern kann. Leider gibt es in SQL Server keinen 128-Bit-Datentyp für Ganzzahlen oder Zahlen, der groß genug ist, um damit umzugehen. Für IPv6 speichere ich es also in VARBINARY (16) und verwende dann den BETWEEN-Operator, um zwischen Bereichen zu suchen. Ich erhalte jedoch mehrere Ergebnisse zu IP-Bereichen, die ich nicht für richtig halte. Wenn möglich, möchte ich für IPv4 und IPv6 denselben Datentyp verwenden.
J Weezy
@JWeezy wollte ich vorschlagen BINARY(16);-). Können Sie mir bitte ein Beispiel mit einem Start- / Endbereich und mindestens zwei Zeilen geben, die Sie zurückerhalten, eine gültige und mindestens eine ungültige? Es kann sein, dass VARbinary einige Werte verkürzt.
Solomon Rutzky
1

Kleiner wird immer schneller sein. Mit kleineren Werten können Sie mehr davon in eine einzelne Seite einfügen, daher weniger E / A, möglicherweise flachere B-Bäume usw.

Alle anderen Dinge (Übersetzungsaufwand, Lesbarkeit, Kompatibilität, CPU-Auslastung, Index-Sargabilität usw.) sind natürlich gleich.

Michael Green
quelle