目录
一、背景
1.1、insert ignore
1.2、replace
1.3、查询除j,k外其他字段重复的语句
二、解决方案
一、背景
该部分进行了问题复盘和思路过程整理,如果想直接看解决思路,可选择目录,跳过背景部分。
1.1、insert ignore
先说业务场景,公司的某个业务是通过生成Excel然后读取Excel插入到数据库实现的,但是由于各种原因,Excel有些小问题,可能会手动修改一些字段,我必须把这部分数据也读进来。举个例子:

以上数据除了JK,其他都不会手动修改,我插入数据的方式就是定时读取Excel表,然后使用insert ignore into方式插入,把A-K全部设置了索引(因为数据高度相似,不得不这么做),这样当发现一样的数据时,就会忽略不插入。
读取代码如下(python3):
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73import openpyxl import mysql.connector def read_excel(file_name): # 打开excel文件,获取工作簿对象 wb1 = openpyxl.load_workbook(file_name) # 从工作薄中获取表单(sheet)对象 sheets = wb1.sheetnames print('该Excel文件包含工作表如下:', sheets, type(sheets)) sheet1 = wb1.active return sheet1 # 将Excel数据转化为元组列表 def excel_to_tuple_list(sheet1): sheet1_max_row = sheet1.max_row sheet1_max_column = sheet1.max_column print("最大行:{}".format(sheet1_max_row)) print("最大列:{}".format(sheet1_max_column)) # 定义一个元组列表,用来存放Excel数据 # 一行就是一个元组,100行row就是100个元组,第一行不读,故-1 tuple_list_excel_data = [0 for i2 in range(sheet1_max_row - 1)] # 首行表头忽略,从第二行开始读 for i1 in range(2, sheet1_max_row + 1): # 将每行第一个数据放入元组 tuple1 = (sheet1.cell(i1, 1).value,) # 从每行第二个数开始 for j1 in range(2, sheet1_max_column + 1): # 之后每次添加一个元素,直到该行结束 tuple2 = (sheet1.cell(i1, j1).value,) tuple1 = tuple1 + tuple2 # 处理完毕后将该元组放入列表,列表从0开始 tuple_list_excel_data[i1 - 2] = tuple1 # 检查列表数据是否完善 for tuple3 in tuple_list_excel_data: print(tuple3) return tuple_list_excel_data def send_mysql(tuple_list): # 连接到MySQL db1 = mysql.connector.connect( host="localhost", user="root", passwd="xxxxxx", database='aaa_bbb_123' ) cursor1 = db1.cursor() cursor1.execute("SHOW TABLES") for x in cursor1: print('将插入MySQL表:', x) sql = ("INSERT ignore INTO abcd (a,b,c,d,e,f,g,h,i,j,k) " + "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)") val = tuple_list cursor1.executemany(sql, val) db1.commit() # 数据表内容有更新,必须使用到该语句 print(cursor1.rowcount, "记录插入成功。n") send_mysql(excel_to_tuple_list(read_excel("./新建Excel.xlsx")))
(定时读取部分忽略,不是重点)
设置的索引如下:
alter table aaa_bbb_123.abcd add unique index abcd1(a,b,c,d,e,f,g,h,i,j,k);
插入效果如下:

数据都读到了,看起来不错。
1.2、replace
就这样运行一段时间以后,业务跟我说,手动修改的内容,数据库会多一条修改的数据,

红色的数据,除了j为13,其他的和绿色的一模一样,业务说因为j和k是会手动修改的,但是明明Excel表已经没有j=10这条数了,数据库里却还有。

细心的你一定发现,id断层了,但是粗心的我此时还没有发现,对业务的问题,我略做思考,决定这么解决:
使用replace代替insert ignore,然后把索引设置为A-I,也就是说,我不管你JK的内容是什么,只要A-I是一样的,我就认为是同一条数据,然后直接replace替换掉。
这样做改动比较少,上面的代码不需要动,只需要改一个地方即可,也就是:
将函数send_mysql中的
复制代码
1
2sql = ("INSERT ignore INTO abcd (a,b,c,d,e,f,g,h,i,j,k) " + "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)")
改为
复制代码
1
2sql = ("replace INTO abcd (a,b,c,d,e,f,g,h,i,j,k) " + "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)")
是的,你没有看错,只是将insert ignore改成了replace,代码部分的修改就已经完成。接下来就是索引部分的修改,由于我不知道怎么删除已有索引中的部分字段,所以做法是直接删除原索引,并增加新索引:
删除原索引
DROP INDEX abcd1 ON aaa_bbb_123.abcd;
增加新索引
alter table aaa_bbb_123.abcd add unique index abcd2(a,b,c,d,e,f,g,h,i);
注意增加新索引时,MySQL提示我有重复数据,原因是之前插入的那两条数据在索引部分是完全相同的,这时需要删除重复数据,为了简便,我直接把整张表包括id全部清空(我只是测试,正式环境千万别这么做)
truncate table aaa_bbb_123.abcd;
然后再执行添加索引操作即可成功,显示abcd表的索引如下
show index from aaa_bbb_123.abcd;

