前言
面試題:如果造10w條測試數據,如何在數據庫插入10w條數據,數據不重復
最近面試經常會問到sql相關的問題,在數據庫中造測試數據是平常工作中經常會用到的場景,一般做壓力測試,性能測試也需在數據庫中先准備測試數據。那么如何批量生成大量的測試數據呢?
由於平常用python較多,所以想到用python先生成sql,再執行sql往數據庫插入數據。
使用語言:python 3.6
插入數據
首先我要插入的 SQL 語句,需每條 id 不重復 ,下面是執行單個插入語句
INSERT INTO `apps`.`apiapp_card` (`id`, `card_id`, `card_user`, `add_time`) VALUES ('1', '', 'test123', '2019-12-17');
10w 太多執行時間長,用 python 先生成 1w條測下執行時間。
首先要生成多個insert 語句,這里我用 python 語言寫段生成sql的腳本。
-
用 %s 替換需要變的字段值,如果有多個值都需要變,可以用多個%s替換對應值,我這里設計的表,只要id不一樣就可以插入成功。
-
用for 循環,每次循環 id 加1,這樣 id 就可以保證不會重復,否則插入數據庫時有重復的無法寫入成功。
-
a 是追加寫入
-
每條sql后面分號隔開
-
每次寫入數據,最后面加\n 換行
# python3
# 作者:上海-悠悠
for i in range(10000):
a = "INSERT INTO `apps`.`apiapp_card` (`id`, `card_id`, `card_user`, `add_time`) VALUES ('%s', '', 'test123', '2019-12-17');"%str(i+1)
with open("a.txt", "a") as fp:
fp.write(a+"\n")
執行python代碼,在本地生成一個 a.text 文件,打開生成的數據,部分如下
INSERT INTO `apps`.`apiapp_card` (`id`, `card_id`, `card_user`, `add_time`) VALUES ('1', '', 'test123', '2019-12-17');
INSERT INTO `apps`.`apiapp_card` (`id`, `card_id`, `card_user`, `add_time`) VALUES ('2', '', 'test123', '2019-12-17');
INSERT INTO `apps`.`apiapp_card` (`id`, `card_id`, `card_user`, `add_time`) VALUES ('3', '', 'test123', '2019-12-17');
INSERT INTO `apps`.`apiapp_card` (`id`, `card_id`, `card_user`, `add_time`) VALUES ('4', '', 'test123', '2019-12-17');
......
INSERT INTO `apps`.`apiapp_card` (`id`, `card_id`, `card_user`, `add_time`) VALUES ('10000', '', 'test123', '2019-12-17');
如果id是手機號呢,如何生成10w個不同手機號?
可以按手機號前3位開頭的號碼段生成,比如186開頭的,先用初始數據 1860000000,再這個數字基礎上每次加1
加到 18600099999,這樣號碼段1860000000-18600099999就是10w個手機號了。
把id換成手機號后,修改代碼如下
# python3
# 作者:上海-悠悠
for i in range(10000):
a = "INSERT INTO `apps`.`apiapp_card` (`id`, `card_id`, `card_user`, `add_time`) VALUES ('%s', '', 'test123', '2019-12-17');"%str(i+1860000000)
with open("a.txt", "a") as fp:
fp.write(a+"\n")
只需在上面基礎上把 str(i+1) 改成 str(i+1860000000) 就可以生成手機號了
INSERT INTO `apps`.`apiapp_card` (`id`, `card_id`, `card_user`, `add_time`) VALUES ('1860000000', '', 'test123', '2019-12-17');
INSERT INTO `apps`.`apiapp_card` (`id`, `card_id`, `card_user`, `add_time`) VALUES ('1860000001', '', 'test123', '2019-12-17');
INSERT INTO `apps`.`apiapp_card` (`id`, `card_id`, `card_user`, `add_time`) VALUES ('1860000002', '', 'test123', '2019-12-17');
把生成的文本復制出來 ,多個INSERT INTO 對應的 sql 一次性貼到 navicat 客戶端執行
執行完成花了5分鍾左右,也就是說10w條得50分鍾,這太慢了,要是數據更多,會等太久,不是我們想要的效果!
批量執行
由於單個執行,花費時間太長,現在需要優化下改成一個 inert 語句,改成批量插入數據,只寫一個 insert into 這樣一次性批量寫到數據庫,會快很多。
可以將SQL語句進行拼接,使用 insert into table () values (),(),(),()然后再一次性插入。
批量執行要么全部成功,要么一個都不會寫入成功,當寫的 SQL 語法有問題時就不會寫入成功了。
需注意:
-
拼接 sql ,多個values 值中間用英文逗號隔開
-
value 值要與數據表的字段一一對應
-
一定要注意最后一條數據后面不是逗號,改成分號
# python3
# 作者:上海-悠悠
insert_sql = "INSERT INTO `apps`.`apiapp_card` VALUES "
with open("b.txt", "a") as fp:
fp.write(insert_sql+"\n")
for i in range(10000):
a = "('%s', '', 'test123', '2019-12-17'),"%str(i+10001)
with open("b.txt", "a") as fp:
fp.write(a+"\n")
執行完成后,復制 b.text 文件的內容,需注意的是這里一定要改成 ;結尾,否則語法報錯
部分數據內容展示如下
INSERT INTO `apps`.`apiapp_card` VALUES
('10001', '', 'test123', '2019-12-17'),
('10002', '', 'test123', '2019-12-17'),
......
('20000', '', 'test123', '2019-12-17');
復制生成的 INSERT INTO 到 navicat 客戶端執行
執行完成,最后看的測試結果,1w條數據只用了0.217秒,速度明顯提高不少。
10w數據插入
接着測下,當生成10 w條數據的時候,會花多少時間?
# 作者:上海-悠悠
# python3
insert_sql = "INSERT INTO `apps`.`apiapp_card` VALUES "
with open("b.txt", "a") as fp:
fp.write(insert_sql+"\n")
for i in range(100000):
a = "('%s', '', 'test123', '2019-12-17'),"%str(i+100000)
with open("b.txt", "a") as fp:
fp.write(a+"\n")
使用python腳本執行后生成的數據如下
INSERT INTO `apps`.`apiapp_card` VALUES
('100000', '', 'test123', '2019-12-17'),
('100001', '', 'test123', '2019-12-17'),
......
('199999', '', 'test123', '2019-12-17');
直接插入mysql 這時候會有報錯:Err 1153 - Got a packet bigger than 'max_allowed_packet' bytes
報錯原因:由於數據量較大,mysql 會對單表數據量較大的 SQL 做限制,10w條數據的字符串超出了max_allowed_packet
的允許范圍。
解決辦法:需修改mysql 數據庫的max_allowed_packet的值,改大一點
max_allowed_packet
先在 navicat 輸入命令查看 max_allowed_packet 最大允許包
show global variables like 'max_allowed_packet';
查看到 value 值是 4194304, 最大限制是 40 M,我們只需的sql字符串太大了,超出了這個范圍。
在 navicat 客戶端我們無法直接修改對應 value值,需登錄到mysql,用命令行修改。
我這里 mysql 是搭建在 docker 上,需先進容器,登錄到mysql.
操作步驟如下:
- docker exec 進docker容器
- mysql -uroot -p 輸入密碼后登錄mysql
- set global max_allowed_packet=419430400; 設置最大允許包 400M
- show global variables like 'max_allowed_packet'; 查看前面設置是否生效
[root@VM_0_2_centos ~]# docker exec -it 934b30a6dc36 /bin/bash
root@934b30a6dc36:/# mysql -uroot -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 303822
Server version: 5.7.27 MySQL Community Server (GPL)
Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show global variables like 'max_allowed_packet';
+--------------------+-----------+
| Variable_name | Value |
+--------------------+-----------+
| max_allowed_packet | 4194304 |
+--------------------+-----------+
1 row in set (0.00 sec)
mysql> set global max_allowed_packet=419430400;
Query OK, 0 rows affected (0.00 sec)
mysql> show global variables like 'max_allowed_packet';
+--------------------+-----------+
| Variable_name | Value |
+--------------------+-----------+
| max_allowed_packet | 419430400 |
+--------------------+-----------+
1 row in set (0.00 sec)
mysql>
從上面的查詢結果可以看到,已經生效了。
再次重新執行上面10w條數據,查看運行結果總共花11秒左右時間。
受影響的行: 100000
時間: 11.678s
上面的方法只能臨時生效,當重啟mysql后,你會發現又還原回去了。
這里還有一種永久生效的方法,需修改my.cnf配置文件
在[mysqld]部分添加一句,如果有就修改對應的值:
- max_allowed_packet=40M
這里的值,可以用 M單位,修改后,需要重啟下mysql就可以生效了
使用python執行
如果不用 navicat 客戶端,直接用python去執行,會花多少時間呢?
先封裝連接mysql的方法,然后拼接執行的sql語句,拼接的時候需注意,最后的字符 ,需改成 ;
在執行代碼前先獲取當前的時間戳,代碼執行完成后再次獲取一次時間戳。兩次的時間間隔,就是執行的時間了,時間單位是s
python 執行 mysql 代碼參考如下
import pymysql
'''
# python3
作者:上海-悠悠
pip install PyMySQL==0.9.3
'''
dbinfo = {
"host": "192.168.1.x",
"user": "root",
"password": "123456",
"port": 3306}
class DbConnect():
def __init__(self, db_cof, database=""):
self.db_cof = db_cof
# 打開數據庫連接
self.db = pymysql.connect(database=database,
cursorclass=pymysql.cursors.DictCursor,
**db_cof)
# 使用cursor()方法獲取操作游標
self.cursor = self.db.cursor()
def select(self, sql):
# SQL 查詢語句
# sql = "SELECT * FROM EMPLOYEE \
# WHERE INCOME > %s" % (1000)
self.cursor.execute(sql)
results = self.cursor.fetchall()
return results
def execute(self, sql):
# SQL 刪除、提交、修改語句
# sql = "DELETE FROM EMPLOYEE WHERE AGE > %s" % (20)
try:
# 執行SQL語句
self.cursor.execute(sql)
# 提交修改
self.db.commit()
except:
# 發生錯誤時回滾
self.db.rollback()
def close(self):
# 關閉連接
self.db.close()
if __name__ == '__main__':
import time
insert_sql = "INSERT INTO `apps`.`apiapp_card` VALUES "
insert_values = "".join(["('%s', '', 'test123', '2019-12-17'), \n"%str(i+100000) for i in range(100000)])
# 拼接sql
sql = insert_sql + insert_values[:-3]+";"
# print(sql)
# 執行sql
time1 = time.time()
db = DbConnect(dbinfo, database="apps")
db.execute(sql)
db.close()
time2 = time.time()
print("總過耗時:%s" % (time2-time1))
使用python執行結果:總過耗時:1.0816256999969482
,結果超出我的想象,10w條數據居然只要1秒鍾!