Repository: ibis-project/ibis

Issue:
bug: AttributeError: 'str' object has no attribute 'exported_columns'
### What happened?

Sorry for the long repro example, but I have no idea what part of this is significant.

```python
from __future__ import annotations
import pandas as pd

import ibis
ibis.options.interactive = True


def group_id(t: Table, keys: str | Column | Iterable[str | Column]) -> IntegerColumn:
    """Number each group from 0 to the "number of groups - 1"

    This is equivalent to pandas.DataFrame.groupby(keys).ngroup().
    """
    # We need an arbitrary column to use for dense_rank
    col: Column = t[t.columns[0]]
    return col.dense_rank().over(ibis.window(order_by=keys))

def with_nunique_per_group(
    t: Table, group_cols: list[str], count_cols: str, result_col: str
) -> Table:
    """Semantically: t[count_cols].nunique().over(ibis.window(group_by=group_cols))

    AKA

    SELECT COUNT(DISTINCT count_cols) OVER (PARTITION BY group_cols) from t

    Except the above doesn't work in duckdb, so we use a workaround.
    """
    # Using a simple count distinct over a window is not possible because we get
    # (duckdb.ParserException) Parser Error: DISTINCT is not implemented for window functions!  # noqa: E501
    if isinstance(count_cols, str):
        count_cols = [count_cols]
    if isinstance(group_cols, str):
        group_cols = [group_cols]
    if result_col in t:
        t = t.drop(result_col)
    t2 = t.mutate(__group_id=group_id(t, group_cols))
    distinct = t2[["__group_id"] + count_cols].distinct()
    vc = distinct.group_by("__group_id").agg(**{result_col: ibis._.count()})
    return t2.inner_join(vc, "__group_id").drop("__group_id")

# Context: Trying to which addresses are associated with business entities,
# that aren't individual residences. Business addresses have many people
# associated with them, but few employers
columns = ["is_business", "street", "first_name", "last_name", "employer"]
data = [
    [True, None, "p", "barnes", "laborers local 341"],
    [True, "2501 commercial dr", "p", "barnes", "laborers local 341"],
    [True, "2501 commercial dr", "p", "barnes", "laborers local 341"],
    [True, "2501 commercial dr", "j", "brady", "laborers local 341"],
    [True, "2501 commercial dr", "l", "hepburn", "local 341"],
    [True, "2501 commercial dr", "l", "leon", "laborers local 341"],
    [True, "2501 commercial dr", "james", "miller", "local 341"],
    [True, "2501 commercial dr", "b", "monks", "local 341"],
    [True, "2501 commercial dr", "n", "monks", "local 341"],
    [True, "2501 commercial dr", "vance", "putman", "laborers local 341"],
    [False, "2413 oak dr", "adam", "baldwin", "anchorage museum"],
    [False, "4081 e 20th ave", "clara", "baldwin", "asd"],
    [False, "4081 e 20th ave", "clara", "baldwin", "asd"],
    [False, "3000 vintage blvd", "clarie", "baldwin", "real estate group"],
    [False, "6461 e hart lake loop", "daniel", "baldwin", "roger hickel"],
]
df = pd.DataFrame(data, columns=columns).convert_dtypes()
t = ibis.memtable(df)

address_id_cols = "street"
person_id_cols = ["first_name", "last_name"]
employer_id_cols = "employer"

t2 = with_nunique_per_group(t, address_id_cols, person_id_cols, "npeople_per_address")
t3 = with_nunique_per_group(
    t2, address_id_cols, employer_id_cols, "nemployers_per_address"
)
# evaluating t2 is fine, however
t3
```
gives
```python traceback
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/IPython/core/formatters.py:972, in MimeBundleFormatter.__call__(self, obj, include, exclude)
    969     method = get_real_method(obj, self.print_method)
    971     if method is not None:
--> 972         return method(include=include, exclude=exclude)
    973     return None
    974 else:

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/expr/types/core.py:31, in _FixedTextJupyterMixin._repr_mimebundle_(self, *args, **kwargs)
     30 def _repr_mimebundle_(self, *args, **kwargs):
---> 31     bundle = super()._repr_mimebundle_(*args, **kwargs)
     32     bundle["text/plain"] = bundle["text/plain"].rstrip()
     33     return bundle

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/rich/jupyter.py:48, in JupyterMixin._repr_mimebundle_(self, include, exclude, **kwargs)
     41 def _repr_mimebundle_(
     42     self: "ConsoleRenderable",
     43     include: Sequence[str],
     44     exclude: Sequence[str],
     45     **kwargs: Any,
     46 ) -> Dict[str, str]:
     47     console = get_console()
---> 48     segments = list(console.render(self, console.options))
     49     html = _render_segments(segments)
     50     text = console._render_buffer(segments)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/rich/console.py:1306, in Console.render(self, renderable, options)
   1304 renderable = rich_cast(renderable)
   1305 if hasattr(renderable, "__rich_console__") and not isclass(renderable):
-> 1306     render_iterable = renderable.__rich_console__(self, _options)  # type: ignore[union-attr]
   1307 elif isinstance(renderable, str):
   1308     text_renderable = self.render_str(
   1309         renderable, highlight=_options.highlight, markup=_options.markup
   1310     )

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/expr/types/relations.py:141, in Table.__rich_console__(self, console, options)
    138 else:
    139     width = options.max_width
--> 141 table = to_rich_table(self, width)
    142 return console.render(table, options=options)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/expr/types/pretty.py:230, in to_rich_table(table, console_width)
    228 # Compute the data and return a pandas dataframe
    229 nrows = ibis.options.repr.interactive.max_rows
--> 230 result = table.limit(nrows + 1).execute()
    232 # Now format the columns in order, stopping if the console width would
    233 # be exceeded.
    234 col_info = []

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/expr/types/core.py:300, in Expr.execute(self, limit, timecontext, params, **kwargs)
    273 def execute(
    274     self,
    275     limit: int | str | None = 'default',
   (...)
    278     **kwargs: Any,
    279 ):
    280     """Execute an expression against its backend if one exists.
    281 
    282     Parameters
   (...)
    298         Keyword arguments
    299     """
--> 300     return self._find_backend(use_default=True).execute(
    301         self, limit=limit, timecontext=timecontext, params=params, **kwargs
    302     )

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/__init__.py:235, in BaseSQLBackend.execute(self, expr, params, limit, **kwargs)
    233 kwargs.pop('timecontext', None)
    234 query_ast = self.compiler.to_ast_ensure_limit(expr, limit, params=params)
--> 235 sql = query_ast.compile()
    236 self._log(sql)
    238 schema = self.ast_schema(query_ast, **kwargs)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/base.py:40, in QueryAST.compile(self)
     38 def compile(self):
     39     compiled_setup_queries = [q.compile() for q in self.setup_queries]
---> 40     compiled_queries = [q.compile() for q in self.queries]
     41     compiled_teardown_queries = [q.compile() for q in self.teardown_queries]
     42     return self.context.collapse(
     43         list(
     44             chain(
   (...)
     49         )
     50     )

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/base.py:40, in <listcomp>(.0)
     38 def compile(self):
     39     compiled_setup_queries = [q.compile() for q in self.setup_queries]
---> 40     compiled_queries = [q.compile() for q in self.queries]
     41     compiled_teardown_queries = [q.compile() for q in self.teardown_queries]
     42     return self.context.collapse(
     43         list(
     44             chain(
   (...)
     49         )
     50     )

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/query_builder.py:173, in AlchemySelect.compile(self)
    169 def compile(self):
    170     # Can't tell if this is a hack or not. Revisit later
    171     self.context.set_query(self)
--> 173     self._compile_subqueries()
    175     frag = self._compile_table_set()
    176     steps = [
    177         self._add_select,
    178         self._add_group_by,
   (...)
    181         self._add_limit,
    182     ]

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/query_builder.py:194, in AlchemySelect._compile_subqueries(self)
    191     return
    193 for expr in self.subqueries:
--> 194     result = self.context.get_compiled_expr(expr)
    195     alias = self.context.get_ref(expr)
    196     result = result.cte(alias)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/translator.py:70, in QueryContext.get_compiled_expr(self, node)
     68     result = node.query
     69 else:
---> 70     result = self._compile_subquery(node)
     72 self.top_context.subquery_memo[node] = result
     73 return result

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/translator.py:33, in QueryContext._compile_subquery(self, op)
     31 def _compile_subquery(self, op):
     32     sub_ctx = self.subcontext()
---> 33     return self._to_sql(op, sub_ctx)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/translator.py:36, in QueryContext._to_sql(self, expr, ctx)
     35 def _to_sql(self, expr, ctx):
---> 36     return self.compiler.to_sql(expr, ctx)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/query_builder.py:396, in AlchemyCompiler.to_sql(cls, expr, context, params, exists)
    394 if exists:
    395     query.exists = True
--> 396 return query.compile()

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/query_builder.py:185, in AlchemySelect.compile(self)
    176 steps = [
    177     self._add_select,
    178     self._add_group_by,
   (...)
    181     self._add_limit,
    182 ]
    184 for step in steps:
--> 185     frag = step(frag)
    187 return frag

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/query_builder.py:218, in AlchemySelect._add_select(self, table_set)
    216 for op in select_set:
    217     if isinstance(op, ops.Value):
--> 218         arg = self._translate(op, named=True)
    219     elif isinstance(op, ops.TableNode):
    220         arg = context.get_ref(op)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/query_builder.py:218, in Select._translate(self, expr, named, permit_subquery)
    211 def _translate(self, expr, named=False, permit_subquery=False):
    212     translator = self.translator_class(
    213         expr,
    214         context=self.context,
    215         named=named,
    216         permit_subquery=permit_subquery,
    217     )
--> 218     return translator.get_result()

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/translator.py:211, in ExprTranslator.get_result(self)
    209 def get_result(self):
    210     """Compile SQL expression into a string."""
--> 211     translated = self.translate(self.node)
    212     if self._needs_name(self.node):
    213         # TODO: this could fail in various ways
    214         translated = self.name(translated, self.node.name)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/translator.py:243, in ExprTranslator.translate(self, op)
    241 elif type(op) in self._registry:
    242     formatter = self._registry[type(op)]
--> 243     return formatter(self, op)
    244 else:
    245     raise com.OperationNotDefinedError(f'No translation rule for {type(op)}')

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/registry.py:112, in _table_column(t, op)
    108 table = op.table
    110 sa_table = get_sqla_table(ctx, table)
--> 112 out_expr = get_col(sa_table, op)
    113 out_expr.quote = t._always_quote_columns
    115 # If the column does not originate from the table set in the current SELECT
    116 # context, we should format as a subquery

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/registry.py:85, in get_col(sa_table, op)
     83 def get_col(sa_table, op: ops.TableColumn) -> sa.sql.ColumnClause:
     84     """Extract a column from a table."""
---> 85     cols = sa_table.exported_columns
     86     colname = op.name
     88     if (col := cols.get(colname)) is not None:

AttributeError: 'str' object has no attribute 'exported_columns'
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/IPython/core/formatters.py:706, in PlainTextFormatter.__call__(self, obj)
    699 stream = StringIO()
    700 printer = pretty.RepresentationPrinter(stream, self.verbose,
    701     self.max_width, self.newline,
    702     max_seq_length=self.max_seq_length,
    703     singleton_pprinters=self.singleton_printers,
    704     type_pprinters=self.type_printers,
    705     deferred_pprinters=self.deferred_printers)
--> 706 printer.pretty(obj)
    707 printer.flush()
    708 return stream.getvalue()

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/IPython/lib/pretty.py:410, in RepresentationPrinter.pretty(self, obj)
    407                         return meth(obj, self, cycle)
    408                 if cls is not object \
    409                         and callable(cls.__dict__.get('__repr__')):
--> 410                     return _repr_pprint(obj, self, cycle)
    412     return _default_pprint(obj, self, cycle)
    413 finally:

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/IPython/lib/pretty.py:778, in _repr_pprint(obj, p, cycle)
    776 """A pprint that just redirects to the normal repr function."""
    777 # Find newlines and replace them with p.break_()
--> 778 output = repr(obj)
    779 lines = output.splitlines()
    780 with p.group():

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/expr/types/core.py:54, in Expr.__repr__(self)
     52 with simple_console.capture() as capture:
     53     try:
---> 54         simple_console.print(self)
     55     except TranslationError as e:
     56         lines = [
     57             "Translation to backend failed",
     58             f"Error message: {e.args[0]}",
     59             "Expression repr follows:",
     60             self._repr(),
     61         ]

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/rich/console.py:1694, in Console.print(self, sep, end, style, justify, overflow, no_wrap, emoji, markup, highlight, width, height, crop, soft_wrap, new_line_start, *objects)
   1692 if style is None:
   1693     for renderable in renderables:
-> 1694         extend(render(renderable, render_options))
   1695 else:
   1696     for renderable in renderables:

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/rich/console.py:1306, in Console.render(self, renderable, options)
   1304 renderable = rich_cast(renderable)
   1305 if hasattr(renderable, "__rich_console__") and not isclass(renderable):
-> 1306     render_iterable = renderable.__rich_console__(self, _options)  # type: ignore[union-attr]
   1307 elif isinstance(renderable, str):
   1308     text_renderable = self.render_str(
   1309         renderable, highlight=_options.highlight, markup=_options.markup
   1310     )

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/expr/types/relations.py:141, in Table.__rich_console__(self, console, options)
    138 else:
    139     width = options.max_width
--> 141 table = to_rich_table(self, width)
    142 return console.render(table, options=options)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/expr/types/pretty.py:230, in to_rich_table(table, console_width)
    228 # Compute the data and return a pandas dataframe
    229 nrows = ibis.options.repr.interactive.max_rows
--> 230 result = table.limit(nrows + 1).execute()
    232 # Now format the columns in order, stopping if the console width would
    233 # be exceeded.
    234 col_info = []

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/expr/types/core.py:300, in Expr.execute(self, limit, timecontext, params, **kwargs)
    273 def execute(
    274     self,
    275     limit: int | str | None = 'default',
   (...)
    278     **kwargs: Any,
    279 ):
    280     """Execute an expression against its backend if one exists.
    281 
    282     Parameters
   (...)
    298         Keyword arguments
    299     """
--> 300     return self._find_backend(use_default=True).execute(
    301         self, limit=limit, timecontext=timecontext, params=params, **kwargs
    302     )

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/__init__.py:235, in BaseSQLBackend.execute(self, expr, params, limit, **kwargs)
    233 kwargs.pop('timecontext', None)
    234 query_ast = self.compiler.to_ast_ensure_limit(expr, limit, params=params)
--> 235 sql = query_ast.compile()
    236 self._log(sql)
    238 schema = self.ast_schema(query_ast, **kwargs)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/base.py:40, in QueryAST.compile(self)
     38 def compile(self):
     39     compiled_setup_queries = [q.compile() for q in self.setup_queries]
---> 40     compiled_queries = [q.compile() for q in self.queries]
     41     compiled_teardown_queries = [q.compile() for q in self.teardown_queries]
     42     return self.context.collapse(
     43         list(
     44             chain(
   (...)
     49         )
     50     )

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/base.py:40, in <listcomp>(.0)
     38 def compile(self):
     39     compiled_setup_queries = [q.compile() for q in self.setup_queries]
---> 40     compiled_queries = [q.compile() for q in self.queries]
     41     compiled_teardown_queries = [q.compile() for q in self.teardown_queries]
     42     return self.context.collapse(
     43         list(
     44             chain(
   (...)
     49         )
     50     )

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/query_builder.py:173, in AlchemySelect.compile(self)
    169 def compile(self):
    170     # Can't tell if this is a hack or not. Revisit later
    171     self.context.set_query(self)
--> 173     self._compile_subqueries()
    175     frag = self._compile_table_set()
    176     steps = [
    177         self._add_select,
    178         self._add_group_by,
   (...)
    181         self._add_limit,
    182     ]

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/query_builder.py:194, in AlchemySelect._compile_subqueries(self)
    191     return
    193 for expr in self.subqueries:
--> 194     result = self.context.get_compiled_expr(expr)
    195     alias = self.context.get_ref(expr)
    196     result = result.cte(alias)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/translator.py:70, in QueryContext.get_compiled_expr(self, node)
     68     result = node.query
     69 else:
---> 70     result = self._compile_subquery(node)
     72 self.top_context.subquery_memo[node] = result
     73 return result

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/translator.py:33, in QueryContext._compile_subquery(self, op)
     31 def _compile_subquery(self, op):
     32     sub_ctx = self.subcontext()
---> 33     return self._to_sql(op, sub_ctx)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/translator.py:36, in QueryContext._to_sql(self, expr, ctx)
     35 def _to_sql(self, expr, ctx):
---> 36     return self.compiler.to_sql(expr, ctx)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/query_builder.py:396, in AlchemyCompiler.to_sql(cls, expr, context, params, exists)
    394 if exists:
    395     query.exists = True
--> 396 return query.compile()

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/query_builder.py:185, in AlchemySelect.compile(self)
    176 steps = [
    177     self._add_select,
    178     self._add_group_by,
   (...)
    181     self._add_limit,
    182 ]
    184 for step in steps:
--> 185     frag = step(frag)
    187 return frag

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/query_builder.py:218, in AlchemySelect._add_select(self, table_set)
    216 for op in select_set:
    217     if isinstance(op, ops.Value):
--> 218         arg = self._translate(op, named=True)
    219     elif isinstance(op, ops.TableNode):
    220         arg = context.get_ref(op)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/query_builder.py:218, in Select._translate(self, expr, named, permit_subquery)
    211 def _translate(self, expr, named=False, permit_subquery=False):
    212     translator = self.translator_class(
    213         expr,
    214         context=self.context,
    215         named=named,
    216         permit_subquery=permit_subquery,
    217     )
--> 218     return translator.get_result()

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/translator.py:211, in ExprTranslator.get_result(self)
    209 def get_result(self):
    210     """Compile SQL expression into a string."""
--> 211     translated = self.translate(self.node)
    212     if self._needs_name(self.node):
    213         # TODO: this could fail in various ways
    214         translated = self.name(translated, self.node.name)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/translator.py:243, in ExprTranslator.translate(self, op)
    241 elif type(op) in self._registry:
    242     formatter = self._registry[type(op)]
--> 243     return formatter(self, op)
    244 else:
    245     raise com.OperationNotDefinedError(f'No translation rule for {type(op)}')

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/registry.py:112, in _table_column(t, op)
    108 table = op.table
    110 sa_table = get_sqla_table(ctx, table)
--> 112 out_expr = get_col(sa_table, op)
    113 out_expr.quote = t._always_quote_columns
    115 # If the column does not originate from the table set in the current SELECT
    116 # context, we should format as a subquery

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/registry.py:85, in get_col(sa_table, op)
     83 def get_col(sa_table, op: ops.TableColumn) -> sa.sql.ColumnClause:
     84     """Extract a column from a table."""
---> 85     cols = sa_table.exported_columns
     86     colname = op.name
     88     if (col := cols.get(colname)) is not None:

AttributeError: 'str' object has no attribute 'exported_columns'
```

