psycodict.slowlog¶
Tools for analyzing the slow-query logs that psycodict writes.
When a query takes longer than the slowcutoff configured in the
[logging] section, PostgresBase._execute appends lines like the
following to the file configured as slowlogfile:
2026-07-20 20:19:14,940 - SELECT "label" FROM "curves" WHERE "n" = 5 ORDER BY "n" ran in \x1b[91m 0.35s \x1b[0m
2026-07-20 20:19:14,940 - Replicate with db.curves.analyze({'n': 5}, ['label'], None, 0)
The timing is wrapped in ANSI color escapes, the Replicate with hint
line follows the query it describes, and versions of this code from before
2019 wrote ... ran in 0.35s without the color escapes. A query whose
inlined values contain newlines spans several physical lines. Search
iterators log a third shape (normally only to the console, but captured
console output is worth parsing too):
Search iterator for curves {'n': 5} required a total of \x1b[91m0.35s\x1b[0m
The functions here parse such files, group the queries by shape (the
query with all constants removed), and produce a report aimed at two
questions: would raising slowcutoff shrink the log substantially, and
which queries might benefit from an index?
Typical usage:
from psycodict.slowlog import show_slow_report
show_slow_report("slow_queries.log", db=db) # db optional
or equivalently db.show_slow_report("slow_queries.log"). Nothing here
writes to the database: when db is provided, the report reads the
indexes recorded in meta_indexes (through table.list_indexes()) to
check whether the columns constrained by slow queries are covered.
- psycodict.slowlog.parse_slow_log(logfile, stats=None)[source]¶
Iterate over the records in a slow-query log file.
INPUT:
logfile– the filename of a slow-query log (as configured by theslowlogfilelogging option), or an open file objectstats– an optional dictionary, updated in place with the keyslines(physical lines read) andunparsed(lines that were not part of any recognized record)
OUTPUT:
An iterator of dictionaries, one per logged query, with keys:
timestamp– a datetime from the logging prefix (None if absent)duration– the logged runtime in seconds, as a floatquery– the SQL that was logged; for search iterator records, the query dictionary as a stringkind–"query"for ordinary statements,"iterator"forSearch iteratorrecordstable– the table the query was issued against, when the log provides it (from the adjacentReplicate withhint line, or from the search iterator line); otherwise None. A hint is attached only when its table appears in the query, since several processes appending to one log can interleave their lines.replicate– thedb.<table>.analyze(...)call from the hint line when present, for replaying the querylines– the number of physical log lines this record occupies, including its hint line and any continuation lines
Multi-line SQL (inlined values containing newlines) is reassembled. Unrecognized lines are skipped and counted in
stats, since a log that has accumulated for years contains lines in formats no longer in use.
- psycodict.slowlog.normalize_query(query)[source]¶
Normalize an SQL query (or a query dictionary rendered as a string) to its shape: literal values are replaced by
?so that queries differing only in their constants compare equal.numbers, quoted strings and boolean literals become
?(string literals and numbers directly after the jsonb path operators->,->>,#>,#>>are kept, since they select which field or array position is queried rather than which value)the contents of
ARRAY[...]literals collapse toARRAY[?]comma-separated lists of replaced values, as in
IN (1, 2, 3), collapse to(?)a clause repeated with
OR(orAND), as produced for example by$inon a jsonb column, collapses to a single copyruns of whitespace (including newlines) become a single space
Identifiers are left alone, even when they contain digits.
EXAMPLES:
sage: from psycodict.slowlog import normalize_query sage: normalize_query("SELECT \"a\" FROM \"t\" WHERE \"n\" = 5 AND \"s\" = 'x1' LIMIT 4") 'SELECT "a" FROM "t" WHERE "n" = ? AND "s" = ? LIMIT ?'
- psycodict.slowlog.normalize_dict_query(query)[source]¶
Normalize a query dictionary rendered as a string (the Python repr that
Search iteratorlog lines carry) to its shape.In a query dictionary the keys – column names and
$-operators like$gte– describe the structure of the query, and only the values are data, sonormalize_query()(which treats every quoted string as a literal value) would collapse structurally different queries such as{'n': {'$gte': 5}}and{'label': {'$lte': 'z'}}to the same shape. Here a quoted string or a number is kept exactly when it is followed by:, i.e. when it is a dictionary key:values (numbers, quoted strings,
True/False/None) become?lists and tuples of replaced values collapse:
[1, 2, 3]becomes[?], so$inqueries group independently of the list lengthruns of whitespace (including newlines) become a single space
EXAMPLES:
sage: from psycodict.slowlog import normalize_dict_query sage: normalize_dict_query("{'n': {'$gte': 5}, 'label': 'a'}") "{'n': {'$gte': ?}, 'label': ?}"
- psycodict.slowlog.slow_query_report(logfile, top=20, cutoff=None, db=None)[source]¶
Analyze a slow-query log file, grouping queries by shape.
INPUT:
logfile– the filename of a slow-query log (as configured by theslowlogfilelogging option), or an open file objecttop– the number of query shapes to include in each ranking (there are two: by total time and by mean time)cutoff– only consider queries at least this slow (in seconds). This simulates raisingslowcutoff: the report shows what the log would have contained with that threshold.db– an optionalPostgresDatabase. When provided, the suggestions check the columns constrained by each query shape against the indexes recorded inmeta_indexes(vialist_indexes) for the search tables involved, and suggestcreate_indexcalls for constrained columns that no existing index leads with.
OUTPUT:
A dictionary with keys:
logfile,cutoff– the corresponding inputslines– the number of physical lines in the fileunparsed– lines that were not part of any recognized recordrecords– the number of parsed query recordsskipped– records belowcutoff(0 when no cutoff is given); the rest of the report describes therecords - skippedotherstotal_time– their summed duration in secondspercentiles– a dictionary with keysp50,p90,p99andmax. The percentiles are computed from a histogram with 3 significant digits, so they are lower bounds accurate to about 1%; the max is exact.thresholds– a list of dictionaries with keyscutoff,records,linesandpercent: raisingslowcutoffto that value would have logged that many records, occupying that many physical lines, i.e. that percentage of the current line volume. The candidate cutoffs mix a fixed ladder with the observed percentiles.shapes– a list of dictionaries, one per query shape, sorted by total time, with keysshape,kind,count,total,mean,max,tables,example(the slowest query retained for this shape; see below),replicate(the hint-line call of that same example record, if it had one) andsuggestions(a list of strings; see the module documentation).shapes_by_mean– the same kind of list, the top shapes ordered by mean time instead of total time (the two lists overlap and share their dictionaries).
Memory use: the per-shape numeric aggregates (
count,total,max,tablesand the shape string itself) are exact and are kept for every distinct shape, so thattotal_time,percentiles,thresholdsand the reported aggregates do not depend ontop. This dictionary grows with the number of DISTINCT shapes – bounded in practice by the variety of queries the application issues, not by the length of the log. The large per-shape strings (exampleandreplicate), by contrast, are retained only for a bounded candidate set: the current leaders by total time (up to4 * topshapes) together with the current leaders by max duration (up to another4 * top), so that both the by-total and the by-mean rankings have reproducible examples. When a shape drops out of both pools its example is discarded, and if it later climbs back the next record of that shape refills it. The example of a reported shape is therefore the slowest of the records that arrived while the shape was retained – normally, but not always, its globally slowest record. Every reported shape has an example, and withtop=Noneall shapes retain theirs.