在Django中查找重复记录

近为公司的文章加上了permalink,由于之前已经有满大的数据量了,所以写了一个小脚本将之前的标题自动转换成url友好的slug字段。不过由于考虑不周,等全部弄好了才想起来忘了给slug字段加上unique属性,转换时也没考虑到这点,结果所有同标题的文章在自动转换后也就都有了相同的slug,这在Django里使用 Article.objects.get() 的时候自然就会报错了。

最快的应急措施是立刻把重复的slug改成具有唯一属性的id值,要做成这件事在SQL里面是满简单的,只要用以下语句就可以达到目的:

UPDATE article SET slug = id
WHERE slug IN (
  SELECT slug
  FROM article
  GROUP BY slug
  HAVING (COUNT(slug) > 1));

不过既然框架都用Django了,我们就来看看用Django怎么来做这件事吧。Django在1.1时为queryset加入了 annotate 这个方法,可以为查询结果添加聚合支持,那让我们打开ipython来试一试吧。

from django.db.models import Count
print Article.objects.values('slug')\
      .annotate(count = Count('slug'))\
      .filter(count__gt=1)

第一次尝试从经验上来说一般是不成功的,果不其然,打印的结果是一个空数组,这是为啥呢?

想起以前写的 Django数据库聚合bug ,自然会认为这也是那个bug的问题了。不过咱还是得验证一下的,以下是Django对这次查询生成的SQL:

SELECT "article"."slug",
       COUNT("article"."slug") AS "count"
FROM "article"
GROUP BY "article"."slug", "article"."created"
HAVING COUNT("article"."slug") > 1
ORDER BY "article"."created" DESC;

慢着,SQL里面怎么莫名其妙地多出来了一个created列?这时候突然一个激灵,Article的Meta类中确实是定义了 ordering 字段来默认使用发布时间倒序排列的。提到排序,便依稀想起Django文档中好像确实有讲到过关于这个的一些注意点了。立刻翻开文档,果然,在 distinct() 下的Note里找到了这么一段:

if you use a values() query to restrict the columns selected, the columns used in any order_by() (or default model ordering) will still be involved and may affect uniqueness of the results.

这下就清楚了,对于只取部分字段的查询来说,需要再调用一次 order_by 来把那些默认的排序字段的影响清楚。所以把上面的语句稍作修改:

from django.db.models import Count
print Article.objects.values('slug')\
      .order_by('created')\
      .annotate(count = Count('slug'))\
      .filter(count__gt=1)

这下便终于得到了重复的slug了。既然已经可以找到重复的slug,接下来就好办了:

from django.db.models import Count, F
Article.objects.filter(
    slug__in = Article.objects.values('slug')\
    .order_by('created')\
    .annotate(count = Count('slug'))\
    .filter(count__gt=1)\
    .values_list('slug', flat = True))\
  .update(slug = F('id'))

OK,大功告成,收工。

Note

我不是DBA,也许上面的做法并不高效,各位童鞋建议点更好的办法来完成这种任务吧 :)

发表评论

评论备注:

  1. 留言时的头像是Gravatar提供的服务。
  2. By submitting a comment here you grant this site a perpetual license to reproduce your words and name/web site in attribution. So, you don't fully own your words, so to speak.