Seven parameters, one feature, and a reasonable goal of never using any of them.
geqo, geqo_threshold, geqo_effort, geqo_pool_size, geqo_generations, geqo_selection_bias, and geqo_seed all configure the Genetic Query Optimizer, an alternative join-order search that PostgreSQL falls back to when a query has too many relations for the regular planner to handle exhaustively. All seven are user context, so any of them can be set per session, per role, or per database.
The reason to take them as a group is that six of the seven only matter if you have already lost. The one that matters is geqo_threshold, and what it controls is whether the other six ever come into play at all.
What GEQO is, and where it came from
The regular PostgreSQL planner finds a join order by dynamic programming: it builds up the best plan for every worthwhile combination of relations, working from pairs upward, and arrives at a provably cheapest join order under its cost model. The documentation calls it a near-exhaustive search, “near” because the planner preferentially considers pairs of relations that actually have a join clause between them, and only considers clauseless pairs when a relation has no other option.
That search is complete but expensive, and its cost grows sharply with the number of relations. At some point it stops being worth it, because, as the documentation puts it, the exhaustive search takes too long, often longer than the penalty of executing a suboptimal plan. geqo_threshold is where PostgreSQL draws that line. Below it, dynamic programming. At or above it, GEQO.
GEQO itself was written by Martin Utesch for the Institute of Automatic Control at the University of Mining and Technology in Freiberg, Germany, and contributed in the 1990s. Its central idea is to treat join ordering as a traveling salesman problem and attack it with a genetic algorithm. Chapter 61 of the documentation, which still carries Utesch’s byline, is worth reading once if only for the historical texture.
How the genetic algorithm works
A candidate join order is encoded as a string of relation IDs. The documentation’s example is 4-1-3-2, meaning join relation 4 to relation 1, then bring in 3, then 2. That string is the chromosome.
The algorithm starts by generating a population of these strings at random. For each one, it calls the standard planner’s cost estimation machinery to price that join sequence, considering all three join strategies at each step, and the resulting cost is the candidate’s fitness. Low cost is fit. The least fit candidates are discarded, and new candidates are bred from the survivors by recombining portions of two parents. This repeats for a set number of generations, and the best sequence seen at any point becomes the plan.
Three implementation details from the documentation are worth having. It is a steady-state genetic algorithm, meaning it replaces the least fit individuals rather than the whole generation, which converges faster. Recombination uses edge recombination crossover, chosen because it preserves adjacency information, which is what matters in a TSP encoding. And mutation as a genetic operator is deprecated in this implementation, because mutating a join order at random tends to produce illegal tours that would then need repairing. Parts of the module are adapted from D. Whitley’s Genitor algorithm.
The most important thing to understand is what GEQO is not doing. It is not using a different cost model, and it is not making different decisions about scan types or join strategies. It calls the same cost estimator the regular planner does. The only thing GEQO changes is the search: instead of covering the space of join orders, it samples it. Everything good and bad about GEQO follows from that one substitution.
The parameters
geqo
Boolean, default on. The master switch. Turning it off means the regular planner handles every query no matter how many relations are involved, with no upper bound on how long that might take. The documentation advises that it is usually best not to turn this off in production, and points you at geqo_threshold for granular control instead. That advice is sound, for reasons we will get to.
geqo_threshold
Integer, default 12, minimum 2. GEQO is used to plan a query with at least this many FROM items. A FULL OUTER JOIN construct counts as only one FROM item, which is worth remembering when you are counting.
This is the parameter that matters, because it is the one that decides whether you are in GEQO’s world at all. The default of 12 dates to an era when 12 relations was a genuinely hard planning problem. On current hardware it frequently is not.
Note also the deliberate relationship with the collapse limits. from_collapse_limit and join_collapse_limit both default to 8, which sits below 12 on purpose: the intent is that flattening stops before a join problem grows large enough to trigger GEQO. This has a consequence people run into when they start tuning. Raising the collapse limits to let the planner consider more join orders can push a query over geqo_threshold, at which point you have traded a complete search of a small space for a random sample of a large one. The documentation warns about exactly this, and if you raise the collapse limits you should move geqo_threshold above them in the same edit.
geqo_effort
Integer from 1 to 10, default 5. This is the knob for people who want one knob, and the documentation is unusually candid about it: geqo_effort does not actually do anything directly. It exists only to compute default values for geqo_pool_size and geqo_generations when those are left at zero. Higher effort means a larger population and more generations, so more planning time and a better chance of stumbling onto a good join order.
If you are going to touch any of the algorithm knobs, this is the one. It is the only one with a coherent meaning at the level a DBA operates: more effort, more time, possibly better plan.
geqo_pool_size
Integer, default 0. The number of individuals in the genetic population. Zero means choose automatically based on geqo_effort and the number of relations in the query, which is what the function gimme_pool_size in geqo_main.c does. It must be at least two, and the documentation suggests useful values are typically in the hundreds to around a thousand.
geqo_generations
Integer, default 0. The number of iterations of the algorithm. Zero means choose automatically based on the pool size, via gimme_number_generations. Useful values are in the same range as the pool size.
Pool size and generations are the two axes of the search budget: how many candidates you hold at once, and how many rounds of breeding you run. Their product is roughly how much planning work you are buying.
geqo_selection_bias
Floating point, from 1.5 to 2.0, default 2.0. This controls the selection pressure, meaning how strongly the algorithm favors the fitter candidates when choosing parents.
Notice that the default is also the maximum. The only direction you can move this parameter is down, toward weaker selection pressure and a more diffuse search. There is no way to make GEQO greedier than it already is out of the box.
geqo_seed
Floating point, from 0 to 1, default 0. The initial value for the random number generator that GEQO uses to sample the join order space. The documentation’s advice is that varying it changes the set of join paths explored and may produce a better or worse result, which is a fair description of rolling dice.
This one has history worth knowing. Before Andres Freund’s 2009 change, GEQO started from an unspecified random state, which meant the same query planned twice could produce two different plans for no reason you could observe. The seed was introduced specifically to stop that. As the documentation now says, as long as geqo_seed and the other GEQO parameters are held fixed, the same plan will be generated for a given query and a given set of statistics.
What determinism does and does not buy you
That fixed seed is worth appreciating, because a planner that returns different plans on successive executions of an unchanged query is genuinely difficult to operate. But it is a narrower guarantee than it first appears, and the difference matters.
Under the regular planner, small changes tend to produce small consequences. Add a predicate, or let statistics drift a little, and the search is still complete, so the plan you get is still the cheapest one under the new inputs. The plan may change, but it changes for a reason you can follow.
Under GEQO, you are not getting the cheapest join order. You are getting the best of a few hundred randomly sampled ones. Change the query slightly, or run ANALYZE, and you are drawing a fresh sample from a differently-shaped space. The result can be a completely different plan with a completely different runtime, and no amount of reading the query will explain why, because the answer is which random join orders happened to come up this time. Determinism under a fixed seed means the dice are not rerolled between executions. It does not mean the plan is stable against anything else.
There is also no indication anywhere in EXPLAIN that GEQO planned a query. Nothing in the plan output announces it. If you want to know whether you are on the genetic path, you have to count FROM items and compare against geqo_threshold yourself.
Why five of these knobs are untunable in practice
geqo_pool_size, geqo_generations, geqo_selection_bias, geqo_seed, and to a lesser extent geqo_effort are hyperparameters of a genetic algorithm. Tuning hyperparameters is a real discipline, and it requires two things you do not have here.
The first is a fitness signal you can measure. Tuning a GA means evaluating configurations against a benchmark and keeping what wins. Your benchmark is one query, or a handful, and the measurement is how fast the resulting plan ran that afternoon on that data. That is not a signal you can optimize against; it is an anecdote.
The second is many trials. Genetic algorithm tuning involves running the thing hundreds or thousands of times across a representative workload. You are going to run it a few times on a slow report.
The project itself has never claimed otherwise. The GEQO chapter has said, continuously and including in the current release, that work is still needed to improve the genetic algorithm parameter settings, and points at the two functions that compute the defaults as the place where a compromise has to be found. That sentence has outlived most of the hardware it was written about.
So the practical position on five of these seven parameters is that they exist for completeness and for the benefit of anyone who wants to experiment with the algorithm itself. Turning geqo_effort up to 10 to buy a better plan on one slow query is a defensible move. Selecting a value for geqo_selection_bias is not a DBA activity, and nobody should feel bad about never having done it.
What the project thinks of GEQO
This is not a fringe opinion, and it is worth quoting rather than paraphrasing.
In 2021, John Naylor started a pgsql-hackers thread titled “a path towards replacing GEQO with something better,” which drew in Tomas Vondra, Robert Haas, Tom Lane, and Bruce Momjian. In the course of discussing whether GEQO would need to be kept around as a fallback, Naylor wrote that he got the feeling it was already de facto obsolete, that the project could adopt a policy of not considering improvements to it beyond bug fixes, and that this was “another way of saying ‘deprecated’.”
The proposed replacement drew on the modern literature, particularly Neumann and Radke’s work on adaptive optimization of very large join queries, with iterative dynamic programming as the fallback for genuinely enormous joins. Nothing from that thread has landed. GEQO is still what you get.
The most recent thing to happen to GEQO is a good illustration of its current standing. In October 2025, Robert Haas removed PlannerInfo’s join_search_private field and converted GEQO to use the new mechanism that lets planner extensions store private state, describing the change as treating GEQO as an in-core planner extension and noting that this was a useful test of the new facility. GEQO’s most recent contribution to PostgreSQL was serving as a convenient guinea pig for extension infrastructure.
What to actually do
Here is where the blanket position needs a qualification, because “turn GEQO off” is the wrong conclusion from all of the above.
Do not set geqo = off globally unless you control every query that reaches the database. GEQO exists to prevent a specific catastrophe: the planner spending minutes, or exhausting memory, on a thirty-way join before executing anything at all. A mediocre plan chosen in 50 milliseconds beats a perfect plan chosen in four minutes, and it beats an OOM kill by considerably more. If a reporting tool or an ORM can generate arbitrary SQL against your database, GEQO is the backstop that keeps a pathological query from taking the planner hostage. Leave it on.
Raise geqo_threshold instead. This is the actual lever. Twelve was calibrated for hardware two decades old, and moving it into the mid-to-high teens is a common and usually safe adjustment on modern machines. Do it per session first and measure.
Measure planning time, not just execution time. EXPLAIN reports Planning Time, and that is the number that tells you whether the exhaustive search is affordable at your new threshold. Watch it as you raise the threshold, on your actual queries.
Understand that the count is not the whole story. The cost of the near-exhaustive search depends on the shape of the join graph, not only the number of relations. A chain or a star, where each relation joins to few others, is far cheaper to search than a densely connected graph where nearly every pair has a join clause. This is why there is no single correct value for geqo_threshold: twenty relations in a snowflake schema and twenty relations in a tangle are different problems.
Treat hitting GEQO as information about your query. If production queries are routinely crossing the threshold, the interesting question is why they have that many relations in the first place. Views built on views, ORM eager loading pulling in half the schema, or partitionwise join across a large number of partitions are the usual sources, and each of them has a fix that is better than tuning a genetic algorithm.
Mind the collapse limits. As noted above, raising from_collapse_limit or join_collapse_limit without raising geqo_threshold above them is a way to accidentally hand your queries to GEQO while trying to improve them.
The summary
GEQO is a 1990s solution to a problem that modern hardware made much smaller, kept alive because the work required to replace it properly is substantial and nobody has finished it. Of its seven parameters, one is a genuine operational control, one is a master switch you should leave alone, and five are hyperparameters for an algorithm that the documentation has openly described as needing better parameter settings for as long as it has existed.
Leave geqo on as a backstop. Push geqo_threshold up until it is rarely reached, measuring planning time as you go. And on the day you find GEQO planning a query in production, read that as a fact about the query rather than an invitation to start tuning selection bias.