OK,都已经做完,来执行程序吧。
为了检验效果,我先把这张表读进去

然后把13改成14,再读一次

看看效果:

似乎不错,13那条数据不见了,取而代之的是14那条数据,效果也实现了,一切看起来都挺正常,但是角落那个奇怪的id,为什么会是4,5,6呢?
因为id不会影响业务,我也就没去管它,直到几个月后,业务跟我说,id已经增长到了2000w。。。
我查询了一下整个库的数据量,总共才40w条数据,id怎么会增长那么多?
然后我统计了一下各个id段数据的分布情况,结果如下:

查询的语句:
select count(*) from xxx.xxx where id<10000000;
select count(*) from xxx.xxx where id>10000000 and id<12000000;
也就是说,0-1000w的id里面,只有36条数据,中间大部分都是空的,为什么呢?
因为replace的机制,当发现a-i字段完全相同的数据,它就删除原数据,然后在新的id下放这条数据,也就是说,每读取一次表格,都会增加几十甚至上百的id,加上前期测试,不断的全部读取,几百张表,也就是每一轮都会增加20w个id,读个几轮下来,轻轻松松上千万。
跟领导沟通了这个事情以后,觉得这么做,就是我还是用insert ignore,然后索引改回A-K,也就是全部索引,然后增加一个字段,filter_data,字段类型:tinyint(1) 默认值:0,当我插入数据的时候,无论有无修改,都添加进去,然后做一次select,当发现A-I字段内容完全相同的数据,我就把旧数据改成filter_data=1,这样业务查询的时候只查filter_data=0的数据即可,这样做还有一个好处,那就是当业务想看修改前的数据时,也可以看,不会丢失原始信息。
1.3、查询除j,k外其他字段重复的语句
在做这一步之前,需要把索引加回来,也就是j,k字段也要加入索引,否则不会有重复的数据。

再把程序改回insert ignore,运行后结果如下:


好,现在数据库里有两条除了j以外完全相同的数据了,那就是id为4和id为7的两条数据,现在我要通过指令的方式找出它们:
select * from aaa_bbb_123.abcd where (a,b,c,d,e,f,g,h,i) in (
select a,b,c,d,e,f,g,h,i from aaa_bbb_123.abcd
group by a,b,c,d,e,f,g,h,i having count(*) > 1)
order by a;
结果如下:

这个语句看起来非常复杂,我们可以一步一步解构它,首先执行中间那个select,看看会发生什么
select a,b,c,d,e,f,g,h,i from aaa_bbb_123.abcd
group by a,b,c,d,e,f,g,h,i having count(*) > 1;
结果如下:

这个可能看的不是很清楚,我们增加一点数据

此时数据库有a为1和a为11的重复数据共4条,各2条。

再次执行刚刚的语句:

看到了吗,刚刚的语句,是找出a-i中的完全相同的数据,前半句select a,b,c,d,e,f,g,h,i from aaa_bbb_123.abcd;就是普通的查询数据

后半句group by a,b,c,d,e,f,g,h,i having count(*) > 1;意思是把数据按照a-i进行分组合并,并展示分组数大于1的数据。
理解了前半句,后半句就好理解了,
select * from aaa_bbb_123.abcd
where (a,b,c,d,e,f,g,h,i) in (分组结果) order by a;
查询数据,筛选条件就是上面的分组结果。

找到了查询语句,现在开始设计程序,思路很简单,首先通过上面的语句查询重复数据,会返回一个元祖列表,里面包含了上面的四行数据,然后我处理每一行的数据,通过这一行找到其他行,按照id排序,将除了最后一个id以外的其他数据的filter_data全部设置为1。
对了,别忘了先增加字段:
alter table aaa_bbb_123.abcd
add filter_data tinyint(1) NOT NULL DEFAULT 0 comment '筛选数据';

