diff --git a/docs/cli.rst b/docs/cli.rst index 36ff029..da6a562 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -1800,6 +1800,23 @@ If the ``_counts`` table ever becomes out-of-sync with the actual table counts y $ sqlite-utils reset-counts mydb.db +.. _cli_analyze: + +Optimizing index usage with ANALYZE +=================================== + +The `SQLite ANALYZE command `__ builds a table of statistics which the query planner can use to make better decisions about which indexes to use for a given query. + +You should run ``ANALYZE`` if your database is large and you do not think your indexes are being efficiently used. + +To run ``ANALYZE`` against every index in a database, use this:: + + $ sqlite-utils analyze mydb.db + +You can run it against specific tables, or against specific named indexes, by passing them as optional arguments:: + + $ sqlite-utils analyze mydb.db mytable idx_mytable_name + .. _cli_vacuum: Vacuum diff --git a/docs/python-api.rst b/docs/python-api.rst index c79e5bb..6b0d542 100644 --- a/docs/python-api.rst +++ b/docs/python-api.rst @@ -2214,7 +2214,7 @@ Pass ``analyze=True`` to run ``ANALYZE`` against the new index after creating it Optimizing index usage with ANALYZE =================================== -The `SQLite ANALYZE command `__ can be used to build a table of statistics which the query planner can then use to make better decisions about which indexes to use for a given query. +The `SQLite ANALYZE command `__ builds a table of statistics which the query planner can use to make better decisions about which indexes to use for a given query. You should run ``ANALYZE`` if your database is large and you do not think your indexes are being efficiently used. diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index 05a7bcc..cbc091c 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -304,6 +304,26 @@ def rebuild_fts(path, tables, load_extension): db[table].rebuild_fts() +@cli.command() +@click.argument( + "path", + type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False), + required=True, +) +@click.argument("names", nargs=-1) +def analyze(path, names): + """Run ANALYZE against the whole database, or against specific named indexes and tables""" + db = sqlite_utils.Database(path) + try: + if names: + for name in names: + db.analyze(name) + else: + db.analyze() + except sqlite3.OperationalError as e: + raise click.ClickException(e) + + @cli.command() @click.argument( "path", diff --git a/tests/test_cli.py b/tests/test_cli.py index ca5c820..b4163c4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2057,3 +2057,41 @@ def test_create_database(tmpdir, enable_wal): assert db.journal_mode == "wal" else: assert db.journal_mode == "delete" + + +@pytest.mark.parametrize( + "options,expected", + ( + ( + [], + [ + {"tbl": "two_indexes", "idx": "idx_two_indexes_species", "stat": "1 1"}, + {"tbl": "two_indexes", "idx": "idx_two_indexes_name", "stat": "1 1"}, + {"tbl": "one_index", "idx": "idx_one_index_name", "stat": "1 1"}, + ], + ), + ( + ["one_index"], + [ + {"tbl": "one_index", "idx": "idx_one_index_name", "stat": "1 1"}, + ], + ), + ( + ["idx_two_indexes_name"], + [ + {"tbl": "two_indexes", "idx": "idx_two_indexes_name", "stat": "1 1"}, + ], + ), + ), +) +def test_analyze(tmpdir, options, expected): + db_path = str(tmpdir / "test.db") + db = Database(db_path) + db["one_index"].insert({"id": 1, "name": "Cleo"}, pk="id") + db["one_index"].create_index(["name"]) + db["two_indexes"].insert({"id": 1, "name": "Cleo", "species": "dog"}, pk="id") + db["two_indexes"].create_index(["name"]) + db["two_indexes"].create_index(["species"]) + result = CliRunner().invoke(cli.cli, ["analyze", db_path] + options) + assert result.exit_code == 0 + assert list(db["sqlite_stat1"].rows) == expected