Съхраняване на IP адреси в точкова четворна нотация в VARCHAR
не е най-оптималния начин за съхраняването им, тъй като dotted-quad е удобно за човека представяне на 32-битово цяло число без знак, което не се поддава на индексиране в база данни. Но понякога е фундаментално по-удобно и в малък мащаб фактът, че заявките изискват сканиране на таблица, обикновено не е проблем.
Съхранените функции на MySQL са добър начин за капсулиране на относително сложна логика зад проста функция, която може да бъде препратена в заявка, което потенциално води до по-лесни за разбиране заявки и намалява грешките при копиране/поставяне.
И така, ето съхранена функция, която написах, наречена find_ip4_in_cidr4()
. Работи донякъде подобно на вградената функция FIND_IN_SET()
-- давате му стойност и му давате "набор" (CIDR спецификация) и той връща стойност, за да посочи дали стойността е в набора.
Първо, илюстрация на функцията в действие:
Ако адресът е вътре в блока, върнете дължината на префикса. Защо да връщате дължината на префикса? Цели числа, различни от нула, са „истина“, така че можем просто да върнем 1
, но ако искате да сортирате съответстващите резултати, за да намерите най-краткия или най-дългия от множество съвпадащи префикси, можете да ORDER BY
върнатата стойност на функцията.
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)
Не сте в блока? Това връща 0 (false).
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)
Има специален случай за адреса с всички нули, връщаме -1 (все още е „вярно“, но запазва реда на сортиране):
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)
Безсмислените аргументи връщат нула:
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)
Сега кодът:
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 ;
Проблем, който не е специфичен за съхранените функции, а по-скоро се отнася за повечето функции на повечето RDBMS платформи, е, че когато колона се използва като аргумент на функция в WHERE
, сървърът не може да „погледне назад“ чрез функцията, за да използва индекс за оптимизиране на заявката.