### What version of ibis are you using?

4.1.0

### What backend(s) are you using, if any?

duckdb

### Relevant log output

_No response_

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:
Thanks for the report, @NickCrews !  
Hopefully we can figure out a simpler reproducer, but at the core of this issue is that `get_sqla_table` should _definitely not_ be returning a string.
And a quick aside, if you find yourself writing this pattern a lot:
```python
    if isinstance(count_cols, str):
        count_cols = [count_cols]
    if isinstance(group_cols, str):
        group_cols = [group_cols]
```

you can make use of `ibis.util.promote_list` which will also handle iterables, and returns an empty list if you give it a `None`
Ok, I went down a rabbit hole and now I'm lost, but:

I think the issue here is with `ibis.backends.base.sql.compiler.translator.make_alias` or rather that we don't have a specialized method for the alchemy backends.  This is what generates the names used in the table_refs and I think that for an alchemy backend we probably want them to be a sqlalchemy alias that points to the associated node, so that it has, for instance, an `exported_columns` attribute.  
But it could be the issue is elsewhere and this is just a separate side-effect, I'm not certain.  But I don't see how anything can get set in `ctx.table_refs` that isn't just a string of the `alias` name.
I bet this is a doozy, thanks for exploring @gforsyth 
I hit this in a separate work stream (fixing our core traversal algorithm). I'll work towards addressing it there when I put up a working PR.

Resolving patch:
diff --git a/ibis/backends/pyspark/__init__.py b/ibis/backends/pyspark/__init__.py
--- a/ibis/backends/pyspark/__init__.py
+++ b/ibis/backends/pyspark/__init__.py
@@ -195,7 +195,7 @@ def compile(self, expr, timecontext=None, params=None, *args, **kwargs):
             expr.op(),
             scope=scope,
             timecontext=timecontext,
-            session=self._session,
+            session=getattr(self, "_session", None),
         )
 
     def execute(self, expr: ir.Expr, **kwargs: Any) -> Any:
diff --git a/ibis/common/graph.py b/ibis/common/graph.py
--- a/ibis/common/graph.py
+++ b/ibis/common/graph.py
@@ -166,10 +166,7 @@ def toposort(node: Node) -> Graph:
 
 
 def traverse(
-    fn: Callable[[Node], tuple[bool | Iterable, Any]],
-    node: Iterable[Node],
-    dedup: bool = True,
-    filter=Node,
+    fn: Callable[[Node], tuple[bool | Iterable, Any]], node: Iterable[Node], filter=Node
 ) -> Iterator[Any]:
     """Utility for generic expression tree traversal.
 
@@ -180,8 +177,6 @@ def traverse(
         controls the traversal, and the second is the result if its not `None`.
     node
         The Node expression or a list of expressions.
-    dedup
-        Whether to allow expression traversal more than once
     filter
         Restrict initial traversal to this kind of node
     """