程序如下:
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71import mysql.connector db1 = mysql.connector.connect( host="localhost", user="root", passwd="123456", database="aaa_bbb_123" ) cursor1 = db1.cursor() def find_duplicate_all(date1): # 查询除j,k外其他字段重复的语句 duplicate_data_field = 'a,b,c,d,e,f,g,h,i' sql = ('select * from abcd where (' + duplicate_data_field + ') in (select ' + duplicate_data_field + ' from abcd group by ' + duplicate_data_field + ' having count(*) > 1) and a="' + date1 + '";') print('sql=', sql) cursor1.execute(sql) return cursor1.fetchall() def change_filter_data(data, i): # 修改单条标记 # 这里必须把条件写全了,因为有部分数据除了a外完全一致, # 而a不是修改字段,所以实际上它们是两条数据 sql1 = ('select * from abcd where ' + 'a = "' + str(data[i][1]) + '" and ' + 'b = "' + str(data[i][2]) + '" and ' + 'c = "' + str(data[i][3]) + '" and ' + 'd = "' + str(data[i][4]) + '" and ' + 'e = "' + str(data[i][5]) + '" and ' + 'f = "' + str(data[i][6]) + '" and ' + 'g = "' + str(data[i][7]) + '" and ' + 'h = "' + str(data[i][8]) + '" and ' + 'i = "' + str(data[i][9]) + '"order by id;') # print('sql1=', sql1, 'n') cursor1.execute(sql1) data1 = cursor1.fetchall() for i in range(len(data1)): print(i, '与上面相同的数据:', data1[i]) # 由于按id排序,可以逐个改filter_data,最后一个id不管 if i != len(data1) - 1: # filter_data改为1 sql2 = "update abcd set filter_data=1 where id=" + str(data1[i][0]) + ";" print('不是最后一个id,修改filter_data:', sql2) cursor1.execute(sql2) db1.commit() else: print('是最后一个id,不修改filter_data') print('n') def change_filter_data_all(date1): # 修改全部标记 data = find_duplicate_all(date1) # 这是一个元祖列表 for i in range(len(data)): print('n', i, '**存在重复的数据:', data[i], 'n') # 通过这一条找出与这条数据相同的,修改单条标记 change_filter_data(data, i) change_filter_data_all('1')
执行结果如下:

可以看到id较低的数据重复数据,筛选字段已经被改为1。
你可能看到我上面加了一个筛选条件a=1,因为在实际业务中a是日期,所以每次调用change_filter_data_all方法时只需要指定要查询和修改的日期即可,不用全表查询,没必要,一旦Excel表被修改,我就读修改的那个日期。
我本以为这样可以万无一失,但是经过我这样一顿操作以后,我发现,即使我不做任何修改,多运行几次insert ignore,然后一旦有新的数据进来,id还是会莫名的增长!!
比如我现在id最大是11,Excel表不做修改,运行几次那个插入的程序,然后改一个数据,再运行一次。结果如下:


