Anthropomorphic white duck wearing jeweler's loupe and apron, examining gemstones at a workbench with tools, jewelry display cases, and a desk lamp in a warm workshop setting.

Last time ended with log_min_duration_statement set to a threshold you would actually investigate. The cost of that threshold is that everything below it is invisible, and the queries below it are frequently the story: one query that runs for a minute is a problem you can see, but twenty million queries at a millisecond each are five and a half hours of database time that never produce a single log line. pg_stat_statements shows you that aggregate. What it can’t show you is an actual specimen: a real execution, with its real parameter values, at its real moment. That’s what these three parameters buy, at a log volume you choose in advance.

The first two work as a pair, and both are superuser context like the rest of the family. log_min_duration_sample (default -1, disabled; milliseconds; 0 considers everything) sets the floor of the band you want to sample from. log_statement_sample_rate (default 1.0) is the dial: the fraction of statements over that floor that actually get logged. The output is indistinguishable from a log_min_duration_statement line, because it is the same line:

1[902/1] LOG: duration: 0.300 ms statement: SELECT 12 + 0

The sampling is a per-statement coin flip, not a stride. At 0.25 one of my test runs logged exactly 10 statements out of 40; at 0.5, 17 out of 40. Statistically honest, arithmetically untidy.

The rule that makes the pair safe to use: log_min_duration_statement has absolute priority. Any statement over it is logged, always, no matter what the sampling says; I verified this with log_statement_sample_rate = 0, and a 415ms query sailed into the log past a sampling configuration that nominally logs nothing. The corollary is that log_min_duration_sample only does anything when it’s set below log_min_duration_statement. Set it above and every statement it would sample is already being logged unconditionally. So the working configuration looks like: log_min_duration_statement = '1s' as the alarm, log_min_duration_sample = '100ms' and log_statement_sample_rate = 0.05 as the statistics.

That priority rule is not a design nicety; it’s a scar. Statement sampling was committed for PostgreSQL 12 in a simpler form: one dial, applied to the statements exceeding log_min_duration_statement itself. Which meant your single worst statement of the day could be flipped away by the sampler, and the one guarantee the log had always made (the worst statements are in it) quietly stopped being true. The community caught it during beta and reverted the feature outright, on the grounds that the second threshold had to ship as part of the feature rather than as a compatibility-breaking repair later. The two-threshold version landed in 13. The lesson generalizes to every sampling scheme you will ever build: decide first what must never be sampled away.

Whole transactions

log_transaction_sample_rate (default 0, also superuser, in core since PostgreSQL 12) samples on a different axis entirely. The coin is flipped once, when a transaction starts, and duration plays no part. If the transaction wins the flip, every statement in it is logged, BEGIN to COMMIT, each with its duration:

1[1062/1] LOG: duration: 0.099 ms statement: BEGIN;
2[1062/2] LOG: duration: 2.068 ms statement: INSERT INTO orders VALUES (9000001, 4242, now());
3[1062/3] LOG: duration: 397.231 ms statement: SELECT count(*) FROM orders WHERE customer_id = 4242;
4[1062/4] LOG: duration: 1.344 ms statement: COMMIT;

This is the trace you want when the problem isn’t a statement but a shape: which statement acquired the lock everything else queued behind, what ran before the slow one, whether the application is holding a transaction open across a network call it shouldn’t. Duration-threshold logging can’t show any of that, because it hands you the slow statement stripped of its context.

One trap to know about before you set it. A statement outside an explicit transaction is a transaction, so on an autocommit workload (which is to say, most ORM read traffic) log_transaction_sample_rate collapses into duration-blind statement sampling; my 40 autocommit statements at 0.5 produced 17 logged “transactions” of one statement each. On that kind of workload it generates volume without the coherence that justifies it. It earns its keep where multi-statement transactions are the norm, and even there it’s a parameter you set for an investigation and put back.

Which is true of all three. The steady state is the defaults, with log_min_duration_statement doing the daily work; the sampling pair comes out when the queries you need to see live below the alarm threshold, and the transaction dial comes out when you need context instead of specimens. If what you actually want is the aggregate, that was never a logging problem. That’s pg_stat_statements, and no sample rate will beat it at its own job.