前言
这几天我在处理重复的商品数据,涉及的商品数量有点多,说实话任务有点艰巨。
我们这边有材料库商品和商城商品两种。
由于之前属性值1.0,1.00和1合并过数据,导致产生了重复材料库商品数据。
商城商品依赖于材料库商品,商城商品有:自营、厂商直供、撮合商城等多种业务场景,自营模式下一个商城商品对应一个材料库商品。
由于材料库商品合并过属性值的数据,从而导致产生了重复的材料库商品,我们需要删除这些重复的材料库商品数据。
但有个问题是:商城商品依赖于材料库商品,如果直接把材料库的商品删除了,商城商品会出现问题。
我们必须要先商城商品中依赖的材料库商品编号替换成未删除的相同的商品编号,比如:材料库商品a,b是相同的商品,但他们的id不同,分别为id1和id2,而商城商品c中之前依赖的材料库商品编号是id1,由于id1要被删除了,需要替换成id2。
问题又来了:这样替换之后,又可能会导致商城商品重复。
出现这样的情况:商城商品d、e、f都依赖于材料库商品id2。
这就需要先删除重复的商城商品了。
1. 如何快速删除数据?
刚开始以为,删除重复数据还不容易,先group by一下,相同的商品只保留一条数据,其余的都删除不就OK了?
一条sql就能搞定:
update mall_sku m
inner join
(select id,sku_id from mall_sku
where sys_status=1 and source=1
group by sku_id
having count(*)>1
) a
on m.sku_id=a.sku_id and m.id != a.id
set m.sys_status=-999,edit_date=now(3)
where m.sys_status=1 and m.source=1但运营同学给我们提了一个需求:删除重复商品数据时,优先保留上架状态的,如果有多个上架状态(1:上架 0:下架)的商品,则保留时间最新的那条数据。
很显然直接用上面的那条sql,是没办法满足运营要求的。
那么,该怎办呢?
用max函数可以吗?
sql改成这样的:
update mall_sku m
inner join
(select id,sku_id,max(status),max(edit_date) from mall_sku
where sys_status=1 and source=1
group by sku_id
having count(*)>1
) a
on m.sku_id=a.sku_id and m.id != a.id
set m.sys_status=-999,edit_date=now(3)
where m.sys_status=1 and m.source=1但没法保证状态最大,并且时间最大,对应的id,跟我们查出来的结果集中的id是同一个id。
这不就出问题呢?
那么,如何解决这个问题呢。
2.第一次优化
我们转化一下思维,在group by之前,先用order by排个序,再group by不就解决问题了?
但group by之后,id有多个可以选择,用哪一个呢?
我们想获取每组的第一个id。
于是将sql改成这样的:
update mall_sku m
inner join
(
select sku_id,substring(group_concat(id),',',1) as id
(
select id,sku_id,status,edit_date from mall_sku
where sys_status=1 and source=1
order by sku_id,status desc,edit_date desc
) b
group by b.sku_id
having count(*)>1
) a
on m.sku_id=a.sku_id and m.id != a.id
set m.sys_status=-999,edit_date=now(3)
where m.sys_status=1 and m.source=1我们先把数据按sku_id、status和edit_date排序,这样可以保证相同的sku_id的数据排在一起,并且status值大的排在前面,如果status相同,则edit_date大的排在前面。
再将相同sku_id的多个不同的id用group_concat函数拼接起来,变成这样的:11,22,111,123,这样可以保证第一个id就是我们想要的id,然后用substring函数将第一个id截取出来。
想法是好的,但很快被打脸了。
3.遇到一个诡异的问题
我通过这种方式预处理了一次数据,但处理完之后,发现还有3条数据没被处理掉。
由于这次数据处理影响商品范围有点大,不能直接在生产环境执行,万一删错数据了,就糟糕了。
因此,我搞了一张备份表,先刷了备份表中的数据。
create table mall_sku_20230607 like mall_sku;
insert into mall_sku_20230607 select * from mall_sku;备份表中仍然有3条重复的商品。
这就很诡异了。
仔细review了一下sql,写法上没有问题,我猜测唯一可能有问题的地方是这里:
m.id != a.id因为上面用group_concat函数将id已经转化成字符串了:
substring(group_concat(id),',',1) as id而m.id != a.id,是用字符串和long类型的数据做比较,就出问题了。
其实之前我也注意到了字段类型不同,但之前了解到的是不同的字段类型做判断时,会字段发生隐式转换,这样会导致sql语句的索引失效。
但现在的问题是不光索引失效了,我测试的结果是这个判断条件有问题。
这简直是一个大坑。
那么,如何解决呢?
改成相同的字段类型不就OK了?
于是sql改成这样:
update mall_sku m
inner join
(
select sku_id,substring(group_concat(id),',',1) as id
(
select id,sku_id,status,edit_date from mall_sku
where sys_status=1 and source=1
order by sku_id,status desc,edit_date desc
) b
group by b.sku_id
having count(*)>1
) a
on m.sku_id=a.sku_id and concat(m.id,'') != a.id
set m.sys_status=-999,edit_date=now(3)
where m.sys_status=1 and m.source=1我们可以使用concat函数将long类型的id,转换成字符串,这样两个字符串字段判断是否相等就没问题。
但没想到的是还是有问题。
4. 第二次优化
经过测试之后发现,通过上面的sql获取到的id,有些状态是0,即下架状态的,导致上架状态的商品被删了,但下架状态的商品却保留下来了。
此时有点懵。
查阅了一些资料发现排序之后,要使用limit,不然排序不会真的生效。
于是优化了一下sql:
update mall_sku m
inner join
(
select sku_id,substring(group_concat(id),',',1) as id
(
select id,sku_id,status,edit_date from mall_sku
where sys_status=1 and source=1
order by sku_id,status desc,edit_date desc
limit 1000000
) b
group by b.sku_id
having count(*)>1
) a
on m.sku_id=a.sku_id and concat(m.id,'') != a.id
set m.sys_status=-999,edit_date=now(3)
where m.sys_status=1 and m.source=1使用limit优化之后,查询结果确实正确了许多,绝大多数结果是对的。
但仍然有少数结果有问题,通过上面的sql获取到的id,有些状态是0,即下架状态的,状态是1的没有获取到。
到底该怎么办呢?
5. 第三次优化
我们上面写的sql的想法是好的,但结果有点无情。
出问题的根本原因是group_concat,即使增加了order by排序,但仍然不能保证排序后第一个id是我们想要的。
我又查阅了一些资料。
发现那条sql的写法有点问题。
于是又优化了一下:
update mall_sku m
inner join
(
select sku_id,id
(
select id,sku_id,status,edit_date from mall_sku
where sys_status=1 and source=1
order by sku_id,status desc,edit_date desc
limit 1000000
) b
group by b.sku_id
having count(*)>1
) a
on m.sku_id=a.sku_id and m.id!= a.id
set m.sys_status=-999,edit_date=now(3)
where m.sys_status=1 and m.source=1其实正确的用法是:
- 用order by排序
- 用limit固定排序
- 去掉group_concat函数,直接返回id
此时的id,就是我们想要的数据。
这次优化之后,sql返回结果终于正确了。
6. 后续
我们在使用update批量处理数据时,要有个好习惯,可以先备份一下数据。
create table mall_sku_2023060710 like mall_sku;
insert into mall_sku_2023060710 select * from mall_sku;万一我们处理完数据之后,在测试的过程中,又发现了其他的问题,这时可能需要回滚数据。
如果有这张备份表,我们可以这样还原:
update mall_sku m1
inner join mall_sku_2023060710 m2 on m1.id=m2.id and m1.sys_status!=m2.sys_status
set m1.sys_status=m2.sys_status,m1.edit_date=m2.edit_date