id还是增长到了30,这就非常离谱,说明上面的努力全都白费了,insert ignore也会增加id,只不过机制不是替换,而是忽略,并在id上自动增加一个数。
二、解决方案
在前面的部分中,我介绍了业务场景和探索过程,主要是:
Excel表部分字段会被手动修改,比如a-k中,j和k会被修改,而我必须把手动修改的值也读到,一开始我使用replace+非修改字段索引的方式插入,结果会导致id异常,然后我新增了一个filter_data字段用于筛选旧数据,使用insert ignore+全字段索引,旧数据filter_data=1 ,正常数据filter_data=0,结果发现insert ignore也会导致id异常增长,现在我必须找到一个方法,能保留所有数据,同时不能导致id异常增长。
经过各种查询,找到了一个看起来比较低效却很实用的方法:
每次插入数据之前,查询一遍该数据是否存在,考虑到还要识别和保留旧数据,所以要加两层筛选,第一遍匹配非修改字段,就是上面的a-i,如果这部分也没有,说明是新数据,直接insert即可。
如果有a-i,继续查询所有字段匹配,就是a-k,如果没有,说明该条数据被修改过(因为a-i是相同的,所以j,k不同),那么在插入之前把符合该数据的filter_data=1 ,再插入新的数据。
如果a-k有,说明是完全一致的数据,直接忽略,不做任何操作。
代码如下:
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113import openpyxl import mysql.connector def read_excel(file_name): # 打开excel文件,获取工作簿对象 wb1 = openpyxl.load_workbook(file_name) # 从工作薄中获取表单(sheet)对象 sheets = wb1.sheetnames print('该Excel文件包含工作表如下:', sheets, type(sheets)) sheet1 = wb1.active return sheet1 # 将Excel数据转化为元组列表 def excel_to_tuple_list(sheet1): sheet1_max_row = sheet1.max_row sheet1_max_column = sheet1.max_column print("最大行:{}".format(sheet1_max_row)) print("最大列:{}".format(sheet1_max_column)) # 定义一个元组列表,用来存放Excel数据 # 一行就是一个元组,100行row就是100个元组,第一行不读,故-1 tuple_list_excel_data = [0 for i2 in range(sheet1_max_row - 1)] # 首行表头忽略,从第二行开始读 for i1 in range(2, sheet1_max_row + 1): # 将每行第一个数据放入元组 tuple1 = (sheet1.cell(i1, 1).value,) # 从每行第二个数开始 for j1 in range(2, sheet1_max_column + 1): # 之后每次添加一个元素,直到该行结束 tuple2 = (sheet1.cell(i1, j1).value,) tuple1 = tuple1 + tuple2 # 处理完毕后将该元组放入列表,列表从0开始 tuple_list_excel_data[i1 - 2] = tuple1 # 检查列表数据是否完善 for tuple3 in tuple_list_excel_data: print(tuple3) return tuple_list_excel_data def send_mysql(tuple_list): # 连接到MySQL db1 = mysql.connector.connect( host="localhost", user="root", passwd="xxxxxx", database='aaa_bbb_123' ) cursor1 = db1.cursor() cursor1.execute("SHOW TABLES") for x in cursor1: print('将插入MySQL表:', x) sql = ("insert INTO abcd (a,b,c,d,e,f,g,h,i,j,k) " + "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)") for one_row in tuple_list: a(sql, one_row, cursor1) db1.commit() # 数据表内容有更新,必须使用到该语句 print(cursor1.rowcount, "记录插入成功。n") def a(sql, one_row, cursor1): select_count = "SELECT count(1) FROM abcd " # 9个非修改字段 product_9 = ("a = '" + str(one_row[0]) + "' and b = '" + str(one_row[1]) + "' and c= '" + str(one_row[2]) + "' and d='" + str(one_row[3]) + "' and e='" + str(one_row[4]) + "' and f='" + str(one_row[5]) + "' and g='" + str(one_row[6]) + "' and h='" + str(one_row[7]) + "' and i='" + str(one_row[8]) + "'") # 注意,变量前后需要加引号 sql2 = (select_count + "WHERE " + product_9) cursor1.execute(sql2) i = cursor1.fetchall()[0][0] if i == 0: print('查询索引部分没有记录,说明是新数据,insert即可') cursor1.execute(sql, one_row) else: # 索引部分有记录,再检查加上修改字段是否有记录 sql3 = (select_count + "WHERE " + "j='" + str(one_row[9]) + "' and k='" + str(one_row[10]) + "' and " + product_9) cursor1.execute(sql3) i = cursor1.fetchall()[0][0] if i == 0: print('索引部分有记录,加上修改字段没有记录,说明修改过,insert前修改旧filter_data') sql4 = ("UPDATE abcd SET " + "filter_data = 1" + " WHERE " + product_9) cursor1.execute(sql4) cursor1.execute(sql, one_row) else: print('找到该记录10个字段完全相同,说明未修改,忽略即可') send_mysql(excel_to_tuple_list(read_excel("./新建Excel.xlsx")))
执行结果:

可以看到id是连续的了,无论执行多少次,无论怎么修改,id都只增长1个。
用a排序来讲一下,可以看到,旧数据全被打上了1,以后直接查filter_data =0的数据即可。

其实这样做,完全可以不要索引了,因为全靠我自己select去查是否有重复数据,而且如果你观察仔细,可以发现我把insert ignore改成了insert,因为插入时不可能有重复的数据,所以可以省略。
讲一下代码,前面的部分没什么特别的,主要是后面的函数a(),把每行的数据都查一遍,cursor1.fetchall()[0][0]返回的是匹配数据的条数,当条数为0时,说明不存在,就可以进行一系列的判断。
另一个部分就是send_mysql函数,我把批量插入的函数改成循环每行判断,这样做看起来效率会比较低但是经过我的测试,40w的数据,程序跑起来也是比较快的,基本上1秒1张Excel表没问题,加上我本来设计的每读完一张表就要延迟一下再读下一张,免得卡死电脑,所以这点影响基本不大。
由于技术有限,有错误或者更好的解决办法,欢迎指教!
最后
以上就是生动雪碧最近收集整理的关于【MySQL】新增数据时id不连续的解决思路的全部内容,更多相关【MySQL】新增数据时id不连续内容请搜索靠谱客的其他文章。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复