Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions docs/database/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,178 @@ class State(rx.State):
return [list(row) for row in session.execute("SELECT * FROM user").all()]
```

## Writing Efficient Queries

The database is much better at filtering, joining, and aggregating than Python.
Every row returned by a query is sent over the network and held in memory as
part of the state — for every user with an open session. Do the work in the
query itself and return only display-ready results.

The examples below use a simple `Order` model.

```python
class Order(rx.Model, table=True):
user_id: int
status: str
amount: float | None = None
```

### Aggregate in SQL, not Python

Avoid fetching a whole table just to compute a summary in Python. Use the
database's aggregate functions (`COUNT`, `SUM`, `AVG`, `MIN`, `MAX`) with
`GROUP BY` so only the summary rows come back.

```python
from sqlmodel import select, func


class OrderStats(rx.State):
order_count: int = 0
orders_by_status: list[tuple[str, int]] = []

@rx.event
def load_stats(self):
with rx.session() as session:
# Count rows in the database instead of len() on a full fetch.
self.order_count = session.exec(select(func.count(Order.id))).one()

# One row per group, ready for a chart or summary table.
self.orders_by_status = [
tuple(row)
for row in session.exec(
select(Order.status, func.count(Order.id)).group_by(Order.status)
).all()
]
```

Related metrics can be computed in a single query with conditional
aggregation, instead of issuing one query per metric:

```python
import sqlalchemy
from sqlmodel import select, func


class OrderKPIs(rx.State):
total_orders: int = 0
completed_orders: int = 0
total_amount: float = 0.0

@rx.event
def load_kpis(self):
with rx.session() as session:
total, completed, amount = session.exec(
select(
func.count(Order.id),
func.sum(
sqlalchemy.case((Order.status == "completed", 1), else_=0)
),
func.coalesce(func.sum(Order.amount), 0.0),
)
).one()
self.total_orders = total
self.completed_orders = completed or 0
self.total_amount = float(amount)
```

### Avoid Queries Inside Loops

Issuing a query per item — the "N+1" pattern — multiplies network round trips.
Fetch everything in one statement with a `JOIN`, an `IN (...)` list, or
`GROUP BY`.

```python
# Inefficient: one query per user (N+1).
with rx.session() as session:
for user in users:
orders = session.exec(Order.select().where(Order.user_id == user.id)).all()

# Efficient: one query for all users at once.
with rx.session() as session:
orders = session.exec(
Order.select().where(Order.user_id.in_([user.id for user in users]))
).all()
```

The `.in_()` method binds each value as a separate query parameter, so it is
safe to use with user-provided values. In raw SQL, use an *expanding* bind
parameter for the same effect — never join the values into the SQL string:

```python
import sqlalchemy

with rx.session() as session:
rows = session.execute(
sqlalchemy.text("SELECT * FROM users WHERE id IN :user_ids").bindparams(
sqlalchemy.bindparam("user_ids", expanding=True)
),
{"user_ids": [1, 2, 3]},
).all()
```

If the related objects are linked with foreign keys, the
[relationship loading techniques](/docs/database/relationships) can also fetch
linked objects without extra queries.

### Return Only What the UI Needs

- Select only the columns the UI displays when tables are wide, instead of
whole rows.
- Give every query that returns detail rows a `.limit()`, and use
offset-based [pagination](/docs/library/tables-and-data-grids/table) when
the user needs more rows.
- Load static data (dropdown options, date bounds) once in an `on_load` event
handler. When a filter changes, re-run only the filtered queries — not the
option lookups.
- Avoid caching large raw query results in state vars to work around a slow
query: per-user state duplicates that data in server memory for every
session. Fix the query instead — aggregate in SQL, add a limit, combine
round trips. Keeping small, display-ready results in state (chart rows, KPI
values, one page of a table) is the intended pattern.

## Handling NULL Values

Database columns may contain `NULL`, which arrives in Python as `None`.
Aggregates like `SUM` and `AVG` also return `NULL` when they run over zero
rows. Casting such results directly will crash:

```python
float(row[0]) # TypeError: float() argument must be ... not 'NoneType'
```

There are two ways to handle this.

### Coalesce in the Query (Preferred)

`COALESCE` returns its first non-`NULL` argument, so the query itself
guarantees a usable value. This is especially important for nullable numeric
columns and for aggregates that may run over empty groups.

```python
from sqlmodel import select, func

with rx.session() as session:
total = session.exec(select(func.coalesce(func.sum(Order.amount), 0.0))).one()
```

The same works in raw SQL: `SELECT COALESCE(SUM(amount), 0) FROM ...`.

### Guard in Python

When consuming rows that may contain `NULL`s, fall back explicitly while
building the values:

```python
orders = [
{
"status": str(row[0] or ""),
"amount": float(row[1]) if row[1] is not None else 0.0,
}
for row in rows
]
```

## Async Database Operations

Reflex provides an async version of the session function called `rx.asession` for asynchronous database operations. This is useful when you need to perform database operations in an async context, such as within async event handlers.
Expand Down
48 changes: 47 additions & 1 deletion docs/database/relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class User(rx.Model, table=True):


class Flag(rx.Model, table=True):
post_id: int = sqlmodel.Field(foreign_key="post.id")
post_id: int = sqlmodel.Field(foreign_key="post.id", index=True)
user_id: int = sqlmodel.Field(foreign_key="user.id")
message: str

Expand Down Expand Up @@ -160,3 +160,49 @@ class Post(rx.Model, table=True):
sa_relationship_kwargs={"lazy": "selectin"},
)
```