@@ -192,11 +187,10 @@ def traverse(
     while todo:
         node = todo.pop()
 
-        if dedup:
-            if node in seen:
-                continue
-            else:
-                seen.add(node)
+        if node in seen:
+            continue
+        else:
+            seen.add(node)
 
         control, result = fn(node)
         if result is not None:
diff --git a/ibis/expr/analysis.py b/ibis/expr/analysis.py
--- a/ibis/expr/analysis.py
+++ b/ibis/expr/analysis.py
@@ -8,7 +8,9 @@
 import toolz
 
 import ibis.common.graph as g
+import ibis.expr.analysis as an
 import ibis.expr.operations as ops
+import ibis.expr.operations.relations as rels
 import ibis.expr.types as ir
 from ibis import util
 from ibis.common.exceptions import IbisTypeError, IntegrityError
@@ -774,28 +776,15 @@ def predicate(node):
 
 def find_subqueries(node: ops.Node) -> Counter:
     counts = Counter()
-
-    def finder(node: ops.Node):
-        if isinstance(node, ops.Join):
-            return [node.left, node.right], None
-        elif isinstance(node, ops.PhysicalTable):
-            return g.halt, None
-        elif isinstance(node, ops.SelfReference):
-            return g.proceed, None
-        elif isinstance(node, (ops.Selection, ops.Aggregation)):
-            counts[node] += 1
-            return [node.table], None
-        elif isinstance(node, ops.TableNode):
-            counts[node] += 1
-            return g.proceed, None
-        elif isinstance(node, ops.TableColumn):
-            return node.table not in counts, None
-        else:
-            return g.proceed, None
-
-    # keep duplicates so we can determine where an expression is used
-    # more than once
-    list(g.traverse(finder, node, dedup=False))
+    for graph in map(g.Graph.from_dfs, util.promote_list(node)):
+        dependents = graph.invert()
+        for u, vs in dependents.toposort().items():
+            # count the number of table-node dependents on the current node
+            # but only if the current node is a selection or aggregation
+            if isinstance(u, (rels.Projection, rels.Aggregation)):
+                counts[u] += len(
+                    set(toolz.concat(map(an.find_immediate_parent_tables, vs)))
+                )
 
     return counts
 


Test patch:
diff --git a/ibis/backends/bigquery/tests/system/snapshots/test_client/test_cross_project_query/out.sql b/ibis/backends/bigquery/tests/system/snapshots/test_client/test_cross_project_query/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/bigquery/tests/system/snapshots/test_client/test_cross_project_query/out.sql
@@ -0,0 +1,6 @@
+SELECT t0.`title`, t0.`tags`
+FROM (
+  SELECT t1.*
+  FROM `bigquery-public-data.stackoverflow.posts_questions` t1
+  WHERE STRPOS(t1.`tags`, 'ibis') - 1 >= 0
+) t0
\ No newline at end of file
diff --git a/ibis/backends/bigquery/tests/system/snapshots/test_client/test_subquery_scalar_params/out.sql b/ibis/backends/bigquery/tests/system/snapshots/test_client/test_subquery_scalar_params/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/bigquery/tests/system/snapshots/test_client/test_subquery_scalar_params/out.sql
@@ -0,0 +1,11 @@
+WITH t0 AS (
+  SELECT t2.`float_col`, t2.`timestamp_col`, t2.`int_col`, t2.`string_col`
+  FROM `ibis-gbq.ibis_gbq_testing.functional_alltypes` t2
+  WHERE t2.`timestamp_col` < @param_0
+)
+SELECT count(t1.`foo`) AS `count`
+FROM (
+  SELECT t0.`string_col`, sum(t0.`float_col`) AS `foo`
+  FROM t0
+  GROUP BY 1
+) t1
\ No newline at end of file
diff --git a/ibis/backends/bigquery/tests/system/test_client.py b/ibis/backends/bigquery/tests/system/test_client.py
--- a/ibis/backends/bigquery/tests/system/test_client.py
+++ b/ibis/backends/bigquery/tests/system/test_client.py
@@ -1,7 +1,7 @@
 import collections
 import datetime
 import decimal
-import re
+import itertools
 
 import pandas as pd
 import pandas.testing as tm
@@ -10,6 +10,7 @@
 
 import ibis
 import ibis.expr.datatypes as dt
+import ibis.expr.operations as ops
 from ibis.backends.bigquery.client import bigquery_param
 
 
@@ -112,18 +113,8 @@ def test_different_partition_col_name(monkeypatch, client):
     assert col in parted_alltypes.columns
 
 
-def test_subquery_scalar_params(alltypes, project_id, dataset_id):
-    expected = f"""\
-SELECT count\\(t0\\.`foo`\\) AS `count`
-FROM \\(
-  SELECT t1\\.`string_col`, sum\\(t1\\.`float_col`\\) AS `foo`
-  FROM \\(
-    SELECT t2\\.`float_col`, t2\\.`timestamp_col`, t2\\.`int_col`, t2\\.`string_col`
-    FROM `{project_id}\\.{dataset_id}\\.functional_alltypes` t2
-    WHERE t2\\.`timestamp_col` < @param_\\d+
-  \\) t1
-  GROUP BY 1
-\\) t0"""
+def test_subquery_scalar_params(alltypes, monkeypatch, snapshot):
+    monkeypatch.setattr(ops.ScalarParameter, "_counter", itertools.count())
     t = alltypes
     p = ibis.param("timestamp").name("my_param")
     expr = (
@@ -136,7 +127,7 @@ def test_subquery_scalar_params(alltypes, project_id, dataset_id):
         .name("count")
     )
     result = expr.compile(params={p: "20140101"})
-    assert re.match(expected, result) is not None
+    snapshot.assert_match(result, "out.sql")
 
 
 def test_repr_struct_of_array_of_struct():
@@ -220,18 +211,11 @@ def test_parted_column(client, kind):
     assert t.columns == [expected_column, "string_col", "int_col"]
 
 
-def test_cross_project_query(public):
+def test_cross_project_query(public, snapshot):
     table = public.table("posts_questions")
     expr = table[table.tags.contains("ibis")][["title", "tags"]]
     result = expr.compile()
-    expected = """\
-SELECT t0.`title`, t0.`tags`
-FROM (
-  SELECT t1.*
-  FROM `bigquery-public-data.stackoverflow.posts_questions` t1
-  WHERE STRPOS(t1.`tags`, 'ibis') - 1 >= 0
-) t0"""
-    assert result == expected
+    snapshot.assert_match(result, "out.sql")
     n = 5
     df = expr.limit(n).execute()
     assert len(df) == n
diff --git a/ibis/backends/impala/tests/snapshots/test_exprs/test_filter_with_analytic/out.sql b/ibis/backends/impala/tests/snapshots/test_exprs/test_filter_with_analytic/out.sql
--- a/ibis/backends/impala/tests/snapshots/test_exprs/test_filter_with_analytic/out.sql
+++ b/ibis/backends/impala/tests/snapshots/test_exprs/test_filter_with_analytic/out.sql
@@ -1,15 +1,18 @@
-SELECT t0.`col`, t0.`analytic`
+WITH t0 AS (
+  SELECT t4.`col`, NULL AS `filter`
+  FROM x t4
+),
+t1 AS (
+  SELECT t0.*
+  FROM t0
+  WHERE t0.`filter` IS NULL
+),
+t2 AS (
+  SELECT t1.`col`, t1.`filter`
+  FROM t1
+)
+SELECT t3.`col`, t3.`analytic`
 FROM (
-  SELECT t1.`col`, count(1) OVER () AS `analytic`
-  FROM (
-    SELECT t2.`col`, t2.`filter`
-    FROM (
-      SELECT t3.*
-      FROM (
-        SELECT t4.`col`, NULL AS `filter`
-        FROM x t4
-      ) t3
-      WHERE t3.`filter` IS NULL
-    ) t2
-  ) t1
-) t0
\ No newline at end of file
+  SELECT t2.`col`, count(1) OVER () AS `analytic`
+  FROM t2
+) t3
\ No newline at end of file
diff --git a/ibis/backends/impala/tests/snapshots/test_sql/test_join_aliasing/out.sql b/ibis/backends/impala/tests/snapshots/test_sql/test_join_aliasing/out.sql
--- a/ibis/backends/impala/tests/snapshots/test_sql/test_join_aliasing/out.sql
+++ b/ibis/backends/impala/tests/snapshots/test_sql/test_join_aliasing/out.sql
@@ -1,36 +1,39 @@
 WITH t0 AS (
-  SELECT t2.`d`, t2.`c`
-  FROM t2
+  SELECT t7.*, t7.`a` + 20 AS `d`
+  FROM test_table t7
 ),
 t1 AS (
-  SELECT t0.`d`, CAST(t0.`d` / 15 AS bigint) AS `idx`, t0.`c`,
-         count(1) AS `row_count`
+  SELECT t0.`d`, t0.`c`
   FROM t0
-  GROUP BY 1, 2, 3
 ),
 t2 AS (
-  SELECT t5.*, t5.`a` + 20 AS `d`
-  FROM test_table t5
+  SELECT t1.`d`, CAST(t1.`d` / 15 AS bigint) AS `idx`, t1.`c`,
+         count(1) AS `row_count`
+  FROM t1
+  GROUP BY 1, 2, 3
+),
+t3 AS (
+  SELECT t2.`d`, sum(t2.`row_count`) AS `total`
+  FROM t2
+  GROUP BY 1
+),
+t4 AS (
+  SELECT t2.*, t3.`total`
+  FROM t2
+    INNER JOIN t3
+      ON t2.`d` = t3.`d`
+),
+t5 AS (
+  SELECT t4.*
+  FROM t4
+  WHERE t4.`row_count` < (t4.`total` / 2)
 )
