Lagring af IP-adresser i prikket quad-notation i en VARCHAR
er ikke den mest optimale måde at gemme dem på, da dotted-quad er en menneskevenlig repræsentation af et 32 bit usigneret heltal, der ikke egner sig til databaseindeksering. Men nogle gange er det grundlæggende mere bekvemt, og i lille skala er det faktum, at forespørgsler kræver en tabelscanning, normalt ikke et problem.
MySQL Stored Functions er en god måde at indkapsle relativt kompleks logik bag en simpel funktion, der kan refereres til i en forespørgsel, hvilket potentielt kan føre til lettere at forstå forespørgsler og reducere kopi/indsæt fejl.
Så her er en gemt funktion jeg skrev kaldet find_ip4_in_cidr4()
. Det virker lidt på samme måde som den indbyggede funktion FIND_IN_SET()
-- du giver den en værdi, og du giver den et "sæt" (CIDR spec), og den returnerer en værdi for at angive, om værdien er i sættet.
Først en illustration af funktionen i aktion:
Hvis adressen er inde i blokken, returneres præfikslængden. Hvorfor returnere præfikslængden? Heltal, der ikke er nul, er "sande", så vi kunne bare returnere 1
, men hvis du vil sortere de matchende resultater for at finde den korteste eller længste af flere matchende præfikser, kan du ORDER BY
funktionens returværdi.
mysql> SELECT find_ip4_in_cidr4('203.0.113.123','203.0.113.0/24');
+-----------------------------------------------------+
| find_ip4_in_cidr4('203.0.113.123','203.0.113.0/24') |
+-----------------------------------------------------+
| 24 |
+-----------------------------------------------------+
1 row in set (0.00 sec)
mysql> SELECT find_ip4_in_cidr4('192.168.100.1','192.168.0.0/16');
+-----------------------------------------------------+
| find_ip4_in_cidr4('192.168.100.1','192.168.0.0/16') |
+-----------------------------------------------------+
| 16 |
+-----------------------------------------------------+
1 row in set (0.00 sec)
Ikke i blokken? Det returnerer 0 (falsk).
mysql> SELECT find_ip4_in_cidr4('192.168.100.1','203.0.113.0/24');
+-----------------------------------------------------+
| find_ip4_in_cidr4('192.168.100.1','203.0.113.0/24') |
+-----------------------------------------------------+
| 0 |
+-----------------------------------------------------+
1 row in set (0.00 sec)
mysql> SELECT find_ip4_in_cidr4('192.168.100.1','192.168.0.0/24');
+-----------------------------------------------------+
| find_ip4_in_cidr4('192.168.100.1','192.168.0.0/24') |
+-----------------------------------------------------+
| 0 |
+-----------------------------------------------------+
1 row in set (0.00 sec)
Der er et særligt tilfælde for adressen helt nul, vi returnerer -1 (stadig "sand", men bevarer sorteringsrækkefølgen):
mysql> SELECT find_ip4_in_cidr4('192.168.100.1','0.0.0.0/0');
+------------------------------------------------+
| find_ip4_in_cidr4('192.168.100.1','0.0.0.0/0') |
+------------------------------------------------+
| -1 |
+------------------------------------------------+
1 row in set (0.00 sec)
Nonsens-argumenter returnerer null:
mysql> SELECT find_ip4_in_cidr4('234.467.891.0','192.168.0.0/24');
+-----------------------------------------------------+
| find_ip4_in_cidr4('234.467.891.0','192.168.0.0/24') |
+-----------------------------------------------------+
| NULL |
+-----------------------------------------------------+
1 row in set (0.00 sec)
Nu, codez:
DELIMITER $$
DROP FUNCTION IF EXISTS `find_ip4_in_cidr4` $$
CREATE DEFINER=`mezzell`@`%` FUNCTION `find_ip4_in_cidr4`(
_address VARCHAR(15),
_block VARCHAR(18)
) RETURNS TINYINT
DETERMINISTIC /* for a given input, this function always returns the same output */
CONTAINS SQL /* the function does not read from or write to tables */
BEGIN
-- given an IPv4 address and a cidr spec,
-- return -1 for a valid address inside 0.0.0.0/0
-- return prefix length if the address is within the block,
-- return 0 if the address is outside the block,
-- otherwise return null
DECLARE _ip_aton INT UNSIGNED DEFAULT INET_ATON(_address);
DECLARE _cidr_aton INT UNSIGNED DEFAULT INET_ATON(SUBSTRING_INDEX(_block,'/',1));
DECLARE _prefix TINYINT UNSIGNED DEFAULT SUBSTRING_INDEX(_block,'/',-1);
DECLARE _bitmask INT UNSIGNED DEFAULT (0xFFFFFFFF << (32 - _prefix)) & 0xFFFFFFFF;
RETURN CASE /* the first match, not "best" match is used in a CASE expression */
WHEN _ip_aton IS NULL OR _cidr_aton IS NULL OR /* sanity checks */
_prefix IS NULL OR _bitmask IS NULL OR
_prefix NOT BETWEEN 0 AND 32 OR
(_prefix = 0 AND _cidr_aton != 0) THEN NULL
WHEN _cidr_aton = 0 AND _bitmask = 0 THEN -1
WHEN _ip_aton & _bitmask = _cidr_aton & _bitmask THEN _prefix /* here's the only actual test needed */
ELSE 0 END;
END $$
DELIMITER ;
Et problem, der ikke er specifikt for lagrede funktioner, men snarere gælder for de fleste funktioner på de fleste RDBMS-platforme, er, at når en kolonne bruges som argument for en funktion i WHERE
, kan serveren ikke "se baglæns" gennem funktionen til at bruge et indeks til at optimere forespørgslen.