├── .gitignore ├── CHANGES.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── django_explain.py ├── pytest.ini └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ofw/django-explain/64c841c0d7eb3f3b90d063dc43aa36c4ceb13172/CHANGES.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Egor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | 2 | include CHANGES.md 3 | include README.md 4 | include LICENSE 5 | global-exclude *.pyc 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django-Explain - a helper to get EXPLAIN or EXPLAIN ANALYZE output for django queryset. 2 | 3 | ## Installation 4 | ```sh 5 | pip install django-queryset 6 | ``` 7 | 8 | ## Example 9 | 10 | ```py 11 | from django.contrib.auth.models import User 12 | from django_explain import explain 13 | 14 | explain_result = explain(User.objects.filter(pk=123)) 15 | explain_result.pretty_print() 16 | 17 | # SELECT "auth_user"."id", 18 | # "auth_user"."password", 19 | # "auth_user"."last_login", 20 | # "auth_user"."is_superuser", 21 | # "auth_user"."username", 22 | # "auth_user"."first_name", 23 | # "auth_user"."last_name", 24 | # "auth_user"."email", 25 | # "auth_user"."is_staff", 26 | # "auth_user"."is_active", 27 | # "auth_user"."date_joined", 28 | # "auth_user"."has_orders" 29 | # FROM "auth_user" 30 | # WHERE "auth_user"."id" = 1 31 | # 32 | # Index Scan using auth_user_pkey on auth_user (cost=0.43..8.45 rows=1 width=1070) (actual time=0.039..0.039 rows=0 loops=1) 33 | # Index Cond: (id = 1) 34 | # Planning time: 1.203 ms 35 | # Execution time: 0.106 ms 36 | 37 | result.get_depesz_url() 38 | # https://explain.depesz.com/s/Tym 39 | ``` 40 | -------------------------------------------------------------------------------- /django_explain.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from django.db import connections 4 | import requests 5 | import sqlparse 6 | 7 | 8 | class ExplainResult(object): 9 | 10 | def __init__(self, sql, explain): 11 | self.sql = sql 12 | self.explain = explain 13 | 14 | def pretty_print(self): 15 | sql = sqlparse.format(self.sql, reindent=True, keyword_case='upper') 16 | print(sql) 17 | print(self.explain) 18 | 19 | def get_depesz_url(self): 20 | payload = {"title": "default", "plan": self.explain} 21 | url = "https://explain.depesz.com" 22 | response = requests.post(url=url, data=payload) 23 | response.raise_for_status() 24 | return url + response.headers.get("Location") 25 | 26 | 27 | def explain(query, analyze=True): 28 | connection = connections[query.db] 29 | with connection.cursor() as c: 30 | sql, params = query.query.sql_with_params() 31 | tmpl = "EXPLAIN {sql}" 32 | if analyze: 33 | tmpl = "EXPLAIN ANALYZE {sql}" 34 | c.execute(tmpl.format(sql=sql), params) 35 | explain_data = "\n".join([i[0] for i in c.fetchall()]) 36 | return ExplainResult(sql=str(query.query), explain=explain_data) 37 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --pyargs 3 | norecursedirs = env venv 4 | python_files= test_*.py itest_*.py 5 | pep8ignore = * E401,E402,E122,E126,E127,E128,E124,E241,E501 6 | pep8maxlinelength = 120 7 | 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | from setuptools import setup, Command 5 | 6 | 7 | class CleanCommand(Command): 8 | """Custom clean command to tidy up the project root.""" 9 | user_options = [] 10 | 11 | def initialize_options(self): 12 | pass 13 | 14 | def finalize_options(self): 15 | pass 16 | 17 | def run(self): 18 | os.system('rm -vrf ./build ./dist ./*.pyc ./*.tgz ./*.egg-info') 19 | 20 | 21 | # allow setup.py to be run from any path 22 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 23 | 24 | 25 | setup( 26 | name='django-explain', 27 | version='1.1.1', 28 | py_modules=[ 29 | 'django_explain', 30 | ], 31 | include_package_data=True, 32 | install_requires=['Django>=1.7', 'requests', 'sqlparse'], 33 | license='MIT License', 34 | description='A helper to get EXPLAIN and EXPLAIN ANALYZE OUTPUT for django queryset.', 35 | author='Egor Orlov', 36 | author_email='oeegor@gmail.com', 37 | platforms='any', 38 | url='https://github.com/oeegor/django-explain', 39 | classifiers=[ 40 | 'Environment :: Web Environment', 41 | 'Intended Audience :: Developers', 42 | 'License :: OSI Approved :: MIT License', 43 | 'Operating System :: OS Independent', 44 | 'Programming Language :: Python', 45 | 'Programming Language :: Python :: 2.7', 46 | 'Programming Language :: Python :: 3.5', 47 | 'Topic :: Internet :: WWW/HTTP', 48 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 49 | ], 50 | cmdclass={ 51 | 'clean': CleanCommand, 52 | }, 53 | ) 54 | --------------------------------------------------------------------------------