-SELECT t3.*, t4.`total`
+SELECT t6.*, t5.`total`
 FROM (
-  SELECT t2.`d`, t2.`b`, count(1) AS `count`,
-         count(DISTINCT t2.`c`) AS `unique`
-  FROM t2
+  SELECT t0.`d`, t0.`b`, count(1) AS `count`,
+         count(DISTINCT t0.`c`) AS `unique`
+  FROM t0
   GROUP BY 1, 2
-) t3
-  INNER JOIN (
-    SELECT t5.*
-    FROM (
-      SELECT t1.*, t7.`total`
-      FROM t1
-        INNER JOIN (
-          SELECT t1.`d`, sum(t1.`row_count`) AS `total`
-          FROM t1
-          GROUP BY 1
-        ) t7
-          ON t1.`d` = t7.`d`
-    ) t5
-    WHERE t5.`row_count` < (t5.`total` / 2)
-  ) t4
-    ON t3.`d` = t4.`d`
\ No newline at end of file
+) t6
+  INNER JOIN t5
+    ON t6.`d` = t5.`d`
\ No newline at end of file
diff --git a/ibis/backends/impala/tests/snapshots/test_sql/test_join_key_name/out.sql b/ibis/backends/impala/tests/snapshots/test_sql/test_join_key_name/out.sql
--- a/ibis/backends/impala/tests/snapshots/test_sql/test_join_key_name/out.sql
+++ b/ibis/backends/impala/tests/snapshots/test_sql/test_join_key_name/out.sql
@@ -8,22 +8,24 @@ WITH t0 AS (
       ON t5.`c_nationkey` = t4.`n_nationkey`
     INNER JOIN tpch_orders t6
       ON t6.`o_custkey` = t5.`c_custkey`
-)
-SELECT t1.`year`, t1.`count` AS `pre_count`, t2.`count` AS `post_count`,
-       t2.`count` / CAST(t1.`count` AS double) AS `fraction`
-FROM (
+),
+t1 AS (
   SELECT extract(t0.`odate`, 'year') AS `year`, count(1) AS `count`
   FROM t0
+  WHERE t0.`o_totalprice` > (
+    SELECT avg(t4.`o_totalprice`) AS `mean`
+    FROM t0 t4
+    WHERE t4.`region` = t0.`region`
+  )
   GROUP BY 1
-) t1
-  INNER JOIN (
-    SELECT extract(t0.`odate`, 'year') AS `year`, count(1) AS `count`
-    FROM t0
-    WHERE t0.`o_totalprice` > (
-      SELECT avg(t4.`o_totalprice`) AS `mean`
-      FROM t0 t4
-      WHERE t4.`region` = t0.`region`
-    )
-    GROUP BY 1
-  ) t2
-    ON t1.`year` = t2.`year`
\ No newline at end of file
+),
+t2 AS (
+  SELECT extract(t0.`odate`, 'year') AS `year`, count(1) AS `count`
+  FROM t0
+  GROUP BY 1
+)
+SELECT t2.`year`, t2.`count` AS `pre_count`, t1.`count` AS `post_count`,
+       t1.`count` / CAST(t2.`count` AS double) AS `fraction`
+FROM t2
+  INNER JOIN t1
+    ON t2.`year` = t1.`year`
\ No newline at end of file
diff --git a/ibis/backends/impala/tests/snapshots/test_sql/test_limit_cte_extract/out.sql b/ibis/backends/impala/tests/snapshots/test_sql/test_limit_cte_extract/out.sql
--- a/ibis/backends/impala/tests/snapshots/test_sql/test_limit_cte_extract/out.sql
+++ b/ibis/backends/impala/tests/snapshots/test_sql/test_limit_cte_extract/out.sql
@@ -1,8 +1,11 @@
-WITH t0 AS (
+SELECT t0.*
+FROM (
   SELECT t2.*
   FROM functional_alltypes t2
   LIMIT 100
-)
-SELECT t0.*
-FROM t0
-  CROSS JOIN t0 t1
\ No newline at end of file
+) t0
+  CROSS JOIN (
+    SELECT t2.*
+    FROM functional_alltypes t2
+    LIMIT 100
+  ) t1
\ No newline at end of file
diff --git a/ibis/backends/impala/tests/snapshots/test_sql/test_multiple_filters2/out.sql b/ibis/backends/impala/tests/snapshots/test_sql/test_multiple_filters2/out.sql
--- a/ibis/backends/impala/tests/snapshots/test_sql/test_multiple_filters2/out.sql
+++ b/ibis/backends/impala/tests/snapshots/test_sql/test_multiple_filters2/out.sql
@@ -1,9 +1,10 @@
-SELECT t0.*
-FROM (
+WITH t0 AS (
   SELECT t1.*
   FROM t0 t1
   WHERE t1.`a` < 100
-) t0
+)
+SELECT t0.*
+FROM t0
 WHERE (t0.`a` = (
   SELECT max(t1.`a`) AS `Max(a)`
   FROM t0 t1
diff --git a/ibis/backends/impala/tests/snapshots/test_sql/test_nested_join_base/out.sql b/ibis/backends/impala/tests/snapshots/test_sql/test_nested_join_base/out.sql
--- a/ibis/backends/impala/tests/snapshots/test_sql/test_nested_join_base/out.sql
+++ b/ibis/backends/impala/tests/snapshots/test_sql/test_nested_join_base/out.sql
@@ -2,12 +2,13 @@ WITH t0 AS (
   SELECT t2.`uuid`, count(1) AS `count`
   FROM t t2
   GROUP BY 1
-)
-SELECT t0.*
-FROM (
+),
+t1 AS (
   SELECT t0.`uuid`, max(t0.`count`) AS `max_count`
   FROM t0
   GROUP BY 1
-) t1
+)
+SELECT t0.*
+FROM t1
   LEFT OUTER JOIN t0
     ON t1.`uuid` = t0.`uuid`
\ No newline at end of file
diff --git a/ibis/backends/impala/tests/snapshots/test_sql/test_nested_join_multiple_ctes/out.sql b/ibis/backends/impala/tests/snapshots/test_sql/test_nested_join_multiple_ctes/out.sql
--- a/ibis/backends/impala/tests/snapshots/test_sql/test_nested_join_multiple_ctes/out.sql
+++ b/ibis/backends/impala/tests/snapshots/test_sql/test_nested_join_multiple_ctes/out.sql
@@ -1,29 +1,30 @@
 WITH t0 AS (
-  SELECT t2.`userid`, t2.`movieid`, t2.`rating`,
-         CAST(t2.`timestamp` AS timestamp) AS `datetime`
-  FROM ratings t2
+  SELECT t2.*
+  FROM t2
+  WHERE (t2.`userid` = 118205) AND
+        (extract(t2.`datetime`, 'year') > 2001) AND
+        (t2.`userid` = 118205) AND
+        (extract(t2.`datetime`, 'year') < 2009)
 ),
 t1 AS (
-  SELECT t0.*, t3.`title`
-  FROM t0
-    INNER JOIN movies t3
-      ON t0.`movieid` = t3.`movieid`
+  SELECT t3.`userid`, t3.`movieid`, t3.`rating`,
+         CAST(t3.`timestamp` AS timestamp) AS `datetime`
+  FROM ratings t3
+),
+t2 AS (
+  SELECT t1.*, t4.`title`
+  FROM t1
+    INNER JOIN movies t4
+      ON t1.`movieid` = t4.`movieid`
 )
-SELECT t1.*
-FROM t1
-WHERE (t1.`userid` = 118205) AND
-      (extract(t1.`datetime`, 'year') > 2001) AND
-      (t1.`movieid` IN (
-  SELECT t2.`movieid`
+SELECT t2.*
+FROM t2
+WHERE (t2.`userid` = 118205) AND
+      (extract(t2.`datetime`, 'year') > 2001) AND
+      (t2.`movieid` IN (
+  SELECT t3.`movieid`
   FROM (
-    SELECT t3.`movieid`
-    FROM (
-      SELECT t1.*
-      FROM t1
-      WHERE (t1.`userid` = 118205) AND
-            (extract(t1.`datetime`, 'year') > 2001) AND
-            (t1.`userid` = 118205) AND
-            (extract(t1.`datetime`, 'year') < 2009)
-    ) t3
-  ) t2
+    SELECT t0.`movieid`
+    FROM t0
+  ) t3
 ))
\ No newline at end of file
diff --git a/ibis/backends/impala/tests/snapshots/test_sql/test_nested_joins_single_cte/out.sql b/ibis/backends/impala/tests/snapshots/test_sql/test_nested_joins_single_cte/out.sql
--- a/ibis/backends/impala/tests/snapshots/test_sql/test_nested_joins_single_cte/out.sql
+++ b/ibis/backends/impala/tests/snapshots/test_sql/test_nested_joins_single_cte/out.sql
@@ -1,23 +1,26 @@
 WITH t0 AS (
-  SELECT t3.`uuid`, count(1) AS `count`
-  FROM t t3
+  SELECT t4.`uuid`, count(1) AS `count`
+  FROM t t4
   GROUP BY 1
-)
-SELECT t1.*, t2.`last_visit`
-FROM (
+),
+t1 AS (
+  SELECT t0.`uuid`, max(t0.`count`) AS `max_count`
+  FROM t0
+  GROUP BY 1
+),
+t2 AS (
+  SELECT t4.`uuid`, max(t4.`ts`) AS `last_visit`
+  FROM t t4
+  GROUP BY 1
+),
+t3 AS (
   SELECT t0.*
-  FROM (
-    SELECT t0.`uuid`, max(t0.`count`) AS `max_count`
-    FROM t0
-    GROUP BY 1
-  ) t3
+  FROM t1
     LEFT OUTER JOIN t0
-      ON (t3.`uuid` = t0.`uuid`) AND
-         (t3.`max_count` = t0.`count`)
-) t1
-  LEFT OUTER JOIN (
-    SELECT t3.`uuid`, max(t3.`ts`) AS `last_visit`
-    FROM t t3
-    GROUP BY 1
-  ) t2
-    ON t1.`uuid` = t2.`uuid`
\ No newline at end of file
+      ON (t1.`uuid` = t0.`uuid`) AND
+         (t1.`max_count` = t0.`count`)
+)
+SELECT t3.*, t2.`last_visit`
+FROM t3
+  LEFT OUTER JOIN t2
+    ON t3.`uuid` = t2.`uuid`
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/bigquery/out.sql b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/bigquery/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/bigquery/out.sql
@@ -0,0 +1,36 @@
+WITH t0 AS (
+  SELECT
+    t5.*,
+    (
+      ROW_NUMBER() OVER (ORDER BY t5.`street`) - 1
+    ) AS `key`
+  FROM data AS t5
+), t1 AS (
+  SELECT
+    t0.`key`
+  FROM t0
+), t2 AS (
+  SELECT
+    t0.`street`,
+    t0.`key`
+  FROM t0
+  INNER JOIN t1
+    ON t0.`key` = t1.`key`
+), t3 AS (
+  SELECT
+    t2.`street`,
+    (
+      ROW_NUMBER() OVER (ORDER BY t2.`street`) - 1
+    ) AS `key`
+  FROM t2
+), t4 AS (
+  SELECT
+    t3.`key`
+  FROM t3
+)
+SELECT
+  t3.`street`,
+  t3.`key`
+FROM t3
+INNER JOIN t4
+  ON t3.`key` = t4.`key`
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/clickhouse/out.sql b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/clickhouse/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/clickhouse/out.sql
@@ -0,0 +1,74 @@
+SELECT
+  t5.street,
+  t5.key
+FROM (
+  SELECT
+    t4.street,
+    (
+      ROW_NUMBER() OVER (ORDER BY t4.street) - 1
+    ) AS key
+  FROM (
+    SELECT
+      t1.street,
+      t1.key
+    FROM (
+      SELECT
+        t0.*,
+        (
+          ROW_NUMBER() OVER (ORDER BY t0.street) - 1
+        ) AS key
+      FROM data AS t0
+    ) AS t1
+    INNER JOIN (
+      SELECT
+        t1.key
+      FROM (
+        SELECT
+          t0.*,
+          (
+            ROW_NUMBER() OVER (ORDER BY t0.street) - 1
+          ) AS key
+        FROM data AS t0
+      ) AS t1
+    ) AS t2
+      ON t1.key = t2.key
+  ) AS t4
+) AS t5
+INNER JOIN (
+  SELECT
+    t5.key
+  FROM (
+    SELECT
+      t4.street,
+      (
+        ROW_NUMBER() OVER (ORDER BY t4.street) - 1
+      ) AS key
+    FROM (
+      SELECT
+        t1.street,
+        t1.key
+      FROM (
+        SELECT
+          t0.*,
+          (
+            ROW_NUMBER() OVER (ORDER BY t0.street) - 1
+          ) AS key
+        FROM data AS t0
+      ) AS t1
+      INNER JOIN (
+        SELECT
+          t1.key
+        FROM (
+          SELECT
+            t0.*,
+            (
+              ROW_NUMBER() OVER (ORDER BY t0.street) - 1
+            ) AS key
+          FROM data AS t0
+        ) AS t1
+      ) AS t2
+        ON t1.key = t2.key
+    ) AS t4
+  ) AS t5
+) AS t6
+  ON t5.key = t6.key
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/duckdb/out.sql b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/duckdb/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/duckdb/out.sql
@@ -0,0 +1,32 @@
+WITH t0 AS (
+  SELECT
+    t5.street AS street,
+    ROW_NUMBER() OVER (ORDER BY t5.street) - 1 AS key
+  FROM data AS t5
+), t1 AS (
+  SELECT
+    t0.key AS key
+  FROM t0
+), t2 AS (
+  SELECT
+    t0.street AS street,
+    t0.key AS key
+  FROM t0
+  JOIN t1
+    ON t0.key = t1.key
+), t3 AS (
+  SELECT
+    t2.street AS street,
+    ROW_NUMBER() OVER (ORDER BY t2.street) - 1 AS key
+  FROM t2
+), t4 AS (
+  SELECT
+    t3.key AS key
+  FROM t3
+)
+SELECT
+  t3.street,
+  t3.key
+FROM t3
+JOIN t4
+  ON t3.key = t4.key
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/impala/out.sql b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/impala/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/impala/out.sql
@@ -0,0 +1,36 @@
+WITH t0 AS (
+  SELECT
+    t5.*,
+    (
+      ROW_NUMBER() OVER (ORDER BY t5.`street`) - 1
+    ) AS `key`
+  FROM `data` AS t5
+), t1 AS (
+  SELECT
+    t0.`key`
+  FROM t0
+), t2 AS (
+  SELECT
+    t0.`street`,
+    t0.`key`
+  FROM t0
+  INNER JOIN t1
+    ON t0.`key` = t1.`key`
+), t3 AS (
+  SELECT
+    t2.`street`,
+    (
+      ROW_NUMBER() OVER (ORDER BY t2.`street`) - 1
+    ) AS `key`
+  FROM t2
+), t4 AS (
+  SELECT
+    t3.`key`
+  FROM t3
+)
+SELECT
+  t3.`street`,
+  t3.`key`
+FROM t3
+INNER JOIN t4
+  ON t3.`key` = t4.`key`
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/mssql/out.sql b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/mssql/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/mssql/out.sql
@@ -0,0 +1,32 @@
+WITH t0 AS (
+  SELECT
+    t5.street AS street,
+    ROW_NUMBER() OVER (ORDER BY t5.street) - 1 AS "key"
+  FROM data AS t5
+), t1 AS (
+  SELECT
+    t0."key" AS "key"
+  FROM t0
+), t2 AS (
+  SELECT
+    t0.street AS street,
+    t0."key" AS "key"
+  FROM t0
+  JOIN t1
+    ON t0."key" = t1."key"
+), t3 AS (
+  SELECT
+    t2.street AS street,
+    ROW_NUMBER() OVER (ORDER BY t2.street) - 1 AS "key"
+  FROM t2
+), t4 AS (
+  SELECT
+    t3."key" AS "key"
+  FROM t3
+)
+SELECT
+  t3.street,
+  t3."key"
+FROM t3
+JOIN t4
+  ON t3."key" = t4."key"
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/mysql/out.sql b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/mysql/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/mysql/out.sql
@@ -0,0 +1,32 @@
+WITH t0 AS (
+  SELECT
+    t5.street AS street,
+    ROW_NUMBER() OVER (ORDER BY t5.street) - 1 AS `key`
+  FROM data AS t5
+), t1 AS (
+  SELECT
+    t0.`key` AS `key`
+  FROM t0
+), t2 AS (
+  SELECT
+    t0.street AS street,
+    t0.`key` AS `key`
+  FROM t0
+  INNER JOIN t1
+    ON t0.`key` = t1.`key`
+), t3 AS (
+  SELECT
+    t2.street AS street,
+    ROW_NUMBER() OVER (ORDER BY t2.street) - 1 AS `key`
+  FROM t2
+), t4 AS (
+  SELECT
+    t3.`key` AS `key`
+  FROM t3
+)
+SELECT
+  t3.street,
+  t3.`key`
+FROM t3
+INNER JOIN t4
+  ON t3.`key` = t4.`key`
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/postgres/out.sql b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/postgres/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/postgres/out.sql
@@ -0,0 +1,32 @@
+WITH t0 AS (
+  SELECT
+    t5.street AS street,
+    ROW_NUMBER() OVER (ORDER BY t5.street) - 1 AS key
+  FROM data AS t5
+), t1 AS (
+  SELECT
+    t0.key AS key
+  FROM t0
+), t2 AS (
+  SELECT
+    t0.street AS street,
+    t0.key AS key
+  FROM t0
+  JOIN t1
+    ON t0.key = t1.key
+), t3 AS (
+  SELECT
+    t2.street AS street,
+    ROW_NUMBER() OVER (ORDER BY t2.street) - 1 AS key
+  FROM t2
+), t4 AS (
+  SELECT
+    t3.key AS key
+  FROM t3
+)
+SELECT
+  t3.street,
+  t3.key
+FROM t3
+JOIN t4
+  ON t3.key = t4.key
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/sqlite/out.sql b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/sqlite/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/sqlite/out.sql
@@ -0,0 +1,32 @@
+WITH t0 AS (
+  SELECT
+    t5.street AS street,
+    ROW_NUMBER() OVER (ORDER BY t5.street) - 1 AS "key"
+  FROM data AS t5
+), t1 AS (
+  SELECT
+    t0."key" AS "key"
+  FROM t0
+), t2 AS (
+  SELECT
+    t0.street AS street,
+    t0."key" AS "key"
+  FROM t0
+  JOIN t1
+    ON t0."key" = t1."key"
+), t3 AS (
+  SELECT
+    t2.street AS street,
+    ROW_NUMBER() OVER (ORDER BY t2.street) - 1 AS "key"
+  FROM t2
+), t4 AS (
+  SELECT
+    t3."key" AS "key"
+  FROM t3
+)
+SELECT
+  t3.street,
+  t3."key"
+FROM t3
+JOIN t4
+  ON t3."key" = t4."key"
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/trino/out.sql b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/trino/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_generic/test_many_subqueries/trino/out.sql
@@ -0,0 +1,32 @@
+WITH t0 AS (
+  SELECT
+    t5.street AS street,
+    ROW_NUMBER() OVER (ORDER BY t5.street) - 1 AS key
+  FROM data AS t5
+), t1 AS (
+  SELECT
+    t0.key AS key
+  FROM t0
+), t2 AS (
+  SELECT
+    t0.street AS street,
+    t0.key AS key
+  FROM t0
+  JOIN t1
+    ON t0.key = t1.key
+), t3 AS (
+  SELECT
+    t2.street AS street,
+    ROW_NUMBER() OVER (ORDER BY t2.street) - 1 AS key
+  FROM t2
+), t4 AS (
+  SELECT
+    t3.key AS key
+  FROM t3
+)
+SELECT
+  t3.street,
+  t3.key
+FROM t3
+JOIN t4
+  ON t3.key = t4.key
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/bigquery/out.sql b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/bigquery/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/bigquery/out.sql
@@ -0,0 +1,22 @@
+WITH t0 AS (
+  SELECT
+    t4.*
+  FROM leaf AS t4
+  WHERE
+    TRUE
+), t1 AS (
+  SELECT
+    t0.`key`
+  FROM t0
+), t2 AS (
+  SELECT
+    t0.`key`
+  FROM t0
+  INNER JOIN t1
+    ON t0.`key` = t1.`key`
+)
+SELECT
+  t2.`key`
+FROM t2
+INNER JOIN t2 AS t3
+  ON t2.`key` = t3.`key`
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/clickhouse/out.sql b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/clickhouse/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/clickhouse/out.sql
@@ -0,0 +1,49 @@
+SELECT
+  t4.key
+FROM (
+  SELECT
+    t1.key
+  FROM (
+    SELECT
+      *
+    FROM leaf AS t0
+    WHERE
+      1
+  ) AS t1
+  INNER JOIN (
+    SELECT
+      t1.key
+    FROM (
+      SELECT
+        *
+      FROM leaf AS t0
+      WHERE
+        1
+    ) AS t1
+  ) AS t2
+    ON t1.key = t2.key
+) AS t4
+INNER JOIN (
+  SELECT
+    t1.key
+  FROM (
+    SELECT
+      *
+    FROM leaf AS t0
+    WHERE
+      1
+  ) AS t1
+  INNER JOIN (
+    SELECT
+      t1.key
+    FROM (
+      SELECT
+        *
+      FROM leaf AS t0
+      WHERE
+        1
+    ) AS t1
+  ) AS t2
+    ON t1.key = t2.key
+) AS t5
+  ON t4.key = t5.key
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/duckdb/out.sql b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/duckdb/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/duckdb/out.sql
@@ -0,0 +1,22 @@
+WITH t0 AS (
+  SELECT
+    t4.key AS key
+  FROM leaf AS t4
+  WHERE
+    CAST(TRUE AS BOOLEAN)
+), t1 AS (
+  SELECT
+    t0.key AS key
+  FROM t0
+), t2 AS (
+  SELECT
+    t0.key AS key
+  FROM t0
+  JOIN t1
+    ON t0.key = t1.key
+)
+SELECT
+  t2.key
+FROM t2
+JOIN t2 AS t3
+  ON t2.key = t3.key
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/impala/out.sql b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/impala/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/impala/out.sql
@@ -0,0 +1,22 @@
+WITH t0 AS (
+  SELECT
+    t4.*
+  FROM leaf AS t4
+  WHERE
+    TRUE
+), t1 AS (
+  SELECT
+    t0.`key`
+  FROM t0
+), t2 AS (
+  SELECT
+    t0.`key`
+  FROM t0
+  INNER JOIN t1
+    ON t0.`key` = t1.`key`
+)
+SELECT
+  t2.`key`
+FROM t2
+INNER JOIN t2 AS t3
+  ON t2.`key` = t3.`key`
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/mssql/out.sql b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/mssql/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/mssql/out.sql
@@ -0,0 +1,22 @@
+WITH t0 AS (
+  SELECT
+    t4."key" AS "key"
+  FROM leaf AS t4
+  WHERE
+    1 = 1
+), t1 AS (
+  SELECT
+    t0."key" AS "key"
+  FROM t0
+), t2 AS (
+  SELECT
+    t0."key" AS "key"
+  FROM t0
+  JOIN t1
+    ON t0."key" = t1."key"
+)
+SELECT
+  t2."key"
+FROM t2
+JOIN t2 AS t3
+  ON t2."key" = t3."key"
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/mysql/out.sql b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/mysql/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/mysql/out.sql
@@ -0,0 +1,22 @@
+WITH t0 AS (
+  SELECT
+    t4.`key` AS `key`
+  FROM leaf AS t4
+  WHERE
+    TRUE = 1
+), t1 AS (
+  SELECT
+    t0.`key` AS `key`
+  FROM t0
+), t2 AS (
+  SELECT
+    t0.`key` AS `key`
+  FROM t0
+  INNER JOIN t1
+    ON t0.`key` = t1.`key`
+)
+SELECT
+  t2.`key`
+FROM t2
+INNER JOIN t2 AS t3
+  ON t2.`key` = t3.`key`
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/postgres/out.sql b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/postgres/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/postgres/out.sql
@@ -0,0 +1,22 @@
+WITH t0 AS (
+  SELECT
+    t4.key AS key
+  FROM leaf AS t4
+  WHERE
+    TRUE
+), t1 AS (
+  SELECT
+    t0.key AS key
+  FROM t0
+), t2 AS (
+  SELECT
+    t0.key AS key
+  FROM t0
+  JOIN t1
+    ON t0.key = t1.key
+)
+SELECT
+  t2.key
+FROM t2
+JOIN t2 AS t3
+  ON t2.key = t3.key
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/snowflake/out.sql b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/snowflake/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/snowflake/out.sql
@@ -0,0 +1,22 @@
+WITH t0 AS (
+  SELECT
+    t4.key AS key
+  FROM leaf AS t4
+  WHERE
+    TRUE
+), t1 AS (
+  SELECT
+    t0.key AS key
+  FROM t0
+), t2 AS (
+  SELECT
+    t0.key AS key
+  FROM t0
+  JOIN t1
+    ON t0.key = t1.key
+)
+SELECT
+  t2.key
+FROM t2
+JOIN t2 AS t3
+  ON t2.key = t3.key
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/sqlite/out.sql b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/sqlite/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/sqlite/out.sql
@@ -0,0 +1,22 @@
+WITH t0 AS (
+  SELECT
+    t4."key" AS "key"
+  FROM leaf AS t4
+  WHERE
+    1 = 1
+), t1 AS (
+  SELECT
+    t0."key" AS "key"
+  FROM t0
+), t2 AS (
+  SELECT
+    t0."key" AS "key"
+  FROM t0
+  JOIN t1
+    ON t0."key" = t1."key"
+)
+SELECT
+  t2."key"
+FROM t2
+JOIN t2 AS t3
+  ON t2."key" = t3."key"
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/trino/out.sql b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/trino/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_sql/test_cte_refs_in_topo_order/trino/out.sql
@@ -0,0 +1,22 @@
+WITH t0 AS (
+  SELECT
+    t4.key AS key
+  FROM leaf AS t4
+  WHERE
+    TRUE
+), t1 AS (
+  SELECT
+    t0.key AS key
+  FROM t0
+), t2 AS (
+  SELECT
+    t0.key AS key
+  FROM t0
+  JOIN t1
+    ON t0.key = t1.key
+)
+SELECT
+  t2.key
+FROM t2
+JOIN t2 AS t3
+  ON t2.key = t3.key
\ No newline at end of file
diff --git a/ibis/backends/tests/snapshots/test_sql/test_group_by_has_index/mssql/out.sql b/ibis/backends/tests/snapshots/test_sql/test_group_by_has_index/mssql/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/backends/tests/snapshots/test_sql/test_group_by_has_index/mssql/out.sql
@@ -0,0 +1,38 @@
+SELECT
+  CASE t0.continent
+    WHEN N'NA'
+    THEN N'North America'
+    WHEN N'SA'
+    THEN N'South America'
+    WHEN N'EU'
+    THEN N'Europe'
+    WHEN N'AF'
+    THEN N'Africa'
+    WHEN N'AS'
+    THEN N'Asia'
+    WHEN N'OC'
+    THEN N'Oceania'
+    WHEN N'AN'
+    THEN N'Antarctica'
+    ELSE N'Unknown continent'
+  END AS cont,
+  SUM(t0.population) AS total_pop
+FROM countries AS t0
+GROUP BY
+  CASE t0.continent
+    WHEN N'NA'
+    THEN N'North America'
+    WHEN N'SA'
+    THEN N'South America'
+    WHEN N'EU'
+    THEN N'Europe'
+    WHEN N'AF'
+    THEN N'Africa'
+    WHEN N'AS'
+    THEN N'Asia'
+    WHEN N'OC'
+    THEN N'Oceania'
+    WHEN N'AN'
+    THEN N'Antarctica'
+    ELSE N'Unknown continent'
+  END
\ No newline at end of file
diff --git a/ibis/backends/tests/test_generic.py b/ibis/backends/tests/test_generic.py
--- a/ibis/backends/tests/test_generic.py
+++ b/ibis/backends/tests/test_generic.py
@@ -31,6 +31,7 @@
 except ImportError:
     ClickhouseDriverOperationalError = None
 
+
 NULL_BACKEND_TYPES = {
     'bigquery': "NULL",
     'clickhouse': 'Nullable(Nothing)',
@@ -910,3 +911,22 @@ def test_memtable_bool_column(backend, con, monkeypatch):
 
     t = ibis.memtable({"a": [True, False, True]})
     backend.assert_series_equal(t.a.execute(), pd.Series([True, False, True], name="a"))
+
+
+@pytest.mark.notimpl(
+    ["dask", "datafusion", "pandas", "polars"],
+    raises=NotImplementedError,
+    reason="not a SQL backend",
+)
+@pytest.mark.notimpl(["pyspark"], raises=com.OperationNotDefinedError)
+def test_many_subqueries(con, snapshot):
+    def query(t, group_cols):
+        t2 = t.mutate(key=ibis.row_number().over(ibis.window(order_by=group_cols)))
+        return t2.inner_join(t2[["key"]], "key")
+
+    t = ibis.table(dict(street="str"), name="data")
+
+    t2 = query(t, group_cols=["street"])
+    t3 = query(t2, group_cols=["street"])
+
+    snapshot.assert_match(str(ibis.to_sql(t3, dialect=con.name)), "out.sql")
diff --git a/ibis/backends/tests/test_sql.py b/ibis/backends/tests/test_sql.py
--- a/ibis/backends/tests/test_sql.py
+++ b/ibis/backends/tests/test_sql.py
@@ -88,7 +88,9 @@ def test_literal(backend, expr):
 @pytest.mark.never(
     ["pandas", "dask", "datafusion", "polars", "pyspark"], reason="not SQL"
 )
-@pytest.mark.notyet(["mssql"], reason="sqlglot doesn't support an mssql dialect")
+@pytest.mark.xfail_version(
+    mssql=["sqlalchemy>=2"], reason="sqlalchemy 2 prefixes literals with `N`"
+)
 def test_group_by_has_index(backend, snapshot):
     countries = ibis.table(
         dict(continent="string", population="int64"), name="countries"
@@ -109,3 +111,18 @@ def test_group_by_has_index(backend, snapshot):
     ).agg(total_pop=_.population.sum())
     sql = str(ibis.to_sql(expr, dialect=backend.name()))
     snapshot.assert_match(sql, "out.sql")
+
+
+@pytest.mark.never(
+    ["pandas", "dask", "datafusion", "polars", "pyspark"], reason="not SQL"
+)
+def test_cte_refs_in_topo_order(backend, snapshot):
+    mr0 = ibis.table(schema=ibis.schema(dict(key="int")), name='leaf')
+
+    mr1 = mr0.filter(ibis.literal(True))
+
+    mr2 = mr1.join(mr1[['key']], ["key"])
+    mr3 = mr2.join(mr2, ['key'])
+
+    sql = str(ibis.to_sql(mr3, dialect=backend.name()))
+    snapshot.assert_match(sql, "out.sql")
diff --git a/ibis/tests/sql/snapshots/test_compiler/test_agg_and_non_agg_filter/out.sql b/ibis/tests/sql/snapshots/test_compiler/test_agg_and_non_agg_filter/out.sql
--- a/ibis/tests/sql/snapshots/test_compiler/test_agg_and_non_agg_filter/out.sql
+++ b/ibis/tests/sql/snapshots/test_compiler/test_agg_and_non_agg_filter/out.sql
@@ -1,9 +1,10 @@
-SELECT t0.*
-FROM (
+WITH t0 AS (
   SELECT t1.*
   FROM my_table t1
   WHERE t1.`a` < 100
-) t0
+)
+SELECT t0.*
+FROM t0
 WHERE (t0.`a` = (
   SELECT max(t1.`a`) AS `Max(a)`
   FROM my_table t1
diff --git a/ibis/tests/sql/snapshots/test_compiler/test_subquery_where_location/out.sql b/ibis/tests/sql/snapshots/test_compiler/test_subquery_where_location/out.sql
--- a/ibis/tests/sql/snapshots/test_compiler/test_subquery_where_location/out.sql
+++ b/ibis/tests/sql/snapshots/test_compiler/test_subquery_where_location/out.sql
@@ -1,10 +1,11 @@
-SELECT count(t0.`foo`) AS `Count(foo)`
+WITH t0 AS (
+  SELECT t2.`float_col`, t2.`timestamp_col`, t2.`int_col`, t2.`string_col`
+  FROM alltypes t2
+  WHERE t2.`timestamp_col` < '2014-01-01T00:00:00'
+)
+SELECT count(t1.`foo`) AS `Count(foo)`
 FROM (
-  SELECT t1.`string_col`, sum(t1.`float_col`) AS `foo`
-  FROM (
-    SELECT t2.`float_col`, t2.`timestamp_col`, t2.`int_col`, t2.`string_col`
-    FROM alltypes t2
-    WHERE t2.`timestamp_col` < '2014-01-01T00:00:00'
-  ) t1
+  SELECT t0.`string_col`, sum(t0.`float_col`) AS `foo`
+  FROM t0
   GROUP BY 1
-) t0
\ No newline at end of file
+) t1
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_compiler/test_table_drop_with_filter/out.sql b/ibis/tests/sql/snapshots/test_compiler/test_table_drop_with_filter/out.sql
--- a/ibis/tests/sql/snapshots/test_compiler/test_table_drop_with_filter/out.sql
+++ b/ibis/tests/sql/snapshots/test_compiler/test_table_drop_with_filter/out.sql
@@ -1,18 +1,21 @@
-SELECT t0.*
+WITH t0 AS (
+  SELECT t4.`a`, t4.`b`, t4.`c` AS `C`
+  FROM t t4
+),
+t1 AS (
+  SELECT t0.*
+  FROM t0
+  WHERE t0.`C` = '2018-01-01T00:00:00'
+),
+t2 AS (
+  SELECT t1.`a`, t1.`b`, '2018-01-01T00:00:00' AS `the_date`
+  FROM t1
+)
+SELECT t3.*
 FROM (
-  SELECT t1.`a`
-  FROM (
-    SELECT t3.`a`, t3.`b`, '2018-01-01T00:00:00' AS `the_date`
-    FROM (
-      SELECT t4.*
-      FROM (
-        SELECT t5.`a`, t5.`b`, t5.`c` AS `C`
-        FROM t t5
-      ) t4
-      WHERE t4.`C` = '2018-01-01T00:00:00'
-    ) t3
-  ) t1
-    INNER JOIN s t2
-      ON t1.`b` = t2.`b`
-) t0
-WHERE t0.`a` < 1.0
\ No newline at end of file
+  SELECT t2.`a`
+  FROM t2
+    INNER JOIN s t5
+      ON t2.`b` = t5.`b`
+) t3
+WHERE t3.`a` < 1.0
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_bug_duplicated_where/out.sql b/ibis/tests/sql/snapshots/test_select_sql/test_bug_duplicated_where/out.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_bug_duplicated_where/out.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_bug_duplicated_where/out.sql
@@ -1,15 +1,17 @@
-SELECT t0.*
+WITH t0 AS (
+  SELECT t3.`arrdelay`, t3.`dest`
+  FROM airlines t3
+),
+t1 AS (
+  SELECT t0.*, avg(t0.`arrdelay`) OVER (PARTITION BY t0.`dest`) AS `dest_avg`,
+         t0.`arrdelay` - avg(t0.`arrdelay`) OVER (PARTITION BY t0.`dest`) AS `dev`
+  FROM t0
+)
+SELECT t2.*
 FROM (
   SELECT t1.*
-  FROM (
-    SELECT t2.*, avg(t2.`arrdelay`) OVER (PARTITION BY t2.`dest`) AS `dest_avg`,
-           t2.`arrdelay` - avg(t2.`arrdelay`) OVER (PARTITION BY t2.`dest`) AS `dev`
-    FROM (
-      SELECT t3.`arrdelay`, t3.`dest`
-      FROM airlines t3
-    ) t2
-  ) t1
+  FROM t1
   WHERE t1.`dev` IS NOT NULL
-) t0
-ORDER BY t0.`dev` DESC
+) t2
+ORDER BY t2.`dev` DESC
 LIMIT 10
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_bug_project_multiple_times/out.sql b/ibis/tests/sql/snapshots/test_select_sql/test_bug_project_multiple_times/out.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_bug_project_multiple_times/out.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_bug_project_multiple_times/out.sql
@@ -1,22 +1,23 @@
 WITH t0 AS (
-  SELECT t2.*, t3.`n_name`, t4.`r_name`
-  FROM tpch_customer t2
-    INNER JOIN tpch_nation t3
-      ON t2.`c_nationkey` = t3.`n_nationkey`
-    INNER JOIN tpch_region t4
-      ON t3.`n_regionkey` = t4.`r_regionkey`
+  SELECT t3.*, t4.`n_name`, t5.`r_name`
+  FROM tpch_customer t3
+    INNER JOIN tpch_nation t4
+      ON t3.`c_nationkey` = t4.`n_nationkey`
+    INNER JOIN tpch_region t5
+      ON t4.`n_regionkey` = t5.`r_regionkey`
+),
+t1 AS (
+  SELECT t0.`n_name`,
+         sum(CAST(t0.`c_acctbal` AS double)) AS `Sum(Cast(c_acctbal, float64))`
+  FROM t0
+  GROUP BY 1
 )
 SELECT t0.`c_name`, t0.`r_name`, t0.`n_name`
 FROM t0
   LEFT SEMI JOIN (
-    SELECT t2.*
-    FROM (
-      SELECT t0.`n_name`,
-             sum(CAST(t0.`c_acctbal` AS double)) AS `Sum(Cast(c_acctbal, float64))`
-      FROM t0
-      GROUP BY 1
-    ) t2
-    ORDER BY t2.`Sum(Cast(c_acctbal, float64))` DESC
+    SELECT t1.*
+    FROM t1
+    ORDER BY t1.`Sum(Cast(c_acctbal, float64))` DESC
     LIMIT 10
-  ) t1
-    ON t0.`n_name` = t1.`n_name`
\ No newline at end of file
+  ) t2
+    ON t0.`n_name` = t2.`n_name`
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_double_nested_subquery_no_aliases/out.sql b/ibis/tests/sql/snapshots/test_select_sql/test_double_nested_subquery_no_aliases/out.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_double_nested_subquery_no_aliases/out.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_double_nested_subquery_no_aliases/out.sql
@@ -1,11 +1,12 @@
-SELECT t0.`key1`, sum(t0.`total`) AS `total`
+WITH t0 AS (
+  SELECT t2.`key1`, t2.`key2`, t2.`key3`, sum(t2.`value`) AS `total`
+  FROM foo_table t2
+  GROUP BY 1, 2, 3
+)
+SELECT t1.`key1`, sum(t1.`total`) AS `total`
 FROM (
-  SELECT t1.`key1`, t1.`key2`, sum(t1.`total`) AS `total`
-  FROM (
-    SELECT t2.`key1`, t2.`key2`, t2.`key3`, sum(t2.`value`) AS `total`
-    FROM foo_table t2
-    GROUP BY 1, 2, 3
-  ) t1
+  SELECT t0.`key1`, t0.`key2`, sum(t0.`total`) AS `total`
+  FROM t0
   GROUP BY 1, 2
-) t0
+) t1
 GROUP BY 1
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_filter_inside_exists/out.sql b/ibis/tests/sql/snapshots/test_select_sql/test_filter_inside_exists/out.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_filter_inside_exists/out.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_filter_inside_exists/out.sql
@@ -1,11 +1,12 @@
-SELECT t0.*
-FROM events t0
+WITH t0 AS (
+  SELECT t2.*
+  FROM purchases t2
+  WHERE t2.`ts` > '2015-08-15'
+)
+SELECT t1.*
+FROM events t1
 WHERE EXISTS (
   SELECT 1
-  FROM (
-    SELECT t2.*
-    FROM purchases t2
-    WHERE t2.`ts` > '2015-08-15'
-  ) t1
-  WHERE t0.`user_id` = t1.`user_id`
+  FROM t0
+  WHERE t1.`user_id` = t0.`user_id`
 )
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_filter_predicates/out.sql b/ibis/tests/sql/snapshots/test_select_sql/test_filter_predicates/out.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_filter_predicates/out.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_filter_predicates/out.sql
@@ -1,11 +1,12 @@
+WITH t0 AS (
+  SELECT t2.*
+  FROM t t2
+  WHERE (lower(t2.`color`) LIKE '%de%') AND
+        (locate('de', lower(t2.`color`)) - 1 >= 0)
+)
 SELECT *
 FROM (
   SELECT *
-  FROM (
-    SELECT t2.*
-    FROM t t2
-    WHERE (lower(t2.`color`) LIKE '%de%') AND
-          (locate('de', lower(t2.`color`)) - 1 >= 0)
-  ) t1
-  WHERE regexp_like(lower(t1.`color`), '.*ge.*')
-) t0
\ No newline at end of file
+  FROM t0
+  WHERE regexp_like(lower(t0.`color`), '.*ge.*')
+) t1
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_filter_self_join_analysis_bug/result.sql b/ibis/tests/sql/snapshots/test_select_sql/test_filter_self_join_analysis_bug/result.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_filter_self_join_analysis_bug/result.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_filter_self_join_analysis_bug/result.sql
@@ -2,16 +2,18 @@ WITH t0 AS (
   SELECT t3.`region`, t3.`kind`, sum(t3.`amount`) AS `total`
   FROM purchases t3
   GROUP BY 1, 2
-)
-SELECT t1.`region`, t1.`total` - t2.`total` AS `diff`
-FROM (
+),
+t1 AS (
+  SELECT t0.*
+  FROM t0
+  WHERE t0.`kind` = 'bar'
+),
+t2 AS (
   SELECT t0.*
   FROM t0
   WHERE t0.`kind` = 'foo'
-) t1
-  INNER JOIN (
-    SELECT t0.*
-    FROM t0
-    WHERE t0.`kind` = 'bar'
-  ) t2
-    ON t1.`region` = t2.`region`
\ No newline at end of file
+)
+SELECT t2.`region`, t2.`total` - t1.`total` AS `diff`
+FROM t2
+  INNER JOIN t1
+    ON t2.`region` = t1.`region`
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_join_between_joins/out.sql b/ibis/tests/sql/snapshots/test_select_sql/test_join_between_joins/out.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_join_between_joins/out.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_join_between_joins/out.sql
@@ -1,14 +1,16 @@
-SELECT t0.*, t1.`value3`, t1.`value4`
-FROM (
+WITH t0 AS (
+  SELECT t2.*, t3.`value4`
+  FROM third t2
+    INNER JOIN fourth t3
+      ON t2.`key3` = t3.`key3`
+),
+t1 AS (
   SELECT t2.*, t3.`value2`
   FROM `first` t2
     INNER JOIN second t3
       ON t2.`key1` = t3.`key1`
-) t0
-  INNER JOIN (
-    SELECT t2.*, t3.`value4`
-    FROM third t2
-      INNER JOIN fourth t3
-        ON t2.`key3` = t3.`key3`
-  ) t1
-    ON t0.`key2` = t1.`key2`
\ No newline at end of file
+)
+SELECT t1.*, t0.`value3`, t0.`value4`
+FROM t1
+  INNER JOIN t0
+    ON t1.`key2` = t0.`key2`
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_join_filtered_tables_no_pushdown/out.sql b/ibis/tests/sql/snapshots/test_select_sql/test_join_filtered_tables_no_pushdown/out.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_join_filtered_tables_no_pushdown/out.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_join_filtered_tables_no_pushdown/out.sql
@@ -1,18 +1,20 @@
-SELECT t0.`value_a`, t1.`value_b`
-FROM (
+WITH t0 AS (
+  SELECT t2.*
+  FROM b t2
+  WHERE (t2.`year` = 2016) AND
+        (t2.`month` = 2) AND
+        (t2.`day` = 29)
+),
+t1 AS (
   SELECT t2.*
   FROM a t2
   WHERE (t2.`year` = 2016) AND
         (t2.`month` = 2) AND
         (t2.`day` = 29)
-) t0
-  LEFT OUTER JOIN (
-    SELECT t2.*
-    FROM b t2
-    WHERE (t2.`year` = 2016) AND
-          (t2.`month` = 2) AND
-          (t2.`day` = 29)
-  ) t1
-    ON (t0.`year` = t1.`year`) AND
-       (t0.`month` = t1.`month`) AND
-       (t0.`day` = t1.`day`)
\ No newline at end of file
+)
+SELECT t1.`value_a`, t0.`value_b`
+FROM t1
+  LEFT OUTER JOIN t0
+    ON (t1.`year` = t0.`year`) AND
+       (t1.`month` = t0.`month`) AND
+       (t1.`day` = t0.`day`)
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_join_projection_subquery_bug/out.sql b/ibis/tests/sql/snapshots/test_select_sql/test_join_projection_subquery_bug/out.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_join_projection_subquery_bug/out.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_join_projection_subquery_bug/out.sql
@@ -1,9 +1,10 @@
-SELECT t1.*, t0.*
-FROM (
+WITH t0 AS (
   SELECT t2.`n_nationkey`, t2.`n_name` AS `nation`, t3.`r_name` AS `region`
   FROM tpch_nation t2
     INNER JOIN tpch_region t3
       ON t2.`n_regionkey` = t3.`r_regionkey`
-) t0
+)
+SELECT t1.*, t0.*
+FROM t0
   INNER JOIN tpch_customer t1
     ON t0.`n_nationkey` = t1.`c_nationkey`
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_limit_cte_extract/out.sql b/ibis/tests/sql/snapshots/test_select_sql/test_limit_cte_extract/out.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_limit_cte_extract/out.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_limit_cte_extract/out.sql
@@ -1,8 +1,11 @@
-WITH t0 AS (
+SELECT t0.*
+FROM (
   SELECT t2.*
   FROM functional_alltypes t2
   LIMIT 100
-)
-SELECT t0.*
-FROM t0
-  INNER JOIN t0 t1
\ No newline at end of file
+) t0
+  INNER JOIN (
+    SELECT t2.*
+    FROM functional_alltypes t2
+    LIMIT 100
+  ) t1
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_loj_subquery_filter_handling/out.sql b/ibis/tests/sql/snapshots/test_select_sql/test_loj_subquery_filter_handling/out.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_loj_subquery_filter_handling/out.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_loj_subquery_filter_handling/out.sql
@@ -1,14 +1,16 @@
-SELECT t0.`id` AS `left_id`, t0.`desc` AS `left_desc`, t1.`id` AS `right_id`,
-       t1.`desc` AS `right_desc`
-FROM (
+WITH t0 AS (
+  SELECT t2.*
+  FROM bar t2
+  WHERE t2.`id` < 3
+),
+t1 AS (
   SELECT t2.*
   FROM foo t2
   WHERE t2.`id` < 2
-) t0
-  LEFT OUTER JOIN (
-    SELECT t2.*
-    FROM bar t2
-    WHERE t2.`id` < 3
-  ) t1
-    ON (t0.`id` = t1.`id`) AND
-       (t0.`desc` = t1.`desc`)
\ No newline at end of file
+)
+SELECT t1.`id` AS `left_id`, t1.`desc` AS `left_desc`, t0.`id` AS `right_id`,
+       t0.`desc` AS `right_desc`
+FROM t1
+  LEFT OUTER JOIN t0
+    ON (t1.`id` = t0.`id`) AND
+       (t1.`desc` = t0.`desc`)
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_sort_then_group_by_propagates_keys/result1.sql b/ibis/tests/sql/snapshots/test_select_sql/test_sort_then_group_by_propagates_keys/result1.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_sort_then_group_by_propagates_keys/result1.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_sort_then_group_by_propagates_keys/result1.sql
@@ -1,10 +1,11 @@
-SELECT t0.`b`, count(1) AS `b_count`
+WITH t0 AS (
+  SELECT t2.*
+  FROM t t2
+  ORDER BY t2.`a` ASC
+)
+SELECT t1.`b`, count(1) AS `b_count`
 FROM (
-  SELECT t1.`b`
-  FROM (
-    SELECT t2.*
-    FROM t t2
-    ORDER BY t2.`a` ASC
-  ) t1
-) t0
+  SELECT t0.`b`
+  FROM t0
+) t1
 GROUP BY 1
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_sort_then_group_by_propagates_keys/result2.sql b/ibis/tests/sql/snapshots/test_select_sql/test_sort_then_group_by_propagates_keys/result2.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_sort_then_group_by_propagates_keys/result2.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_sort_then_group_by_propagates_keys/result2.sql
@@ -1,10 +1,11 @@
-SELECT t0.`b`, count(1) AS `b_count`
+WITH t0 AS (
+  SELECT t2.*
+  FROM t t2
+  ORDER BY t2.`b` ASC
+)
+SELECT t1.`b`, count(1) AS `b_count`
 FROM (
-  SELECT t1.`b`
-  FROM (
-    SELECT t2.*
-    FROM t t2
-    ORDER BY t2.`b` ASC
-  ) t1
-) t0
+  SELECT t0.`b`
+  FROM t0
+) t1
 GROUP BY 1
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_topk_analysis_bug/out.sql b/ibis/tests/sql/snapshots/test_select_sql/test_topk_analysis_bug/out.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_topk_analysis_bug/out.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_topk_analysis_bug/out.sql
@@ -1,19 +1,21 @@
+WITH t0 AS (
+  SELECT t3.`dest`, avg(t3.`arrdelay`) AS `Mean(arrdelay)`
+  FROM airlines t3
+  WHERE t3.`dest` IN ('ORD', 'JFK', 'SFO')
+  GROUP BY 1
+),
+t1 AS (
+  SELECT t3.*
+  FROM airlines t3
+  WHERE t3.`dest` IN ('ORD', 'JFK', 'SFO')
+)
 SELECT `origin`, count(1) AS `count`
