docs(database): apply house style to the tables guide (#50021)

Part 1 of a 5-PR stack on
`apps/docs/content/guides/database/tables.mdx`, one change type per PR.

## Problem

The page addressed the reader as "we" in about 18 places. CONTRIBUTING
reserves `we` for the Supabase team and asks that the reader be `you`.
None of it was caught by the linter, because
`Rule004ExcludeWords/first_person` only bans singular first person.

Alongside that: scare quotes on established terms, parenthetical asides
that CONTRIBUTING disallows, future tense where present tense reads
better, an ordered list that repeated `1.` four times, and three
relative links where `/docs/...` paths belong.

**The three diagrams had alt text that named a topic instead of
describing the picture.** "Schemas and tables" tells a screen reader
nothing about a diagram showing two schema boxes, one labeled `public`
holding six tables and one labeled `api` holding three.

## Solution

Inline rewrites and cuts. **Nothing in this PR moves a line from one
place to another.**

Each alt now describes its diagram: the column types in the table
diagram, the arrow between matching columns in the foreign key diagram,
and the two labeled schemas with their table counts.

Two deletions worth calling out:

- The `<br />` spacer after the data type table.
- The four-item benefits list under "When to use views". The four
headings immediately below restate it verbatim.

Also fixes "Every column is a predefined type", which states the
relationship backwards. A column has a type; it isn't one.

## One dead link, surfaced by the conversion

The Loading data intro pointed at `guides/database/api`, which isn't a
page. It exists only as a redirect in `apps/www/lib/redirects.js`, and
that redirect doesn't serve the docs deployment, so the link 404s there.
As a relative link it was invisible to the link checker; converting it
to a `/docs/...` path is what made the Docs E2E suite catch it.

It now points at `/docs/guides/api`, the live page that 13 other guides
already link to.

## What this PR leaves to the ones above it

Section moves and the Views page split are #50022. Corrections to claims
are #50023. New content is #50024 and #50025.

## Manual testing

Preview:
https://docs-git-docs-tables-style-supabase.vercel.app/docs/guides/database/tables

1. Open the preview. The intro reads "Excel spreadsheets" and
"relational databases", and the only remaining "we" is "We provide a SQL
editor within the Dashboard", which refers to Supabase rather than the
reader.
2. Inspect the three images on the preview. Each `alt` describes the
diagram rather than naming its topic.
3. Follow the **Data API** link under "Loading data". It resolves
instead of returning 404.
4. Run `npx prettier --check
apps/docs/content/guides/database/tables.mdx` and `pnpm lint:mdx` from
`apps/docs`. Both pass.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Documentation**
- Clarified guidance on table creation, data types, primary keys, bulk
loading, relationships, schemas, views, and materialized views.
- Improved wording, capitalization, terminology, and internal navigation
throughout the tables guide.
  - Updated diagram alt text with more descriptive captions.
  - Updated the loading data section to link to the Data API guide.
- Revised the bulk-loading example with an explicit column list, CSV
options, and a simplified database connection command.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Miranda Limonczenko authored and GitHub committed 2026-09-14 16:31:11 -07:00
1 parent 2cd9b42e80
commit e022145be9
1 file changed
+58 -65
+58 -65
View File
@@ -7,8 +7,8 @@ video: 'https://www.youtube.com/v/TKwF3IGij5c'
Tables are where you store your data.
Tables are similar to excel spreadsheets. They contain columns and rows.
For example, this table has 3 "columns" (`id`, `name`, `description`) and 4 "rows" of data:
Tables are similar to Excel spreadsheets. They contain columns and rows.
For example, this table has 3 columns named `id`, `name`, and `description`, and 4 rows of data:
{/* supa-mdx-lint-disable Rule003Spelling */}
@@ -21,14 +21,14 @@ For example, this table has 3 "columns" (`id`, `name`, `description`) and 4 "row
{/* supa-mdx-lint-enable Rule003Spelling */}
There are a few important differences from a spreadsheet, but it's a good starting point if you're new to Relational databases.
There are a few important differences from a spreadsheet, but it's a good starting point if you're new to relational databases.
## Creating tables
When creating a table, it's best practice to add columns at the same time.
<Image
alt="Tables and columns"
alt="A table containing five columns, each labeled with its data type: integer, text, text, json, and datetime."
src={{
dark: '/docs/img/database/managing-tables/creating-tables.png',
@@ -38,10 +38,10 @@ width={1600}
height={1145}
/>
You must define the "data type" of each column when it is created. You can add and remove columns at any time after creating a table.
You must define the data type of each column when you create it. You can add and remove columns at any time after creating a table.
Supabase provides several options for creating tables. You can use the Dashboard or create them directly using SQL.
We provide a SQL editor within the Dashboard, or you can [connect](../../guides/database/connecting-to-postgres) to your database
We provide a SQL editor within the Dashboard, or you can [connect](/docs/guides/database/connecting-to-postgres) to your database
and run the SQL queries yourself.
<Tabs
@@ -82,17 +82,17 @@ create table movies (
<Admonition type="note">
When naming tables, use lowercase and underscores instead of spaces (e.g., `table_name`, not `Table Name`).
When naming tables, use lowercase and underscores instead of spaces. For example, use `table_name` rather than `Table Name`.
</Admonition>
## Columns
You must define the "data type" when you create a column.
You must define the data type when you create a column.
### Data types
Every column is a predefined type. Postgres provides many [default types](https://www.postgresql.org/docs/current/datatype.html), and you can even design your own (or use extensions) if the default types don't fit your needs. You can use any data type that Postgres supports via the SQL editor. We only support a subset of these in the Table Editor in an effort to keep the experience focused for people with less experience with databases.
Every column has a data type. Postgres provides many [default types](https://www.postgresql.org/docs/current/datatype.html), and you can design your own or use extensions if the default types don't fit your needs. You can use any data type that Postgres supports via the SQL editor. The Table Editor supports a subset of these, which keeps the experience focused for people with less database experience.
<details>
<summary>Show/Hide default data types</summary>
@@ -145,16 +145,14 @@ Every column is a predefined type. Postgres provides many [default types](https:
</details>
<br />
You can "cast" columns from one type to another, however there can be some incompatibilities between types.
For example, if you cast a `timestamp` to a `date`, you will lose all the time information that was previously saved.
You can cast columns from one type to another, but some types are incompatible.
For example, if you cast a `timestamp` to a `date`, you lose all the time information that was previously saved.
### Primary keys
A table can have a "primary key" - a unique identifier for every row of data. A few tips for Primary Keys:
A table can have a primary key, a unique identifier for every row of data. A few tips for primary keys:
- It's recommended to create a Primary Key for every table in your database.
- Create a primary key for every table in your database.
- You can use any column as a primary key, as long as it is unique for every row.
- It's common to use a `uuid` type or a numbered `identity` column as your primary key.
@@ -164,14 +162,14 @@ create table movies (
);
```
In the example above, we have:
In the example above, you:
1. created a column called `id`
1. assigned the data type `bigint`
1. instructed the database that this should be `generated always as identity`, which means that Postgres will automatically assign a unique number to this column.
1. Because it's unique, we can also use it as our `primary key`.
1. Created a column called `id`.
2. Assigned the data type `bigint`.
3. Instructed the database that this column is `generated always as identity`, so Postgres automatically assigns it a unique number.
4. Used it as the `primary key`, because the value is unique.
We could also use `generated by default as identity`, which would allow us to insert our own unique values.
You can also use `generated by default as identity`, which lets you insert your own unique values.
```sql
create table movies (
@@ -181,8 +179,8 @@ create table movies (
## Loading data
There are several ways to load data in Supabase. You can load data directly into the database or using the [APIs](../../guides/database/api).
Use the "Bulk Loading" instructions if you are loading large data sets.
There are several ways to load data in Supabase. You can load data directly into the database, or use the [Data API](/docs/guides/api).
If you're loading large data sets, follow the bulk data loading instructions.
### Basic data loading
@@ -351,38 +349,38 @@ await supabase.From<Movie>().Insert(movies);
### Bulk data loading
When inserting large data sets it's best to use Postgres's [COPY](https://www.postgresql.org/docs/current/sql-copy.html) command.
This loads data directly from a file into a table. There are several file formats available for copying data: text, CSV, binary, JSON, etc.
When inserting large data sets, use Postgres's [COPY](https://www.postgresql.org/docs/current/sql-copy.html) command.
This loads data directly from a file into a table. Several file formats are available for copying data, including text, CSV, binary, and JSON.
For example, if you wanted to load a CSV file into your movies table:
For example, to load a CSV file into your `movies` table:
```text ./movies.csv
"The Empire Strikes Back", "After the Rebels are brutally overpowered by the Empire on the ice planet Hoth, Luke Skywalker begins Jedi training with Yoda."
"Return of the Jedi", "After a daring mission to rescue Han Solo from Jabba the Hutt, the Rebels dispatch to Endor to destroy the second Death Star."
```
You would [connect](../../guides/database/connecting-to-postgres#direct-connection) to your database directly and load the file with the COPY command:
[Connect](/docs/guides/database/connecting-to-postgres#direct-connection) to your database directly and load the file with the `COPY` command:
```bash
psql -h DATABASE_URL -p 5432 -d postgres -U postgres \
-c "\COPY movies FROM './movies.csv';"
```
Additionally use the `DELIMITER`, `HEADER` and `FORMAT` options as defined in the Postgres [COPY](https://www.postgresql.org/docs/current/sql-copy.html) docs.
You can also use the `DELIMITER`, `HEADER`, and `FORMAT` options as defined in the Postgres [COPY](https://www.postgresql.org/docs/current/sql-copy.html) docs.
```bash
psql -h DATABASE_URL -p 5432 -d postgres -U postgres \
-c "\COPY movies FROM './movies.csv' WITH DELIMITER ',' CSV HEADER"
```
If you receive an error `FATAL: password authentication failed for user "postgres"`, reset your database password in the Database Settings and try again.
If you receive an error `FATAL: password authentication failed for user "postgres"`, reset your database password in **Database Settings** and try again.
## Joining tables with foreign keys
Tables can be "joined" together using Foreign Keys.
Tables can be joined together using foreign keys.
<Image
alt="Foreign Keys"
alt="Two tables. An arrow runs from a highlighted column in the first table to a matching highlighted column in the second."
src={{
dark: '/docs/img/database/managing-tables/joining-tables.png',
@@ -392,9 +390,9 @@ width={1600}
height={1145}
/>
This is where the "Relational" naming comes from, as data typically forms some sort of relationship.
This is where the term relational comes from, because data typically forms some sort of relationship.
In our "movies" example above, we might want to add a "category" for each movie (for example, "Action", or "Documentary").
In the `movies` example above, you might want to add a category for each movie, such as Action or Documentary.
Create a new table called `categories` and link it to the `movies` table.
```sql
@@ -407,8 +405,8 @@ alter table movies
add column category_id bigint references categories;
```
You can also create "many-to-many" relationships by creating a "join" table.
For example if you had the following situations:
You can also create many-to-many relationships by creating a join table.
For example, consider this situation:
- You have a list of `movies`.
- A movie can have several `actors`.
@@ -452,10 +450,10 @@ create table performances (
## Schemas
Tables belong to `schemas`. Schemas are a way of organizing your tables, often for security reasons.
Tables belong to schemas. Schemas are a way of organizing your tables, often for security reasons.
<Image
alt="Schemas and tables"
alt="Two schemas side by side. The schema labeled public holds six tables, and the schema labeled api holds three."
src={{
dark: '/docs/img/database/managing-tables/schemas.png',
@@ -465,15 +463,15 @@ width={1600}
height={1145}
/>
If you don't explicitly pass a schema when creating a table, Postgres will assume that you want to create the table in the `public` schema.
If you don't explicitly pass a schema when creating a table, Postgres creates the table in the `public` schema.
We can create schemas for organizing tables. For example, we might want a private schema which is hidden from our API:
You can create schemas to organize tables. For example, you might want a private schema that's hidden from your API:
```sql
create schema private;
```
Now we can create tables inside the `private` schema:
Now you can create tables inside the `private` schema:
```sql
create table private.salaries (
@@ -485,15 +483,15 @@ create table private.salaries (
<Admonition type="note">
If you want to access a custom schema through the Supabase Data API, you need to expose it and grant the appropriate permissions. See [Using Custom Schemas](/docs/guides/api/using-custom-schemas) for detailed steps. For security best practices around schema exposure, see [Securing your API](/docs/guides/api/securing-your-api).
A custom schema isn't reachable through the Supabase Data API until you expose it and grant the appropriate permissions. See [Using custom schemas](/docs/guides/api/using-custom-schemas) for the steps, and [Securing your API](/docs/guides/api/securing-your-api) for security best practices around schema exposure.
</Admonition>
## Views
A View is a convenient shortcut to a query. Creating a view does not involve new tables or data. When run, an underlying query is executed, returning its results to the user.
A view is a convenient shortcut to a query. Creating a view doesn't involve new tables or data. When you run a view, Postgres executes the underlying query and returns its results.
Say we have the following tables from a database of a university:
Say you have the following tables from a university database:
**`students`**
@@ -526,7 +524,7 @@ Say we have the following tables from a database of a university:
| 5 | 3 | 2 | A |
| 6 | 3 | 3 | B- |
Creating a view consisting of all the three tables will look like this:
Creating a view that consists of all three tables looks like this:
```sql
create view transcripts as
@@ -543,7 +541,7 @@ create view transcripts as
grant all on table transcripts to authenticated;
```
Once done, we can now access the underlying query with:
Then you can access the underlying query with:
```sql
select * from transcripts;
@@ -551,7 +549,7 @@ select * from transcripts;
### View security
By default, views are accessed with their creator's permission ("security definer"). If a privileged role creates a view, others accessing it will use that role's elevated permissions. To enforce row level security policies, define the view with the "security invoker" modifier.
By default, views are accessed with their creator's permission, known as `security definer`. If a privileged role creates a view, others accessing it use that role's elevated permissions. To enforce row level security policies, define the view with the `security_invoker` modifier.
```sql
-- alter a security_definer view to be security_invoker
@@ -566,16 +564,11 @@ create view <view name> with(security_invoker=true) as (
### When to use views
Views provide several benefits:
- Simplicity
- Consistency
- Logical Organization
- Security
Views provide several benefits.
#### Simplicity
As a query becomes more complex, it can be a hassle to call it over and over - especially when we run it regularly. In the example above, instead of repeatedly running:
As a query becomes more complex, calling it repeatedly gets tedious, especially when you run it regularly. In the example above, instead of repeatedly running:
```sql
select
@@ -590,17 +583,17 @@ from
left join courses on grades.course_id = courses.id;
```
We can run this instead:
You can run this instead:
```sql
select * from transcripts;
```
Additionally, a view behaves like a typical table. We can safely use it in table `JOIN`s or even create new views using existing views.
A view also behaves like a typical table. You can safely use it in table joins or create new views from existing views.
#### Consistency
Views ensure that the likelihood of mistakes decreases when repeatedly executing a query. In our example above, we may decide that we want to exclude the course _Introduction to Postgres_. The query would become:
Views reduce the likelihood of mistakes when you execute a query repeatedly. In the example above, you might decide to exclude the course _Introduction to Postgres_. The query becomes:
```sql
select
@@ -616,21 +609,21 @@ from
where courses.code != 'PG101';
```
Without a view, we would need to go into every dependent query to add the new rule. This would increase in the likelihood of errors and inconsistencies, as well as introducing a lot of effort for a developer. With views, we can alter the underlying query in the view **transcripts**. The change will be applied to all applications using this view.
Without a view, you need to add the new rule to every dependent query. That increases the likelihood of errors and inconsistencies, and it takes considerable effort. With views, you alter the underlying query in the `transcripts` view, and the change applies to every application using it.
#### Logical organization
With views, we can give our query a name. This is extremely useful for teams working with the same database. Instead of guessing what a query is supposed to do, a well-named view can explain it. For example, by looking at the name of the view **transcripts**, we can infer that the underlying query might involve the **students**, **courses**, and **grades** tables.
With views, you can give your query a name. This is useful for teams working with the same database. Instead of guessing what a query does, a well-named view explains it. For example, the name of the `transcripts` view suggests that the underlying query involves the `students`, `courses`, and `grades` tables.
#### Security
Views can restrict the amount and type of data presented to a user. Instead of allowing a user direct access to a set of tables, we provide them a view instead. We can prevent them from reading sensitive columns by excluding them from the underlying query.
Views can restrict the amount and type of data presented to a user. Instead of giving a user direct access to a set of tables, you give them a view. You can prevent them from reading sensitive columns by excluding those columns from the underlying query.
### Materialized views
A [materialized view](https://www.postgresql.org/docs/current/rules-materializedviews.html) is a form of view but it also stores the results to disk. In subsequent reads of a materialized view, the time taken to return its results would be much faster than a conventional view. This is because the data is readily available for a materialized view while the conventional view executes the underlying query each time it is called.
A [materialized view](https://www.postgresql.org/docs/current/rules-materializedviews.html) is a form of view that also stores its results to disk. Subsequent reads of a materialized view return results much faster than a conventional view, because the data is already available. A conventional view executes the underlying query each time you call it.
Using our example above, a materialized view can be created like this:
Using the example above, you can create a materialized view like this:
```sql
create materialized view transcripts as
@@ -654,19 +647,19 @@ select * from transcripts;
### Refreshing materialized views
Unfortunately, there is a trade-off - data in materialized views are not always up to date. We need to refresh it regularly to prevent the data from becoming too stale. To do so:
There's a trade-off: data in a materialized view isn't always up to date. Refresh it regularly to prevent the data from becoming too stale.
```sql
refresh materialized view transcripts;
```
It's up to you how regularly refresh your materialized views, and it's probably different for each view depending on its use-case.
How often you refresh a materialized view is up to you, and it probably differs for each view depending on its use case.
### Materialized views vs conventional views
Materialized views are useful when execution times for queries or views are too slow. These could likely occur in views or queries involving multiple tables and billions of rows. When using such a view, however, there should be tolerance towards data being outdated. Some use-cases for materialized views are internal dashboards and analytics.
Materialized views are useful when execution times for queries or views are too slow. This happens in views or queries that involve multiple tables and billions of rows. Use a materialized view only when you can tolerate outdated data. Internal dashboards and analytics are common use cases.
Creating a materialized view is not a solution to inefficient queries. You should always seek to optimize a slow running query even if you are implementing a materialized view.
Creating a materialized view isn't a solution to inefficient queries. Always optimize a slow-running query, even when you implement a materialized view.
## Resources