### Querying Related Rows on Demand

For master-detail interfaces — a table of parent rows where selecting a row
reveals its related rows — it is not necessary to eager load every
relationship up front. Store the selected id in state and query only the
related rows for that selection, filtering on the foreign key.

```python
from typing import List, Optional

import reflex as rx


class PostFlagsState(rx.State):
posts: List[Post] = []
selected_post_id: Optional[int] = None
selected_flags: List[Flag] = []
Comment thread
greptile-apps[bot] marked this conversation as resolved.

@rx.event
def load_posts(self):
with rx.session() as session:
self.posts = session.exec(Post.select().limit(15)).all()

@rx.event
def select_post(self, post_id: int):
self.selected_post_id = post_id
with rx.session() as session:
self.selected_flags = session.exec(
Flag.select().where(Flag.post_id == post_id)
).all()
```

This runs a single indexed query per selection and avoids holding every child
row in state — the `index=True` on `Flag.post_id` keeps the foreign-key lookup
off a full table scan (adding the index to an existing table requires a
migration). Avoid the inverse pattern of querying children in a Python loop
over parents (the "N+1" pattern) — when the children of many parents are
needed at once, use one query with `.in_()` on the foreign key, or one of the
eager loading options above.

```python
post_ids = [post.id for post in self.posts]
with rx.session() as session:
flags = session.exec(Flag.select().where(Flag.post_id.in_(post_ids))).all()
```
17 changes: 14 additions & 3 deletions docs/library/tables-and-data-grids/table.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ rx.table.root(

```md alert info
# Set the table `width` to fit within its container and prevent it from overflowing.
If the table has too many columns to fit, wrap the `rx.table.root` in a container with `overflow_x="auto"` (e.g. `rx.box(rx.table.root(...), overflow_x="auto", width="100%")`) so the table scrolls horizontally inside the container instead of stretching the page.
```

## Showing State data (using foreach)
Expand Down Expand Up @@ -342,7 +343,7 @@ def database_table_example():
),
rx.input(
placeholder="Search here...",
on_change=lambda value: DatabaseTableState.filter_values(value),
on_change=DatabaseTableState.filter_values.debounce(500),
),
rx.table.root(
rx.table.header(
Expand Down Expand Up @@ -607,6 +608,11 @@ For filtering the `rx.input` component is used. The data is filtered based on th

The `%` character before and after `search_value` makes it a wildcard pattern that matches any sequence of characters before or after the `search_value`. `query.where(...)` modifies the existing query to include a filtering condition. The `or_` operator is a logical OR operator that combines multiple conditions. The query will return results that match any of these conditions. `Customer.name.ilike(search_value)` checks if the `name` column of the `Customer` table matches the `search_value` pattern in a case-insensitive manner (`ilike` stands for "case-insensitive like").

```md alert info
# Debounce the search input
`on_change` fires on every keystroke, and each event here runs a database query. Chaining `.debounce(500)` on the event handler waits until the user pauses typing for 500ms, so one query runs per search instead of one per character. See [event actions](/docs/events/event-actions) for details.
```

```python
class Customer(rx.Model, table=True):
"""The customer model."""
Expand Down Expand Up @@ -712,7 +718,7 @@ def loading_data_table_example_2():
),
rx.input(
placeholder="Search here...",
on_change=lambda value: DatabaseTableState2.filter_values(value),
on_change=DatabaseTableState2.filter_values.debounce(500),
),
rx.table.root(
rx.table.header(
Expand Down Expand Up @@ -802,7 +808,7 @@ def loading_data_table_example2():
),
rx.input(
placeholder="Search here...",
on_change=lambda value: DatabaseTableState2.filter_values(value),
on_change=DatabaseTableState2.filter_values.debounce(500),
),
rx.table.root(
rx.table.header(
Expand Down Expand Up @@ -1044,6 +1050,11 @@ def loading_data_table_example3():
)
```

```md alert info
# Combining pagination with search and sorting
Give each paginated table its own state vars (`offset`, `limit`, `total_items`) so multiple tables on a page paginate independently, and reset `offset` to `0` whenever the search or sort value changes — otherwise the user may be left on a page number beyond the end of the newly filtered results.
```

## More advanced examples

The real power of the `rx.table` comes where you are able to visualise, add and edit data live in your app. Check out these apps and code to see how this is done: app: https://customer-data-app.reflex.run code: https://github.com/reflex-dev/templates/tree/main/customer_data_app and code: https://github.com/reflex-dev/templates/tree/main/sales.
Expand Down
Loading