-FROM (
-  SELECT t2.*
-  FROM airlines t2
-  WHERE t2.`dest` IN ('ORD', 'JFK', 'SFO')
-) t0
+FROM t1
   LEFT SEMI JOIN (
-    SELECT t2.*
-    FROM (
-      SELECT t3.`dest`, avg(t3.`arrdelay`) AS `Mean(arrdelay)`
-      FROM airlines t3
-      WHERE t3.`dest` IN ('ORD', 'JFK', 'SFO')
-      GROUP BY 1
-    ) t2
-    ORDER BY t2.`Mean(arrdelay)` DESC
+    SELECT t0.*
+    FROM t0
+    ORDER BY t0.`Mean(arrdelay)` DESC
     LIMIT 10
-  ) t1
-    ON t0.`dest` = t1.`dest`
+  ) t2
+    ON t1.`dest` = t2.`dest`
 GROUP BY 1
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_topk_operation/e1.sql b/ibis/tests/sql/snapshots/test_select_sql/test_topk_operation/e1.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_topk_operation/e1.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_topk_operation/e1.sql
@@ -1,13 +1,14 @@
+WITH t0 AS (
+  SELECT t1.`city`, avg(t1.`v2`) AS `Mean(v2)`
+  FROM tbl t1
+  GROUP BY 1
+)
 SELECT *
