├── database_size ├── __init__.py ├── sql │ ├── table.mysql.sql │ └── table.postgresql.sql ├── utils.py ├── models.py └── admin.py ├── README.md ├── .gitignore └── setup.py /database_size/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 3, 0) 2 | __version__ = '.'.join(map(str, VERSION)) 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | django-database-size 2 | ===================== 3 | 4 | Adds a page to Django admin that lists the size of all tables in the database. 5 | 6 | Installation 7 | ------------ 8 | 9 | sudo python setup.py install 10 | 11 | Or via pip with: 12 | 13 | sudo pip install django-database-size 14 | 15 | Install the appropriate view in /sql (currently only PostgreSQL and MySQL supported). 16 | 17 | Add to your INSTALLED_APPS. 18 | 19 | Usage 20 | ----- 21 | 22 | Browse to /admin/database_size/. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | bin/ 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | MANIFEST 23 | 24 | # Installer logs 25 | pip-log.txt 26 | pip-delete-this-directory.txt 27 | 28 | # Unit test / coverage reports 29 | .tox/ 30 | .coverage 31 | .cache 32 | nosetests.xml 33 | coverage.xml 34 | 35 | # Translations 36 | *.mo 37 | 38 | # Mr Developer 39 | .mr.developer.cfg 40 | .project 41 | .pydevproject 42 | .settings 43 | 44 | # Rope 45 | .ropeproject 46 | 47 | # Django stuff: 48 | *.log 49 | *.pot 50 | 51 | # Sphinx documentation 52 | docs/_build/ 53 | 54 | # Local virtualenvs used for testing. 55 | /.env* 56 | 57 | # PIP install version files. 58 | /=* 59 | -------------------------------------------------------------------------------- /database_size/sql/table.mysql.sql: -------------------------------------------------------------------------------- 1 | /* 2 | 2013.11.21 CKS 3 | Lists all table sizes. 4 | */ 5 | DROP VIEW IF EXISTS database_size_table CASCADE; 6 | CREATE OR REPLACE VIEW database_size_table 7 | AS 8 | SELECT CONCAT(table_schema, '-', table_name) AS id, 9 | table_schema AS schema_name, 10 | table_name AS table_name, 11 | null AS table_owner, -- TODO: not available in MySQL?! 12 | (data_length + index_length) AS size_in_bytes 13 | FROM information_schema.TABLES 14 | UNION ALL 15 | SELECT CONCAT(table_schema, '-byschema') AS id, 16 | table_schema AS schema_name, 17 | 'ALL' AS table_name, 18 | 'ALL' AS table_owner, -- TODO: not available in MySQL?! 19 | SUM(data_length + index_length) AS size_in_bytes 20 | FROM information_schema.TABLES 21 | GROUP BY table_schema 22 | UNION ALL 23 | SELECT 'all-all' AS id, 24 | 'ALL' AS schema_name, 25 | 'ALL' AS table_name, 26 | 'ALL' AS table_owner, -- TODO: not available in MySQL?! 27 | SUM(data_length + index_length) AS size_in_bytes 28 | FROM information_schema.TABLES; -------------------------------------------------------------------------------- /database_size/sql/table.postgresql.sql: -------------------------------------------------------------------------------- 1 | /* 2 | 2013.5.12 CKS 3 | Lists all table sizes. 4 | */ 5 | DROP VIEW IF EXISTS database_size_table CASCADE; 6 | CREATE OR REPLACE VIEW database_size_table 7 | AS 8 | SELECT CONCAT(CAST(schemaname AS VARCHAR), '-', CAST(tablename AS VARCHAR)) AS id, 9 | schemaname AS schema_name, 10 | tablename AS table_name, 11 | tableowner AS table_owner, 12 | pg_total_relation_size(schemaname || '.' || tablename) AS size_in_bytes 13 | FROM pg_tables 14 | UNION ALL 15 | SELECT CONCAT(CAST(schemaname AS VARCHAR), '-byschema') AS id, 16 | schemaname AS schema_name, 17 | 'ALL' AS table_name, 18 | 'ALL' AS table_owner, 19 | SUM(pg_total_relation_size(schemaname || '.' || tablename))::bigint AS size_in_bytes 20 | FROM pg_tables 21 | GROUP BY schemaname 22 | UNION ALL 23 | SELECT 'all-all' AS id, 24 | 'ALL' AS schema_name, 25 | 'ALL' AS table_name, 26 | 'ALL' AS table_owner, 27 | SUM(pg_total_relation_size(schemaname || '.' || tablename))::bigint AS size_in_bytes 28 | FROM pg_tables; 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | from distutils.core import setup, Command 5 | import database_size 6 | 7 | setup( 8 | name='django-database-size', 9 | version=database_size.__version__, 10 | description='Monitor the size of database tables from Django admin.', 11 | author='Chris Spencer', 12 | author_email='chrisspen@gmail.com', 13 | url='http://github.com/chrisspen/django-database-size', 14 | packages=[ 15 | 'database_size', 16 | ], 17 | package_data = { 18 | 'database_size': [ 19 | 'sql/*.*', 20 | ], 21 | }, 22 | #https://pypi.python.org/pypi?%3Aaction=list_classifiers 23 | classifiers=[ 24 | 'Development Status :: 5 - Production/Stable', 25 | 'Framework :: Django', 26 | 'Intended Audience :: Developers', 27 | 'License :: OSI Approved :: BSD License', 28 | 'Operating System :: OS Independent', 29 | 'Programming Language :: Python', 30 | 'Programming Language :: PL/SQL', 31 | ], 32 | install_requires = ['Django>=1.4'], 33 | ) -------------------------------------------------------------------------------- /database_size/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | # http://code.activestate.com/recipes/577081-humanized-representation-of-a-number-of-bytes/ 4 | def humanize_bytes(bytes, precision=1): 5 | """Return a humanized string representation of a number of bytes. 6 | 7 | Assumes `from __future__ import division`. 8 | 9 | >>> humanize_bytes(1) 10 | '1 byte' 11 | >>> humanize_bytes(1024) 12 | '1.0 kB' 13 | >>> humanize_bytes(1024*123) 14 | '123.0 kB' 15 | >>> humanize_bytes(1024*12342) 16 | '12.1 MB' 17 | >>> humanize_bytes(1024*12342,2) 18 | '12.05 MB' 19 | >>> humanize_bytes(1024*1234,2) 20 | '1.21 MB' 21 | >>> humanize_bytes(1024*1234*1111,2) 22 | '1.31 GB' 23 | >>> humanize_bytes(1024*1234*1111,1) 24 | '1.3 GB' 25 | """ 26 | abbrevs = ( 27 | (1<<50, 'PB'), 28 | (1<<40, 'TB'), 29 | (1<<30, 'GB'), 30 | (1<<20, 'MB'), 31 | (1<<10, 'kB'), 32 | (1, 'bytes') 33 | ) 34 | if bytes == 1: 35 | return '1 byte' 36 | for factor, suffix in abbrevs: 37 | if bytes >= factor: 38 | break 39 | return '%.*f %s' % (precision, bytes / factor, suffix) 40 | -------------------------------------------------------------------------------- /database_size/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | class StringWithTitle(str): 5 | """ 6 | String class with a title method. Can be used to override 7 | admin app names. 8 | 9 | http://ionelmc.wordpress.com/2011/06/24/custom-app-names-in-the-django-admin/ 10 | """ 11 | 12 | def __new__(cls, value, title): 13 | instance = str.__new__(cls, value) 14 | instance._title = title 15 | return instance 16 | 17 | def title(self): 18 | return self._title 19 | 20 | __copy__ = lambda self: self 21 | __deepcopy__ = lambda self, memodict: self 22 | 23 | APP_LABEL = StringWithTitle('database_size', 'Database Size') 24 | 25 | class Table(models.Model): 26 | 27 | # MySQL can have no more than 255 length... 28 | id = models.CharField(max_length=255, primary_key=True) 29 | 30 | schema_name = models.CharField(max_length=500) 31 | table_name = models.CharField(max_length=500) 32 | table_owner = models.CharField(max_length=500) 33 | size_in_bytes = models.IntegerField() 34 | 35 | class Meta: 36 | managed = False 37 | #db_table = 'database_size_table' 38 | #db_table = 'database_size_databasesizetable' 39 | ordering = ('-size_in_bytes',) 40 | app_label = APP_LABEL 41 | verbose_name = _('table') 42 | 43 | def __unicode__(self): 44 | return '%s.%s' % (self.schema_name, self.table_name) 45 | -------------------------------------------------------------------------------- /database_size/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.db import connections 3 | from django.contrib.admin import SimpleListFilter 4 | 5 | import models 6 | import utils 7 | 8 | class SelectDatabaseListFilter(SimpleListFilter): 9 | 10 | title = 'database' 11 | 12 | parameter_name = 'database' 13 | 14 | default_value = 'default' 15 | 16 | def __init__(self, request, params, model, model_admin): 17 | self.parameter_val = None 18 | try: 19 | self.parameter_val = request.GET.get(self.parameter_name, self.default_value) 20 | except Exception as e: 21 | pass 22 | super(SelectDatabaseListFilter, self).__init__(request, params, model, model_admin) 23 | 24 | def lookups(self, request, model_admin): 25 | """ 26 | Must be overriden to return a list of tuples (value, verbose value) 27 | """ 28 | return [(conn, conn) for conn in connections] 29 | 30 | def choices(self, cl): 31 | for lookup, title in self.lookups(None, None): 32 | yield { 33 | 'selected': self.parameter_val == lookup, 34 | 'query_string': cl.get_query_string({ 35 | self.parameter_name: lookup, 36 | }, []), 37 | 'display': title, 38 | } 39 | 40 | def queryset(self, request, queryset): 41 | """ 42 | Returns the filtered queryset. 43 | """ 44 | queryset._db = self.parameter_val 45 | return queryset 46 | 47 | class TableAdmin(admin.ModelAdmin): 48 | 49 | list_display = ( 50 | #'id', 51 | #'site', 52 | 'table_name', 53 | 'schema_name', 54 | 'table_owner', 55 | 'size_in_bytes', 56 | 'pretty_size', 57 | ) 58 | list_filter = ( 59 | 'schema_name', 60 | 'table_owner', 61 | SelectDatabaseListFilter, 62 | ) 63 | search_fields = ( 64 | 'schema_name', 65 | 'table_name', 66 | 'table_owner', 67 | ) 68 | readonly_fields = ( 69 | 'pretty_size', 70 | ) 71 | 72 | def has_delete_permission(self, request, obj=None): 73 | return False 74 | 75 | def has_add_permission(self, request, obj=None): 76 | return False 77 | 78 | def get_actions(self, request): 79 | actions = super(TableAdmin, self).get_actions(request) 80 | del actions['delete_selected'] 81 | return actions 82 | 83 | def get_readonly_fields(self, request, obj=None): 84 | readonly_fields = list(self.readonly_fields) 85 | return readonly_fields + [f.name for f in self.model._meta.fields] 86 | 87 | def pretty_size(self, obj=None): 88 | if obj is None: 89 | return '' 90 | return utils.humanize_bytes(obj.size_in_bytes) 91 | pretty_size.short_description = 'size' 92 | pretty_size.admin_order_field = 'size_in_bytes' 93 | 94 | admin.site.register(models.Table, TableAdmin) 95 | --------------------------------------------------------------------------------