Refactor: N+1 query#3867
Conversation
and configure Django to use it. To activate it, set the environment variable DJANGO_DEBUG_TOOLBAR=true. The toolbar interferes with Pytest, so to run tests in VS Code or from the run.sh script, set DJANGO_DEBUG_TOOLBAR=false.
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
If session.flow equals None, this will avoid queries that look like SELECT ... WHERE "core_enrollmentflow"."id" IS NULL and which are unnecessary. If session.flow does not exist (KeyError), using .get on the session ensures safety so we can remove KeyError from the try...except.
If session.agency equals None, this will avoid queries that look like SELECT ... WHERE "core_transitagency"."id" IS NULL and which are unnecessary. If session.agency does not exist (KeyError), using .get on the session ensures safety so we can remove KeyError from the try...except.
The result from getting the transit agency from the request's session is cached to avoid querying the database every time session.agency(request) is called during the same request-response cycle.
The result from getting the flow from the request's session is cached to avoid querying the database every time session.flow(request) is called during the same request-response cycle.
which will avoid making extra queries by removing list, since the conversion to a list is not actually needed.
02cd7cf to
2cae695
Compare
by using select_related() which implements a SQL JOIN in a single database query. Limit the related fields to transit_processor_config which is a ForeignKey and accessed by the _agency_context processor.
| return models.TransitAgency.by_id(request.session[_AGENCY]) | ||
| except (KeyError, models.TransitAgency.DoesNotExist): | ||
| agency = models.TransitAgency.by_id(agency_id) | ||
| request._cached_agency = agency |
There was a problem hiding this comment.
Is there a more "Django" way to do this caching? Attaching a random prop on to the request feels a little hacky and like it would be easy to get out of control.
There was a problem hiding this comment.
Completely agree, I had the same feeling. I'll do a bit more searching for a cleaner, more "Django" way to do this. I think it's worth the effort it since it does lower the duplicated queries by a good bit especially on the /eligibility part.
| return models.EnrollmentFlow.by_id(request.session[_FLOW]) | ||
| except (KeyError, models.EnrollmentFlow.DoesNotExist): | ||
| flow = models.EnrollmentFlow.by_id(flow_id) | ||
| request._cached_flow = flow |
Scotchester
left a comment
There was a problem hiding this comment.
A few comments on DDT loading before I dig into the meat of the PR.
| if DEBUG_TOOLBAR: | ||
| INSTALLED_APPS.append("debug_toolbar") | ||
| MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") | ||
| INTERNAL_IPS = ["127.0.0.1"] |
There was a problem hiding this comment.
I believe this has no effect when overriding SHOW_TOOLBAR_CALLBACK completely. If I comment it out or change the IP, DDT still appears.
| INTERNAL_IPS = ["127.0.0.1"] |
If we weren't using Docker, I think we could not override SHOW_TOOLBAR_CALLBACK and rely only on INTERNAL_IPS, but Docker makes that complicated. There's a utility to try and help with that, but it's not well documented and apparently doesn't work with Docker Compose.
| def show_toolbar(request): | ||
| # Show the toolbar when in local development mode | ||
| return DEBUG | ||
|
|
||
| DEBUG_TOOLBAR_CONFIG = { | ||
| "SHOW_TOOLBAR_CALLBACK": show_toolbar, | ||
| } |
There was a problem hiding this comment.
This may not be worth the readability tradeoff, but you could simplify this with a lambda function:
| def show_toolbar(request): | |
| # Show the toolbar when in local development mode | |
| return DEBUG | |
| DEBUG_TOOLBAR_CONFIG = { | |
| "SHOW_TOOLBAR_CALLBACK": show_toolbar, | |
| } | |
| DEBUG_TOOLBAR_CONFIG = { | |
| "SHOW_TOOLBAR_CALLBACK": lambda request: DEBUG, | |
| } |
| # SECURITY WARNING: don't run with debug turned on in production! | ||
| DEBUG = os.environ.get("DJANGO_DEBUG", "false").lower() == "true" | ||
|
|
||
| # SECURITY WARNING: don't run with debug_toolbar turned on in production! |
There was a problem hiding this comment.
For extra caution?
| # SECURITY WARNING: don't run with debug_toolbar turned on in production! | |
| # SECURITY WARNING: don't run with debug_toolbar turned on in any non-local environment! |
I also wonder if it'd be nice to move this down with the activation code? Everything else up here are core Django settings.
|
I agree with the feedback to look for a better caching mechanism, but besides that, the query optimizations are great! |
Closes #3859
This PR installs the Django Debug Toolbar (DDT) to help diagnose
N+1 Queryissues. To turn on the toolbar, add aDJANGO_DEBUG_TOOLBARenvironment variable to your.envand set it totrue. It also makes the following changes to get rid of some redundant queries:sessionrefactor 1 -NULLagency/flow (aba8325 and 0363ca7): If an agency or a flow isn't selected (the user hasn't reached that part of the application), we shouldn't query the database.sessionrefactor 2 -cacheagency/flow (078b9d4 and 6d68958): This one feels a bit hacky but it is restricted to only two places in thesessionmodule and it does remove a lot of redundant queries, especially in theeligiblitypart of the application. Creating a sort of caching layer middleware would definitely be cleaner and maybe it's something we can come back to.select_related()(22deca9): Implements a SQL JOIN in a single database query. It limits the related fields totransit_processor_configwhich is aForeignKeyand accessed by the_agency_contextcontext processor.Interpreting SQL results from the DDT
To look for redundant queries, look for SQL items that say
Duplicated N times.. For example, the screenshot below highlights aSELECTquery duplicated 5 times in the DDT. Note that each instance (duplicate) of the query is shown as a unique item (the screenshot below has been truncated to two items, if you were to keep scrolling down you'd see all five items listed).So a helpful method to troubleshoot redundant queries is:
Duplicated N times..pyfile (or template) created the query.Duplicated N times.Reviewing
Turn on the DDT. Confirm the following drop in the number of queries by checking out the commit associated with each refactor and running through the application:
main(or 19cf724)NULLagency/flowcacheagency/flowselect_related()//providers/eligibility(regional)/eligibility/eligibility/start(regional)/eligibility/start