-FROM tbl t0
+FROM tbl t1
   LEFT SEMI JOIN (
-    SELECT t2.*
-    FROM (
-      SELECT t0.`city`, avg(t0.`v2`) AS `Mean(v2)`
-      FROM tbl t0
-      GROUP BY 1
-    ) t2
-    ORDER BY t2.`Mean(v2)` DESC
+    SELECT t0.*
+    FROM t0
+    ORDER BY t0.`Mean(v2)` DESC
     LIMIT 10
-  ) t1
-    ON t0.`city` = t1.`city`
\ No newline at end of file
+  ) t2
+    ON t1.`city` = t2.`city`
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_topk_operation/e2.sql b/ibis/tests/sql/snapshots/test_select_sql/test_topk_operation/e2.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_topk_operation/e2.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_topk_operation/e2.sql
@@ -1,13 +1,14 @@
+WITH t0 AS (
+  SELECT t1.`city`, count(t1.`city`) AS `count`
+  FROM tbl t1
+  GROUP BY 1
+)
 SELECT *
-FROM tbl t0
+FROM tbl t1
   LEFT SEMI JOIN (
-    SELECT t2.*
-    FROM (
-      SELECT t0.`city`, count(t0.`city`) AS `count`
-      FROM tbl t0
-      GROUP BY 1
-    ) t2
-    ORDER BY t2.`count` DESC
+    SELECT t0.*
+    FROM t0
+    ORDER BY t0.`count` DESC
     LIMIT 10
-  ) t1
-    ON t0.`city` = t1.`city`
\ No newline at end of file
+  ) t2
+    ON t1.`city` = t2.`city`
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_topk_predicate_pushdown_bug/out.sql b/ibis/tests/sql/snapshots/test_select_sql/test_topk_predicate_pushdown_bug/out.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_topk_predicate_pushdown_bug/out.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_topk_predicate_pushdown_bug/out.sql
@@ -1,21 +1,22 @@
 WITH t0 AS (
-  SELECT t2.*, t3.`n_name`, t4.`r_name`
-  FROM tpch_customer t2
-    INNER JOIN tpch_nation t3
-      ON t2.`c_nationkey` = t3.`n_nationkey`
-    INNER JOIN tpch_region t4
-      ON t3.`n_regionkey` = t4.`r_regionkey`
+  SELECT t3.*, t4.`n_name`, t5.`r_name`
+  FROM tpch_customer t3
+    INNER JOIN tpch_nation t4
+      ON t3.`c_nationkey` = t4.`n_nationkey`
+    INNER JOIN tpch_region t5
+      ON t4.`n_regionkey` = t5.`r_regionkey`
+),
+t1 AS (
+  SELECT t0.`n_name`, sum(t0.`c_acctbal`) AS `Sum(c_acctbal)`
+  FROM t0
+  GROUP BY 1
 )
 SELECT *
 FROM t0
   LEFT SEMI JOIN (
-    SELECT t2.*
-    FROM (
-      SELECT t0.`n_name`, sum(t0.`c_acctbal`) AS `Sum(c_acctbal)`
-      FROM t0
-      GROUP BY 1
-    ) t2
-    ORDER BY t2.`Sum(c_acctbal)` DESC
+    SELECT t1.*
+    FROM t1
+    ORDER BY t1.`Sum(c_acctbal)` DESC
     LIMIT 10
-  ) t1
-    ON t0.`n_name` = t1.`n_name`
\ No newline at end of file
+  ) t2
+    ON t0.`n_name` = t2.`n_name`
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_sqlalchemy/test_gh_1045/out.sql b/ibis/tests/sql/snapshots/test_sqlalchemy/test_gh_1045/out.sql
--- a/ibis/tests/sql/snapshots/test_sqlalchemy/test_gh_1045/out.sql
+++ b/ibis/tests/sql/snapshots/test_sqlalchemy/test_gh_1045/out.sql
@@ -1,40 +1,41 @@
-SELECT
-  t0.t1_id1,
-  t0.t1_val1,
-  t1.dt,
-  t1.id3,
-  t1.t3_val2,
-  t1.id2a,
-  t1.id2b,
-  t1.val2
-FROM (
+WITH t2 AS (
+  SELECT
+    t4.id1 AS t1_id1,
+    t4.val1 AS t1_val1
+  FROM test1 AS t4
+), t0 AS (
   SELECT
-    t2.id1 AS t1_id1,
-    t2.val1 AS t1_val1
-  FROM test1 AS t2
-) AS t0
+    t4.val2 AS val2,
+    t4.dt AS dt,
+    CAST(t4.id3 AS BIGINT) AS id3
+  FROM test3 AS t4
+), t1 AS (
+  SELECT
+    t0.dt AS dt,
+    t0.id3 AS id3,
+    t0.id3 AS t3_val2
+  FROM t0
+)
+SELECT
+  t2.t1_id1,
+  t2.t1_val1,
+  t3.dt,
+  t3.id3,
+  t3.t3_val2,
+  t3.id2a,
+  t3.id2b,
+  t3.val2
+FROM t2
 LEFT OUTER JOIN (
   SELECT
-    t2.dt AS dt,
-    t2.id3 AS id3,
-    t2.t3_val2 AS t3_val2,
-    t3.id2a AS id2a,
-    t3.id2b AS id2b,
-    t3.val2 AS val2
-  FROM (
-    SELECT
-      t4.dt AS dt,
-      t4.id3 AS id3,
-      t4.id3 AS t3_val2
-    FROM (
-      SELECT
-        t5.val2 AS val2,
-        t5.dt AS dt,
-        CAST(t5.id3 AS BIGINT) AS id3
-      FROM test3 AS t5
-    ) AS t4
-  ) AS t2
-  JOIN test2 AS t3
-    ON t3.id2b = t2.id3
-) AS t1
-  ON t0.t1_id1 = t1.id2a
\ No newline at end of file
+    t1.dt AS dt,
+    t1.id3 AS id3,
+    t1.t3_val2 AS t3_val2,
+    t5.id2a AS id2a,
+    t5.id2b AS id2b,
+    t5.val2 AS val2
+  FROM t1
+  JOIN test2 AS t5
+    ON t5.id2b = t1.id3
+) AS t3
+  ON t2.t1_id1 = t3.id2a
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_sqlalchemy/test_lower_projection_sort_key/out.sql b/ibis/tests/sql/snapshots/test_sqlalchemy/test_lower_projection_sort_key/out.sql
--- a/ibis/tests/sql/snapshots/test_sqlalchemy/test_lower_projection_sort_key/out.sql
+++ b/ibis/tests/sql/snapshots/test_sqlalchemy/test_lower_projection_sort_key/out.sql
@@ -1,30 +1,31 @@
+WITH t0 AS (
+  SELECT
+    t3.foo_id AS foo_id,
+    SUM(t3.f) AS total
+  FROM star1 AS t3
+  GROUP BY
+    1
+), t1 AS (
+  SELECT
+    t0.foo_id AS foo_id,
+    t0.total AS total,
+    t4.value1 AS value1
+  FROM t0
+  JOIN star2 AS t4
+    ON t0.foo_id = t4.foo_id
+)
 SELECT
