Django导入iTunes数据后Admin页面加载异常求助
Django Admin加载大量关联数据无响应的排查求助
我已将外部iTunes数据导入Django自有数据库,包含783122首歌曲、59445张专辑、97562位艺术家。配置Django Admin后,期望在艺术家或专辑页面内联显示关联歌曲,但访问页面时无报错,持续加载超过30分钟仍无响应。已将自然键替换为bigint主键,问题依旧。怀疑索引创建耗时,但数据量未超百万行,耗时过长不符合预期,求排查思路,是否需要换用Flask?
模型代码
from django.db import models class ArtistKeys(models.Model): naturalkey = models.CharField(unique=True, max_length=90) class AlbumKeys(models.Model): naturalkey = models.CharField(unique=True, max_length=90) class SongKeys(models.Model): persistentid = models.CharField(unique=True, max_length=90) class ItunesArtist(models.Model): def __str__(self): if self.name: return self.name else: return "" id = models.IntegerField(primary_key=True) naturalkey = models.CharField(unique=True, max_length=90) name = models.CharField(max_length=600, blank=True, null=True) expressed = models.BooleanField() # class ItunesAlbumArtist(models.Model): # albumartist = models.CharField(primary_key=True, max_length=90) # name = models.CharField(max_length=600, blank=True, null=True) class ItunesAlbum(models.Model): def __str__(self): if self.name: return self.name else: return "" id = models.BigIntegerField(primary_key=True) naturalkey = models.CharField(unique=True, max_length=90) name = models.CharField(max_length=600, blank=True, null=True) samplerate = models.IntegerField(blank=True, null=True) trackcount = models.SmallIntegerField(blank=True, null=True) class ItunesSong(models.Model): def __str__(self): # return "{} – {} – {}".format(self.artist, self.album, self.name) if self.name: return self.name else: return "" id = models.BigIntegerField(primary_key=True) album = models.CharField(max_length=600, blank=True, null=True) artist = models.CharField(max_length=600, blank=True, null=True) name = models.CharField(max_length=600, blank=True, null=True) albumartist = models.CharField(max_length=600, blank=True, null=True) albumrating = models.SmallIntegerField(blank=True, null=True) albumratingcomputed = models.BooleanField(blank=True, null=True) artworkcount = models.SmallIntegerField(blank=True, null=True) bpm = models.SmallIntegerField(blank=True, null=True) bitrate = models.SmallIntegerField(blank=True, null=True) clean = models.BooleanField(blank=True, null=True) comments = models.CharField(max_length=360, blank=True, null=True) composer = models.CharField(max_length=360, blank=True, null=True) contentrating = models.CharField(max_length=60, blank=True, null=True) dateadded = models.DateTimeField(blank=True, null=True) datemodified = models.DateTimeField(blank=True, null=True) disabled = models.BooleanField(blank=True, null=True) disccount = models.SmallIntegerField(blank=True, null=True) discnumber = models.SmallIntegerField(blank=True, null=True) disliked = models.BooleanField(blank=True, null=True) episode = models.CharField(max_length=20, blank=True, null=True) episodeorder = models.IntegerField(blank=True, null=True) explicit = models.BooleanField(blank=True, null=True) filetype = models.BigIntegerField(blank=True, null=True) genre = models.CharField(max_length=100, blank=True, null=True) grouping = models.CharField(max_length=180, blank=True, null=True) hd = models.BooleanField(blank=True, null=True) hasvideo = models.BooleanField(blank=True, null=True) kind = models.CharField(max_length=40, blank=True, null=True) location = models.CharField(max_length=1070, blank=True, null=True) loved = models.BooleanField(blank=True, null=True) movie = models.BooleanField(blank=True, null=True) normalization = models.IntegerField(blank=True, null=True) partofgaplessalbum = models.BooleanField(blank=True, null=True) persistentid = models.CharField(unique=True, max_length=20) playcount = models.SmallIntegerField(blank=True, null=True) playdate = models.BigIntegerField(blank=True, null=True) playdateutc = models.DateTimeField(blank=True, null=True) podcast = models.BooleanField(blank=True, null=True) protected = models.BooleanField(blank=True, null=True) purchased = models.BooleanField(blank=True, null=True) rating = models.SmallIntegerField(blank=True, null=True) ratingcomputed = models.BooleanField(blank=True, null=True) releasedate = models.DateTimeField(blank=True, null=True) samplerate = models.IntegerField(blank=True, null=True) season = models.SmallIntegerField(blank=True, null=True) series = models.CharField(max_length=120, blank=True, null=True) size = models.BigIntegerField(blank=True, null=True) skipcount = models.SmallIntegerField(blank=True, null=True) skipdate = models.DateTimeField(blank=True, null=True) sortalbum = models.CharField(max_length=600, blank=True, null=True) sortalbumartist = models.CharField(max_length=600, blank=True, null=True) sortartist = models.CharField(max_length=600, blank=True, null=True) sortcomposer = models.CharField(max_length=600, blank=True, null=True) sortname = models.CharField(max_length=600, blank=True, null=True) sortseries = models.CharField(max_length=600, blank=True, null=True) tvshow = models.BooleanField(blank=True, null=True) totaltime = models.IntegerField(blank=True, null=True) trackcount = models.SmallIntegerField(blank=True, null=True) tracknumber = models.IntegerField(blank=True, null=True) tracktype = models.CharField(max_length=10, blank=True, null=True) unplayed = models.BooleanField(blank=True, null=True) videoheight = models.SmallIntegerField(blank=True, null=True) videowidth = models.SmallIntegerField(blank=True, null=True) volumeadjustment = models.SmallIntegerField(blank=True, null=True) work = models.CharField(max_length=80, blank=True, null=True) year = models.SmallIntegerField(blank=True, null=True) created = models.DateTimeField(blank=True, null=True) modified = models.DateTimeField(blank=True, null=True) releaseid = models.ForeignKey( ItunesAlbum, max_length=90, blank=True, null=True, on_delete=models.PROTECT, ) artistid = models.ForeignKey( ItunesArtist, max_length=90, blank=True, null=True, on_delete=models.PROTECT, related_name="artist_songs", related_query_name="artist_songs", ) expressedartistid = models.ForeignKey( ItunesArtist, max_length=90, blank=True, null=True, on_delete=models.PROTECT, related_name="expressed_artist_songs", related_query_name="expressed_artist_songs", ) albumartistid = models.ForeignKey( ItunesArtist, max_length=90, blank=True, null=True, on_delete=models.PROTECT, related_name="album_artist_songs", related_query_name="album_artist_songs", ) compilation = models.BooleanField(blank=True, null=True)
Admin代码
from django.contrib import admin from .models import ItunesArtist, ItunesAlbum, ItunesSong class ArtistSongInline(admin.TabularInline): fk_name = 'artistid' model = ItunesSong class ExpressedArtistSongInline(admin.TabularInline): fk_name = 'expressedartistid' model = ItunesSong class AlbumArtistSongInline(admin.TabularInline): model = ItunesSong class ItunesArtistAdmin(admin.ModelAdmin): inlines = [ExpressedArtistSongInline] class ItunesAlbumAdmin(admin.ModelAdmin): inlines = [AlbumArtistSongInline] admin.site.register(ItunesArtist, ItunesArtistAdmin) admin.site.register(ItunesAlbum, ItunesAlbumAdmin) admin.site.register(ItunesSong)
排查与优化思路
1. 确认数据库索引是否生效
Django外键默认会创建索引,但手动导入数据后可能存在索引未创建的情况。直接查询数据库验证:
- PostgreSQL:
\d itunes_song查看索引列表 - MySQL:
SHOW INDEX FROM itunes_song;
如果外键字段(expressedartistid、albumartistid、releaseid等)无索引,手动创建:
CREATE INDEX idx_itunes_song_expressedartistid ON itunes_song(expressedartistid); CREATE INDEX idx_itunes_song_albumartistid ON itunes_song(albumartistid); CREATE INDEX idx_itunes_song_releaseid ON itunes_song(releaseid);
2. 限制内联数据量与字段
Django Admin默认会加载所有关联数据,热门艺术家可能关联数千首歌,全量加载必然导致超时。修改Inline配置:
class ExpressedArtistSongInline(admin.TabularInline): fk_name = 'expressedartistid' model = ItunesSong fields = ('name', 'album', 'year') # 只显示必要字段,减少数据传输 extra = 0 # 不显示空表单 def get_queryset(self, request): qs = super().get_queryset(request) # 限制显示数量+预关联专辑表,避免N+1查询 return qs.select_related('releaseid')[:100]
3. 查看慢查询日志定位问题
开启Django数据库查询日志,查看页面加载时的SQL执行情况:
在settings.py中添加日志配置:
LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'console': {'class': 'logging.StreamHandler'}, }, 'loggers': { 'django.db.backends': { 'level': 'DEBUG', 'handlers': ['console'], }, }, }
运行服务器后访问页面,观察控制台输出的SQL,重点检查是否存在全表扫描、未使用索引的查询。
4. 替换内联为跳转链接
对于关联数据量大的场景,内联显示本身不合理,改为在Admin列表页添加跳转链接,直接筛选该艺术家/专辑的所有歌曲:
class ItunesArtistAdmin(admin.ModelAdmin): def songs_link(self, obj): from django.urls import reverse # 生成带筛选条件的歌曲列表链接 url = reverse('admin:your_app_name_itunessong_changelist') + f'?expressedartistid__id={obj.id}' return f'<a href="{url}">查看所有关联歌曲</a>' songs_link.allow_tags = True songs_link.short_description = '关联歌曲' list_display = ('name', 'expressed', 'songs_link') class ItunesAlbumAdmin(admin.ModelAdmin): def songs_link(self, obj): from django.urls import reverse url = reverse('admin:your_app_name_itunessong_changelist') + f'?releaseid__id={obj.id}' return f'<a href="{url}">查看所有关联歌曲</a>' songs_link.allow_tags = True songs_link.short_description = '关联歌曲' list_display = ('name', 'trackcount', 'songs_link')
5. 优化数据库配置
检查数据库缓存配置,确保有足够内存缓存数据:
- PostgreSQL:调整
shared_buffers(建议设为系统内存的1/4) - MySQL:调整
innodb_buffer_pool_size(建议设为系统内存的50%-70%)
关于是否换用Flask
完全没必要,Django可以轻松处理百万级数据量,当前问题是Admin配置未优化导致的,而非框架本身的承载能力问题。
内容的提问来源于stack exchange,提问作者Jason Witherspoon
相关产品推荐
相关产品推荐

