Skip to content

Commit

Permalink
Add debug output for Butler queries
Browse files Browse the repository at this point in the history
Add an environment variable DAF_BUTLER_DEBUG_QUERIES that enables logging of queries and query plans.
  • Loading branch information
dhirving committed Jul 9, 2024
1 parent 45e570d commit 693dcb2
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 0 deletions.
23 changes: 23 additions & 0 deletions python/lsst/daf/butler/registry/interfaces/_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
]

import enum
import os
import sys
import uuid
import warnings
from abc import ABC, abstractmethod
Expand All @@ -53,6 +55,7 @@
from ...name_shrinker import NameShrinker
from ...timespan_database_representation import TimespanDatabaseRepresentation
from .._exceptions import ConflictingDefinitionError
from ._database_explain import get_query_plan


class DatabaseInsertMode(enum.Enum):
Expand Down Expand Up @@ -1864,6 +1867,7 @@ def query(
else:
connection = self._session_connection
try:
self._log_query_and_plan(connection, sql)
# TODO: SelectBase is not good for execute(), but it used
# everywhere, e.g. in daf_relation. We should switch to Executable
# at some point.
Expand All @@ -1875,6 +1879,25 @@ def query(
if connection is not self._session_connection:
connection.close()

def _log_query_and_plan(
self,
connection: sqlalchemy.Connection,
sql: sqlalchemy.sql.expression.Executable | sqlalchemy.sql.expression.SelectBase,
) -> None:
"""Log the given SQL statement and the DB's plan for executing it if
the environment variable DAF_BUTLER_DEBUG_QUERIES is set to a truthy
value.
"""
if os.environ.get("DAF_BUTLER_DEBUG_QUERIES", False):
assert isinstance(sql, sqlalchemy.SelectBase)
compiled = sql.compile(connection)
print(

Check warning on line 1894 in python/lsst/daf/butler/registry/interfaces/_database.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/daf/butler/registry/interfaces/_database.py#L1892-L1894

Added lines #L1892 - L1894 were not covered by tests
f"Executing SQL statement:\n{compiled}\nBind parameters: {compiled.params}", file=sys.stderr
)

query_plan = get_query_plan(connection, sql)
print(f"Query plan:\n{query_plan}", file=sys.stderr)

Check warning on line 1899 in python/lsst/daf/butler/registry/interfaces/_database.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/daf/butler/registry/interfaces/_database.py#L1898-L1899

Added lines #L1898 - L1899 were not covered by tests

@abstractmethod
def constant_rows(
self,
Expand Down
79 changes: 79 additions & 0 deletions python/lsst/daf/butler/registry/interfaces/_database_explain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# This file is part of daf_butler.
#
# Developed for the LSST Data Management System.
# This product includes software developed by the LSST Project
# (http://www.lsst.org).
# See the COPYRIGHT file at the top-level directory of this distribution
# for details of code ownership.
#
# This software is dual licensed under the GNU General Public License and also
# under a 3-clause BSD license. Recipients may choose which of these licenses
# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
# respectively. If you choose the GPL option then the following text applies
# (but note that there is still no warranty even if you opt for BSD instead):
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from __future__ import annotations

from typing import Any

from sqlalchemy import ClauseElement, Connection, Executable
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.sql.compiler import SQLCompiler


def get_query_plan(connection: Connection, sql: ClauseElement) -> str:
"""Retrieve the query plan for a given statement from the DB as a
human-readable string.
Parameters
----------
connection : `sqlalchemy.Connection`
Database connection used to retrieve query plan.
sql : `sqlalchemy.ClauseElement`
SQL statement for which we will retrieve a query plan.
"""
if connection.dialect.name != "postgresql":
# This could be implemented for SQLite using its EXPLAIN QUERY PLAN
# syntax, but the result rows are a little different and we haven't had
# a need for it yet.
return "(not available)"

Check warning on line 52 in python/lsst/daf/butler/registry/interfaces/_database_explain.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/daf/butler/registry/interfaces/_database_explain.py#L52

Added line #L52 was not covered by tests

with connection.execute(_Explain(sql)) as explain_cursor:
lines = explain_cursor.scalars().all()
return "\n".join(lines)

Check warning on line 56 in python/lsst/daf/butler/registry/interfaces/_database_explain.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/daf/butler/registry/interfaces/_database_explain.py#L55-L56

Added lines #L55 - L56 were not covered by tests


# This is based on code from the sqlalchemy wiki at
# https://github.com/sqlalchemy/sqlalchemy/wiki/Query-Plan-SQL-construct
class _Explain(Executable, ClauseElement):
"""Custom SQLAlchemy construct for retrieving query plan from the DB.
Parameters
----------
statement : `sqlalchemy.SelectBase`
SQLAlchemy SELECT statement to retrieve query plan for.
"""

def __init__(self, statement: ClauseElement) -> None:
self.statement = statement

Check warning on line 71 in python/lsst/daf/butler/registry/interfaces/_database_explain.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/daf/butler/registry/interfaces/_database_explain.py#L71

Added line #L71 was not covered by tests


@compiles(_Explain, "postgresql")
def _compile_explain(element: _Explain, compiler: SQLCompiler, **kw: Any) -> str:
text = "EXPLAIN "
text += compiler.process(element.statement, **kw)

Check warning on line 77 in python/lsst/daf/butler/registry/interfaces/_database_explain.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/daf/butler/registry/interfaces/_database_explain.py#L76-L77

Added lines #L76 - L77 were not covered by tests

return text

Check warning on line 79 in python/lsst/daf/butler/registry/interfaces/_database_explain.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/daf/butler/registry/interfaces/_database_explain.py#L79

Added line #L79 was not covered by tests

0 comments on commit 693dcb2

Please sign in to comment.