-  t0.foo_id,
-  t0.total,
-  t0.value1
+  t2.foo_id,
+  t2.total,
+  t2.value1
 FROM (
   SELECT
     t1.foo_id AS foo_id,
     t1.total AS total,
     t1.value1 AS value1
-  FROM (
-    SELECT
-      t2.foo_id AS foo_id,
-      t2.total AS total,
-      t3.value1 AS value1
-    FROM (
-      SELECT
-        t4.foo_id AS foo_id,
-        SUM(t4.f) AS total
-      FROM star1 AS t4
-      GROUP BY
-        1
-    ) AS t2
-    JOIN star2 AS t3
-      ON t2.foo_id = t3.foo_id
-  ) AS t1
+  FROM t1
   WHERE
     t1.total > 100
-) AS t0
+) AS t2
 ORDER BY
-  t0.total DESC
\ No newline at end of file
+  t2.total DESC
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_sqlalchemy/test_no_cart_join/out.sql b/ibis/tests/sql/snapshots/test_sqlalchemy/test_no_cart_join/out.sql
--- a/ibis/tests/sql/snapshots/test_sqlalchemy/test_no_cart_join/out.sql
+++ b/ibis/tests/sql/snapshots/test_sqlalchemy/test_no_cart_join/out.sql
@@ -1,8 +1,4 @@
-SELECT
-  t1.ancestor_node_sort_order,
-  1 AS n
-FROM facts AS t0
-JOIN (
+WITH t0 AS (
   SELECT
     t2.ancestor_level_name AS ancestor_level_name,
     t2.ancestor_level_number AS ancestor_level_number,
@@ -12,9 +8,14 @@ JOIN (
       t2.ancestor_level_number - 1
     ) * 7, '-'), t2.ancestor_level_name) AS product_level_name
   FROM products AS t2
