解決MYSQL大表問題-實戰篇(二)

#首先上表結構mysql

CREATE TABLE `sys_history` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `did` bigint(20) NOT NULL ,
  `ndata` int(11) NOT NULL DEFAULT '0' ,
  `create_time` datetime NOT NULL,
  `update_time` datetime DEFAULT NULL,
  `is_deleted` int(11) DEFAULT '0',
  PRIMARY KEY (`id`,`did`,`create_time`),
  UNIQUE KEY `hindex` (`id`,`did`,`create_time`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8
/*!50500 PARTITION BY RANGE  COLUMNS(create_time)
(PARTITION p2018061300 VALUES LESS THAN ('2018-06-13 00:00:00') ENGINE = InnoDB,
 PARTITION p2018061302 VALUES LESS THAN ('2018-06-13 02:00:00') ENGINE = InnoDB,
 PARTITION p2018061304 VALUES LESS THAN ('2018-06-13 04:00:00') ENGINE = InnoDB,
 PARTITION p2018061306 VALUES LESS THAN ('2018-06-13 06:00:00') ENGINE = InnoDB,
 PARTITION p2018061308 VALUES LESS THAN ('2018-06-13 08:00:00') ENGINE = InnoDB) */
複製代碼

在建立表的時候,首先創建了iddidcreate_time三個聯合索引,因爲個人數據中,didcreate_time都會重複,可是三個合起來就確定不會重複,因此我就選擇了建立聯合索引。至於爲什麼這樣見索引就本身google或者度娘啦,這不是本文的重點。sql

這裏我使用了Range分區,由於咱們這裏設想的是按小時建立分區,這是一個時間範圍,範圍應該連續可是不重疊,使用PARTITION BY RANGE, VALUES LESS THAN關鍵字。不使用COLUMNS關鍵字時RANGE括號內必須爲整數字段名或返回肯定整數的函數,添加COLUMNS關鍵字可定義非integer範圍及多列範圍,不過須要注意COLUMNS括號內只能是列名,不支持函數;多列範圍時,多列範圍必須呈遞增趨勢。值得注意的是我建表的時候已經預先建立了幾個分區,舉例來講:PARTITION p2018061300 VALUES LESS THAN ('2018-06-13 00:00:00') ENGINE = InnoDB,這裏我預先建立了一個名爲p2018061300的分區,其 LESS值爲2018-06-13 00:00:00,目的是想把create_time<=2018-06-13 00:00:00的數據放入改分區,p2018061300p2018061302p2018061304p2018061306從分區名也能夠看出每兩個小時一個分區,p2018061300的意思就是2018年6月13日0點以前的數據的分區。bash

自動分區

總不能每次都手動每兩小時建立分區吧...必須是自動的! 這時候存儲過程就要登場了!less

自動每兩個小時建立一個分區的存儲過程貼上來

-- 自動建立表分區的存儲過程
DROP PROCEDURE IF EXISTS AUTO_PARTITION_HOUR;

DELIMITER //
CREATE PROCEDURE AUTO_PARTITION_HOUR(IN $table_name VARCHAR(64), IN $range_hours INT,  IN $min_time VARCHAR(20))
BEGIN

	-- $table_name 待分片的表名
	-- $range_hours 每隔分區的跨度時長,例如2小時,儘可能取24能整除的數
	-- $min_time 已有歷史數據的最小時間,決定起始分片的時間,若是爲 null 則自動取當天的零點

	DECLARE $base_dir VARCHAR(64);
	DECLARE $monthly_dir  VARCHAR(64);
	DECLARE $now VARCHAR(30); -- 當前時間戳
	DECLARE $zero_am VARCHAR(30); -- 明天零點
	DECLARE $stop_hour VARCHAR(30); -- 預建立終止時間戳
	DECLARE $sql_partition_template VARCHAR(500); -- 建分片的 SQL 模板

	DECLARE $partition_name VARCHAR(20); -- 新分片名字
	DECLARE $last_less_than_hour VARCHAR(30); -- 上一個分片的 less 值
	DECLARE $less_than_hour VARCHAR(30); -- 上一個分片的 less 值
	DECLARE $sql_tmp VARCHAR(500); -- 臨時拼接的 SQL

	-- 數據文件和索引文件的存放目錄
	SET $base_dir = CONCAT('/data/mysql/hisdata/', DATABASE(), '/', $table_name, '/');

	-- 當前系統時間
	SET $now = DATE_FORMAT(now(), '%Y-%m-%d %H:%i:%s');
	-- 今天零點
	SET $zero_am = DATE_FORMAT(now(), '%Y-%m-%d %00:%00:%00');
	-- 預建立分區的終止小時值(後天零點)
	SET $stop_hour =  DATE_FORMAT(now()+interval 172800 second, '%Y-%m-%d %00:%00:%00');

	-- 建立新分片的SQL模板
	SET $sql_partition_template = CHAR(10);
	SET $sql_partition_template = CONCAT($sql_partition_template, 'ALTER TABLE ', $table_name, ' ADD PARTITION (');
	SET $sql_partition_template = CONCAT($sql_partition_template, CHAR(10), 'PARTITION $partition_name VALUES LESS THAN (');
	SET $sql_partition_template = CONCAT($sql_partition_template,'\'$less_than_hour\'',')');
	SET $sql_partition_template = CONCAT($sql_partition_template, CHAR(10), ');');

	-- 查找上一個分片的終止時間
	SET $last_less_than_hour = NULL;
	SELECT SUBSTRING(PARTITION_DESCRIPTION,2,19) INTO $last_less_than_hour FROM INFORMATION_SCHEMA.PARTITIONS
	WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=$table_name
	ORDER BY PARTITION_ORDINAL_POSITION DESC LIMIT 1;

	IF $last_less_than_hour IS NULL OR $last_less_than_hour='' THEN
		IF $min_time IS NULL THEN
			-- 沒有記錄,設置爲當天零點
			SET $last_less_than_hour = $zero_am;
		ELSE
			-- 設置爲已有最先記錄的當天零點
			SET $last_less_than_hour = DATE_FORMAT($min_time, '%Y-%m-%d %00:%00:%00');
		END IF;

	END IF;

	-- 循環預建立分區
	_PARTITION_LOOP_ : LOOP
		SET $less_than_hour = DATE_ADD($last_less_than_hour ,interval $range_hours HOUR);
		IF $less_than_hour > $stop_hour THEN
			LEAVE _PARTITION_LOOP_;
		END IF;

		SET $partition_name = CONCAT('p', DATE_FORMAT($less_than_hour, '%Y%m%d%H'));

		SET $sql_tmp = $sql_partition_template;
		SET $sql_tmp = REPLACE($sql_tmp, '$partition_name', $partition_name);
		SET $sql_tmp = REPLACE($sql_tmp, '$less_than_hour', $less_than_hour);
		SET @stmt_sql = $sql_tmp;
		PREPARE stmt FROM @stmt_sql;
		EXECUTE stmt;
		DEALLOCATE PREPARE stmt;

		SET $last_less_than_hour = $less_than_hour;
	END LOOP _PARTITION_LOOP_;

END
//
DELIMITER ;
複製代碼

存儲過程的解讀

個人存儲過程是參考網上一位大牛的,網址忘了...Sorry 大體意思就是輸入表名分片時間步進最先數據時間,而後查找上一個分片的終止時間,若是沒有記錄就設置爲當天的0點,或者最先記錄當天的0點, 若是已經有分片記錄了,就取分片的終止時間,而後循環建立分區,每循環一次小時加上$range_hours個小時,循環裏面有個條件跳出函數

IF $less_than_hour > $stop_hour THEN
			LEAVE _PARTITION_LOOP_;
		END IF;

複製代碼

這裏的$stop_hour就是以前設置的後天的0點。 這個存儲過程天天運行一次就能夠了。post

自動執行存儲過程

這個簡單啦,建立一個自動運行的Event就能夠了,上代碼google

-- 建立事件
create event auto_partition_event
	on schedule every 1 DAY starts '2018-06-12 18:30:00'
	ON COMPLETION PRESERVE ENABLE
	do
		call AUTO_PARTITION_HOUR('sys_history',2);
複製代碼

限制保留歷史數據的天數

上一篇說到了要求保留一年的歷史數據,那麼就意味着須要定時刪除一年前的數據和分區(注意:刪除分區就是刪除數據),那麼想到的又是存儲過程,上代碼spa

-- 循環刪除分區
	_CLEAR_PARTITION_LOOP_:LOOP
		-- 查找最先的一個分區的時間
		SET $most_less_than_hour = NULL;
		SELECT SUBSTRING(PARTITION_DESCRIPTION,2,19) INTO $most_less_than_hour FROM INFORMATION_SCHEMA.PARTITIONS
		WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=$table_name
		ORDER BY PARTITION_ORDINAL_POSITION LIMIT 1;

        SET $most_partition_name = CONCAT('p', DATE_FORMAT($most_less_than_hour, '%Y%m%d%H'));
		SET $most_less_than_hour = DATE_FORMAT($most_less_than_hour, '%Y-%m-%d %00:%00:%00');
		IF timestampdiff(day,$most_less_than_hour,$zero_am) < $delay_day THEN
			LEAVE _CLEAR_PARTITION_LOOP_;
		END IF;

		SET $del_sql = CHAR(10);
		SET $del_sql = CONCAT($del_sql,'ALTER TABLE ',$table_name,' DROP PARTITION ', $most_partition_name);

		SET @sd_sql = $del_sql;
		PREPARE sd FROM @sd_sql;
		EXECUTE sd;
		DEALLOCATE PREPARE sd;

	END LOOP _CLEAR_PARTITION_LOOP_;
複製代碼

建立一個循環自動循環刪除時間大於輸入保留天數的數據,這裏多了一個輸入參數$delay_day,例如我只想保留365天的數據, 那麼$delay_day輸入值就爲365。code

貼完整的存儲過程

-- 自動建立表分區的存儲過程
DROP PROCEDURE IF EXISTS AUTO_PARTITION_HOUR;

DELIMITER //
CREATE PROCEDURE AUTO_PARTITION_HOUR(IN $table_name VARCHAR(64), IN $range_hours INT,  IN $min_time VARCHAR(20),IN $delay_day INT)
BEGIN

	-- $table_name 待分片的表名
	-- $range_hours 每隔分區的跨度時長,例如2小時,儘可能取24能整除的數
	-- $min_time 已有歷史數據的最小時間,決定起始分片的時間,若是爲 null 則自動取當天的零點

	DECLARE $base_dir VARCHAR(64);
	DECLARE $monthly_dir  VARCHAR(64);
	DECLARE $now VARCHAR(30); -- 當前時間戳
	DECLARE $zero_am VARCHAR(30); -- 明天零點
	DECLARE $stop_hour VARCHAR(30); -- 預建立終止時間戳
	DECLARE $sql_partition_template VARCHAR(500); -- 建分片的 SQL 模板

	DECLARE $partition_name VARCHAR(20); -- 新分片名字
	DECLARE $last_less_than_hour VARCHAR(30); -- 上一個分片的 less 值
	DECLARE $less_than_hour VARCHAR(30); -- 上一個分片的 less 值
	DECLARE $sql_tmp VARCHAR(500); -- 臨時拼接的 SQL

	DECLARE $most_less_than_hour VARCHAR(30); -- 最先一個分區的less 值
	DECLARE $most_partition_name VARCHAR(30); -- 最先一個分區的名稱
	DECLARE $del_sql VARCHAR(100);

	-- 數據文件和索引文件的存放目錄
	SET $base_dir = CONCAT('/data/mysql/hisdata/', DATABASE(), '/', $table_name, '/');

	-- 當前系統時間
	SET $now = DATE_FORMAT(now(), '%Y-%m-%d %H:%i:%s');
	-- 今天零點
	SET $zero_am = DATE_FORMAT(now(), '%Y-%m-%d %00:%00:%00');
	-- 預建立分區的終止小時值(後天零點)
	SET $stop_hour =  DATE_FORMAT(now()+interval 172800 second, '%Y-%m-%d %00:%00:%00');

	-- 建立新分片的SQL模板
	SET $sql_partition_template = CHAR(10);
	SET $sql_partition_template = CONCAT($sql_partition_template, 'ALTER TABLE ', $table_name, ' ADD PARTITION (');
	SET $sql_partition_template = CONCAT($sql_partition_template, CHAR(10), 'PARTITION $partition_name VALUES LESS THAN (');
	SET $sql_partition_template = CONCAT($sql_partition_template,'\'$less_than_hour\'',')');
	SET $sql_partition_template = CONCAT($sql_partition_template, CHAR(10), ');');

	-- 查找上一個分片的終止小時值
	SET $last_less_than_hour = NULL;
	SELECT SUBSTRING(PARTITION_DESCRIPTION,2,19) INTO $last_less_than_hour FROM INFORMATION_SCHEMA.PARTITIONS
	WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=$table_name
	ORDER BY PARTITION_ORDINAL_POSITION DESC LIMIT 1;

	IF $last_less_than_hour IS NULL OR $last_less_than_hour='' THEN
		IF $min_time IS NULL THEN
			-- 沒有記錄,設置爲當天零點
			SET $last_less_than_hour = $zero_am;
		ELSE
			-- 設置爲已有最先記錄的當天零點
			SET $last_less_than_hour = DATE_FORMAT($min_time, '%Y-%m-%d %00:%00:%00');
		END IF;

	END IF;

	-- 循環預建立分區
	_PARTITION_LOOP_ : LOOP
		SET $less_than_hour = DATE_ADD($last_less_than_hour ,interval $range_hours HOUR);
		IF $less_than_hour > $stop_hour THEN
			LEAVE _PARTITION_LOOP_;
		END IF;

		SET $partition_name = CONCAT('p', DATE_FORMAT($less_than_hour, '%Y%m%d%H'));

		SET $sql_tmp = $sql_partition_template;
		SET $sql_tmp = REPLACE($sql_tmp, '$partition_name', $partition_name);
		SET $sql_tmp = REPLACE($sql_tmp, '$less_than_hour', $less_than_hour);
		SET @stmt_sql = $sql_tmp;
		PREPARE stmt FROM @stmt_sql;
		EXECUTE stmt;
		DEALLOCATE PREPARE stmt;

		SET $last_less_than_hour = $less_than_hour;
	END LOOP _PARTITION_LOOP_;

	-- 循環刪除分區
	_CLEAR_PARTITION_LOOP_:LOOP
		-- 查找最先的一個分區的時間
		SET $most_less_than_hour = NULL;
		SELECT SUBSTRING(PARTITION_DESCRIPTION,2,19) INTO $most_less_than_hour FROM INFORMATION_SCHEMA.PARTITIONS
		WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=$table_name
		ORDER BY PARTITION_ORDINAL_POSITION LIMIT 1;

        SET $most_partition_name = CONCAT('p', DATE_FORMAT($most_less_than_hour, '%Y%m%d%H'));
		SET $most_less_than_hour = DATE_FORMAT($most_less_than_hour, '%Y-%m-%d %00:%00:%00');
		IF timestampdiff(day,$most_less_than_hour,$zero_am) < $delay_day THEN
			LEAVE _CLEAR_PARTITION_LOOP_;
		END IF;

		SET $del_sql = CHAR(10);
		SET $del_sql = CONCAT($del_sql,'ALTER TABLE ',$table_name,' DROP PARTITION ', $most_partition_name);

		SET @sd_sql = $del_sql;
		PREPARE sd FROM @sd_sql;
		EXECUTE sd;
		DEALLOCATE PREPARE sd;

	END LOOP _CLEAR_PARTITION_LOOP_;

END
//
DELIMITER ;
複製代碼

這樣每隔一天運行一次,就能夠自動建立直到後天0點的分區,而且自動刪除過時的分區。索引

上一篇 解決MYSQL大表問題-小白篇-高手忽略 (一)

相關文章
相關標籤/搜索