-) AS t1
-  ON t0.product_id = t1.descendant_node_natural_key
+)
+SELECT
+  t0.ancestor_node_sort_order,
+  1 AS n
+FROM facts AS t1
+JOIN t0
+  ON t1.product_id = t0.descendant_node_natural_key
 GROUP BY
   1
 ORDER BY
-  t1.ancestor_node_sort_order
\ No newline at end of file
+  t0.ancestor_node_sort_order
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_sqlalchemy/test_subquery_aliased/out.sql b/ibis/tests/sql/snapshots/test_sqlalchemy/test_subquery_aliased/out.sql
--- a/ibis/tests/sql/snapshots/test_sqlalchemy/test_subquery_aliased/out.sql
+++ b/ibis/tests/sql/snapshots/test_sqlalchemy/test_subquery_aliased/out.sql
@@ -1,14 +1,15 @@
-SELECT
-  t0.foo_id,
-  t0.total,
-  t1.value1
-FROM (
+WITH t0 AS (
   SELECT
     t2.foo_id AS foo_id,
     SUM(t2.f) AS total
   FROM star1 AS t2
   GROUP BY
     1
-) AS t0
+)
+SELECT
+  t0.foo_id,
+  t0.total,
+  t1.value1
+FROM t0
 JOIN star2 AS t1
   ON t0.foo_id = t1.foo_id
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_sqlalchemy/test_tpc_h11/out.sql b/ibis/tests/sql/snapshots/test_sqlalchemy/test_tpc_h11/out.sql
--- a/ibis/tests/sql/snapshots/test_sqlalchemy/test_tpc_h11/out.sql
+++ b/ibis/tests/sql/snapshots/test_sqlalchemy/test_tpc_h11/out.sql
@@ -1,40 +1,41 @@
+WITH t0 AS (
+  SELECT
+    t2.ps_partkey AS ps_partkey,
+    SUM(t2.ps_supplycost * t2.ps_availqty) AS value
+  FROM partsupp AS t2
+  JOIN supplier AS t3
+    ON t2.ps_suppkey = t3.s_suppkey
+  JOIN nation AS t4
+    ON t4.n_nationkey = t3.s_nationkey
+  WHERE
+    t4.n_name = 'GERMANY'
+  GROUP BY
+    1
+)
 SELECT
-  t0.ps_partkey,
-  t0.value
+  t1.ps_partkey,
+  t1.value
 FROM (
   SELECT
-    t1.ps_partkey AS ps_partkey,
-    t1.value AS value
-  FROM (
-    SELECT
-      t2.ps_partkey AS ps_partkey,
-      SUM(t2.ps_supplycost * t2.ps_availqty) AS value
-    FROM partsupp AS t2
-    JOIN supplier AS t3
-      ON t2.ps_suppkey = t3.s_suppkey
-    JOIN nation AS t4
-      ON t4.n_nationkey = t3.s_nationkey
-    WHERE
-      t4.n_name = 'GERMANY'
-    GROUP BY
-      1
-  ) AS t1
+    t0.ps_partkey AS ps_partkey,
+    t0.value AS value
+  FROM t0
   WHERE
-    t1.value > (
+    t0.value > (
       SELECT
         anon_1.total
       FROM (
         SELECT
-          SUM(t2.ps_supplycost * t2.ps_availqty) AS total
-        FROM partsupp AS t2
-        JOIN supplier AS t3
-          ON t2.ps_suppkey = t3.s_suppkey
-        JOIN nation AS t4
-          ON t4.n_nationkey = t3.s_nationkey
+          SUM(t3.ps_supplycost * t3.ps_availqty) AS total
+        FROM partsupp AS t3
+        JOIN supplier AS t4
+          ON t3.ps_suppkey = t4.s_suppkey
+        JOIN nation AS t5
+          ON t5.n_nationkey = t4.s_nationkey
         WHERE
-          t4.n_name = 'GERMANY'
+          t5.n_name = 'GERMANY'
       ) AS anon_1
     ) * 0.0001
-) AS t0
+) AS t1
 ORDER BY
-  t0.value DESC
\ No newline at end of file
+  t1.value DESC
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_sqlalchemy/test_tpc_h17/out.sql b/ibis/tests/sql/snapshots/test_sqlalchemy/test_tpc_h17/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/tests/sql/snapshots/test_sqlalchemy/test_tpc_h17/out.sql
@@ -0,0 +1,15 @@
+SELECT
+  SUM(t0.l_extendedprice) / 7 AS avg_yearly
+FROM lineitem AS t0
+JOIN part AS t1
+  ON t1.p_partkey = t0.l_partkey
+WHERE
+  t1.p_brand = 'Brand#23'
+  AND t1.p_container = 'MED BOX'
+  AND t0.l_quantity < (
+    SELECT
+      AVG(t0.l_quantity) AS "Mean(l_quantity)"
+    FROM lineitem AS t0
+    WHERE
+      t0.l_partkey = t1.p_partkey
+  ) * 0.2
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_sqlalchemy/test_where_correlated_subquery_with_join/out.sql b/ibis/tests/sql/snapshots/test_sqlalchemy/test_where_correlated_subquery_with_join/out.sql
--- a/ibis/tests/sql/snapshots/test_sqlalchemy/test_where_correlated_subquery_with_join/out.sql
+++ b/ibis/tests/sql/snapshots/test_sqlalchemy/test_where_correlated_subquery_with_join/out.sql
@@ -1,26 +1,27 @@
+WITH t0 AS (
+  SELECT
+    t2.ps_partkey AS ps_partkey,
+    t2.ps_supplycost AS ps_supplycost
+  FROM partsupp AS t2
+  JOIN supplier AS t3
+    ON t3.s_suppkey = t2.ps_suppkey
+)
 SELECT
-  t0.p_partkey,
-  t0.ps_supplycost
+  t1.p_partkey,
+  t1.ps_supplycost
 FROM (
   SELECT
-    t1.p_partkey AS p_partkey,
-    t2.ps_supplycost AS ps_supplycost
-  FROM part AS t1
-  JOIN partsupp AS t2
-    ON t1.p_partkey = t2.ps_partkey
-) AS t0
+    t2.p_partkey AS p_partkey,
+    t3.ps_supplycost AS ps_supplycost
+  FROM part AS t2
+  JOIN partsupp AS t3
+    ON t2.p_partkey = t3.ps_partkey
+) AS t1
 WHERE
-  t0.ps_supplycost = (
+  t1.ps_supplycost = (
     SELECT
-      MIN(t1.ps_supplycost) AS "Min(ps_supplycost)"
-    FROM (
-      SELECT
-        t2.ps_partkey AS ps_partkey,
-        t2.ps_supplycost AS ps_supplycost
-      FROM partsupp AS t2
-      JOIN supplier AS t3
-        ON t3.s_suppkey = t2.ps_suppkey
-    ) AS t1
+      MIN(t0.ps_supplycost) AS "Min(ps_supplycost)"
+    FROM t0
     WHERE
-      t1.ps_partkey = t0.p_partkey
+      t0.ps_partkey = t1.p_partkey
   )
\ No newline at end of file
diff --git a/ibis/tests/sql/test_sqlalchemy.py b/ibis/tests/sql/test_sqlalchemy.py
--- a/ibis/tests/sql/test_sqlalchemy.py
+++ b/ibis/tests/sql/test_sqlalchemy.py
@@ -584,3 +584,33 @@ def test_order_by_expr(snapshot):
     t = ibis.table(dict(a="int", b="string"), name="t")
     expr = t[lambda t: t.a == 1].order_by(lambda t: t.b + "a")
     snapshot.assert_match(to_sql(expr), "out.sql")
+
+
+def test_tpc_h17(snapshot):
+    BRAND = "Brand#23"
+    CONTAINER = "MED BOX"
+
+    lineitem = ibis.table(
+        dict(
+            l_partkey="!int32", l_quantity="!int32", l_extendedprice="!decimal(15, 2)"
+        ),
+        name="lineitem",
+    )
+    part = ibis.table(
+        dict(p_partkey="!int32", p_brand="!string", p_container="!string"), name="part"
+    )
+
+    q = lineitem.join(part, part.p_partkey == lineitem.l_partkey)
+    innerq = lineitem.filter([lineitem.l_partkey == q.p_partkey])
+    q = q.filter(
+        [
+            q.p_brand == BRAND,
+            q.p_container == CONTAINER,
+            q.l_quantity < (0.2 * innerq.l_quantity.mean()),
+        ]
+    )
+    q = q.aggregate(
+        avg_yearly=q.l_extendedprice.sum() / ibis.literal(7.0, type="decimal(15, 2)")
+    )
+
+    snapshot.assert_match(to_sql(q), "out.sql")