Tables & Schema
Explore your tables, visualize database schemas, browse rows, inspect columns, and perform safe data updates in AxiomDB.
Tables & Schema
The AxiomDB dashboard gives you a clean, visual interface to explore your branch's database schema, browse through table data, inspect columns and relationships, and safely update records. There is no need to write complex SQL queries just to view your data—though you can always jump into a SQL editor whenever you need it!
Navigating the Table Explorer
The Table Explorer is your main starting point for exploring your database structure. It shows a list of all the tables in your public schema, along with estimated row counts, storage size, and index details.
┌─────────────────────────────────────────────────────────────────┐
│ Table Explorer [Search tables...] │
│ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ ● users 12,847 rows 4.2 MB ││
│ │ ● orders 89,203 rows 28.1 MB ││
│ │ ● order_items 234,102 rows 45.6 MB ││
│ │ ● products 342 rows 1.1 MB ││
│ │ ● payments 87,450 rows 18.3 MB ││
│ │ ● sessions 1,203,441 rows 98.7 MB ││
│ │ ○ _prisma_migrations 12 rows 0.1 MB ││
│ └─────────────────────────────────────────────────────────────┘│
│ │
│ 7 tables · 196.1 MB total · 1,627,397 total rows │
└─────────────────────────────────────────────────────────────────┘Understanding table metadata
For each table in the list, we show some helpful stats:
| Field | Source | Description |
|---|---|---|
| Row count | pg_stat_user_tables.n_live_tup | An estimated count of active rows (updated by database ANALYZE commands). |
| Table size | pg_relation_size() | The storage size of the table's data pages alone. |
| Total size | pg_total_relation_size() | The complete size of the table, including its data, indexes, and TOAST pages. |
| Last vacuum | pg_stat_user_tables.last_vacuum | The last time a manual or automatic database cleanup (VACUUM) occurred. |
| Last analyze | pg_stat_user_tables.last_analyze | The last time database stats were calculated (ANALYZE). |
| Dead tuples | pg_stat_user_tables.n_dead_tup | Rows that have been deleted or updated and are waiting to be cleaned up. |
The query behind these stats
If you're curious about how we get these numbers, here is the SQL query running behind the scenes:
SELECT
c.relname AS table_name,
pg_size_pretty(pg_relation_size(c.oid)) AS table_size,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size,
s.n_live_tup AS row_count,
s.n_dead_tup AS dead_tuples,
s.last_vacuum,
s.last_analyze
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
LEFT JOIN pg_stat_user_tables s ON s.relid = c.oid
WHERE n.nspname = 'public'
AND c.relkind = 'r'
ORDER BY pg_total_relation_size(c.oid) DESC;Schema Flow Visualizer
The Schema Flow Visualizer creates an interactive diagram of your database's tables and foreign key relationships. It automatically checks your database constraints and maps how tables connect to one another.
┌──────────────────────────────────────────────────────────────────┐
│ Schema Flow [Zoom: 100%] │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ users │──────►│ orders │──────►│ order_items │ │
│ │ │ 1:N │ │ 1:N │ │ │
│ │ id (PK) │ │ id (PK) │ │ id (PK) │ │
│ │ email │ │ user_id │ │ order_id │ │
│ │ name │ │ status │ │ product_id │ │
│ └──────────┘ │ total │ │ quantity │ │
│ └─────┬────┘ │ price │ │
│ │ └──────┬───────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────────┐ │
│ │ payments │ │ products │ │
│ │ │ │ │ │
│ │ id (PK) │ │ id (PK) │ │
│ │ order_id │ │ name │ │
│ │ amount │ │ price │ │
│ │ method │ │ stock │ │
│ └──────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────────┘How we detect relationships
To build this visual map, we query your database's foreign key constraints using this SQL query:
SELECT
tc.table_name AS source_table,
kcu.column_name AS source_column,
ccu.table_name AS target_table,
ccu.column_name AS target_column,
tc.constraint_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
JOIN information_schema.constraint_column_usage ccu
ON ccu.constraint_name = tc.constraint_name
AND ccu.table_schema = tc.table_schema
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'public'
ORDER BY tc.table_name;Determining relationships (Cardinality)
We figure out table relationships (1:1, 1:N, or N:M) by inspecting constraints:
- Unique constraint on the foreign key column →
1:1relationship. - No unique constraint on the foreign key column →
1:Nrelationship. - Junction table holding foreign keys for two other tables →
N:Mrelationship.
We check this using a query like this:
-- Check if foreign key column has a unique constraint
SELECT
kcu.column_name,
CASE WHEN tc.constraint_type = 'UNIQUE' THEN '1:1' ELSE '1:N' END AS cardinality
FROM information_schema.key_column_usage kcu
LEFT JOIN information_schema.table_constraints tc
ON kcu.constraint_name = tc.constraint_name
AND tc.constraint_type = 'UNIQUE'
WHERE kcu.table_name = 'orders'
AND kcu.column_name = 'user_id';Row Browser
The Row Browser lets you browse, filter, sort, and manage your data with simple controls.
┌─────────────────────────────────────────────────────────────────┐
│ orders · 89,203 rows [Filter] [Sort] [Export]│
│ │
│ ┌──────┬──────────┬────────┬─────────┬────────────┬──────────┐ │
│ │ id │ user_id │ status │ total │ created_at │ actions │ │
│ ├──────┼──────────┼────────┼─────────┼────────────┼──────────┤ │
│ │ 1001 │ usr_abc │ paid │ $49.99 │ 2025-03-20 │ [Edit] │ │
│ │ 1002 │ usr_def │ pending│ $129.50 │ 2025-03-20 │ [Edit] │ │
│ │ 1003 │ usr_ghi │ paid │ $19.99 │ 2025-03-19 │ [Edit] │ │
│ │ 1004 │ usr_jkl │ failed │ $89.00 │ 2025-03-19 │ [Edit] │ │
│ │ 1005 │ usr_mno │ paid │ $249.99 │ 2025-03-18 │ [Edit] │ │
│ └──────┴──────────┴────────┴─────────┴────────────┴──────────┘ │
│ │
│ Showing 1–5 of 89,203 [< Prev] Page 1 [Next >] │
└─────────────────────────────────────────────────────────────────┘Fast and smooth paging
For optimal performance, the Row Browser uses cursor-based pagination. This prevents database slowdowns on large tables:
-- First page
SELECT * FROM orders
ORDER BY id ASC
LIMIT 50;
-- Next page (cursor = last id from previous page)
SELECT * FROM orders
WHERE id > :cursor
ORDER BY id ASC
LIMIT 50;Why cursor-based pagination?
Traditional offset-based pagination (OFFSET 10000 LIMIT 50) can slow down your database because PostgreSQL has to scan and discard all preceding rows. Cursor-based pagination uses table indexes, keeping requests lightning fast no matter which page you're on!
Sorting columns
Sorting is as simple as clicking a column header. The browser executes:
-- Ascending
SELECT * FROM orders ORDER BY total ASC LIMIT 50;
-- Descending
SELECT * FROM orders ORDER BY total DESC LIMIT 50;Tip: You can sort by multiple columns by holding the Shift key while clicking column headers.
Building custom filters
Our visual filter builder translates your criteria into SQL WHERE clauses:
┌─────────────────────────────────────────┐
│ Filter │
│ │
│ [status] [equals] [paid] [AND] │
│ [total] [> ] [50.00] [Apply] │
│ │
└─────────────────────────────────────────┘Here's the SQL query that gets generated:
SELECT * FROM orders
WHERE status = 'paid' AND total > 50.00
ORDER BY id ASC
LIMIT 50;We support a wide range of filter options:
| Operator | Type Compatibility | SQL generated |
|---|---|---|
equals | All | = $1 |
not equals | All | != $1 |
> | Numeric, Date | > $1 |
< | Numeric, Date | < $1 |
>= | Numeric, Date | >= $1 |
<= | Numeric, Date | <= $1 |
contains | Text | LIKE '%' || $1 || '%' |
starts with | Text | LIKE $1 || '%' |
is null | All | IS NULL |
is not null | All | IS NOT NULL |
in | All | IN ($1, $2, ...) |
What if my tables are empty?
If a table doesn't have any rows yet, we'll show you a clean empty state to help you get started:
┌─────────────────────────────────────────────────────────────────┐
│ orders · 0 rows │
│ │
│ ┌─────────────────┐ │
│ │ │ │
│ │ No rows yet │ │
│ │ │ │
│ │ Insert data │ │
│ │ via your app │ │
│ │ or run a │ │
│ │ migration. │ │
│ │ │ │
│ └─────────────────┘ │
│ │
│ [Open SQL Console] [View Schema] │
└─────────────────────────────────────────────────────────────────┘Checking for empty tables
We detect empty tables in your database using this query:
SELECT
c.relname AS table_name,
s.n_live_tup AS row_count,
CASE WHEN s.n_live_tup = 0 THEN true ELSE false END AS is_empty
FROM pg_class c
JOIN pg_stat_user_tables s ON s.relid = c.oid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public' AND c.relkind = 'r';Safe database operations
To prevent accidental updates or deletions, we wrap database changes in transactions and ask for confirmation before applying them.
Editing a row
You can update individual cells directly inside the browser. We'll run:
-- Generated when you edit a cell
BEGIN;
UPDATE orders SET status = 'shipped' WHERE id = 1001;
COMMIT;Deleting a row
Deleting a record requires a quick confirmation step. The matching SQL query:
-- Generated when you delete a row
BEGIN;
DELETE FROM orders WHERE id = 1001;
COMMIT;Bulk updates and deletions
If you need to make changes to multiple rows at once, select them to run a batch update:
┌─────────────────────────────────────────────────────────────────┐
│ orders · 3 rows selected [Delete] [Export]│
│ │
│ ☑ 1001 │ usr_abc │ paid │ $49.99 │ 2025-03-20 │
│ ☑ 1002 │ usr_def │ pending │ $129.50 │ 2025-03-20 │
│ ☑ 1003 │ usr_ghi │ paid │ $19.99 │ 2025-03-19 │
└─────────────────────────────────────────────────────────────────┘The matching query looks like this:
BEGIN;
DELETE FROM orders WHERE id IN (1001, 1002, 1003);
COMMIT;Applying database changes
Database writes and modifications run via the owner role using your DIRECT_URL. Updates are applied immediately and cannot be undone, so please double-check your values before confirming!
Can I undo an edit?
While there is no direct undo button, you can trace all dashboard changes in the branch's audit logs:
{
"event": "ROW_UPDATED",
"table": "orders",
"row_id": 1001,
"changes": {
"status": { "old": "pending", "new": "shipped" }
},
"actor": "usr_x9y8z7",
"timestamp": "2025-03-20T14:30:00Z"
}Column Inspection
Clicking on any table reveals its column metadata, including default values, constraints, and indexes:
┌─────────────────────────────────────────────────────────────────┐
│ orders · Columns │
│ │
│ ┌──────────────┬──────────────┬─────────┬─────────┬──────────┐ │
│ │ Column │ Type │ Nullable│ Default │ Key │ │
│ ├──────────────┼──────────────┼─────────┼─────────┼──────────┤ │
│ │ id │ bigint │ NO │ nextval │ PK │ │
│ │ user_id │ uuid │ NO │ — │ FK→users │ │
│ │ status │ varchar(20) │ NO │ 'pending│ — │ │
│ │ total │ numeric(10,2)│ NO │ — │ — │ │
│ │ created_at │ timestamptz │ NO │ now() │ — │ │
│ │ updated_at │ timestamptz │ YES │ — │ — │ │
│ │ notes │ text │ YES │ — │ — │ │
│ └──────────────┴──────────────┴─────────┴─────────┴──────────┘ │
│ │
│ Indexes │
│ ┌─────────────────────────────────┬──────────┬────────────────┐ │
│ │ Index Name │ Columns │ Unique │ │
│ ├─────────────────────────────────┼──────────┼────────────────┤ │
│ │ orders_pkey │ id │ Yes │ │
│ │ orders_user_id_idx │ user_id │ No │ │
│ │ orders_status_created_at_idx │ status, │ No │ │
│ │ │ created_at│ │ │
│ └─────────────────────────────────┴──────────┴────────────────┘ │
└─────────────────────────────────────────────────────────────────┘Retrieving column details
We get this information by querying standard system tables:
SELECT
c.column_name,
c.data_type,
c.character_maximum_length,
c.numeric_precision,
c.is_nullable,
c.column_default,
CASE WHEN pk.column_name IS NOT NULL THEN 'PK' ELSE '' END AS is_primary_key,
CASE WHEN fk.column_name IS NOT NULL THEN 'FK' ELSE '' END AS is_foreign_key
FROM information_schema.columns c
LEFT JOIN (
SELECT kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.constraint_type = 'PRIMARY KEY'
AND tc.table_name = 'orders'
AND tc.table_schema = 'public'
) pk ON pk.column_name = c.column_name
LEFT JOIN (
SELECT kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_name = 'orders'
AND tc.table_schema = 'public'
) fk ON fk.column_name = c.column_name
WHERE c.table_name = 'orders'
AND c.table_schema = 'public'
ORDER BY c.ordinal_position;Foreign Key Relationships
The column details view also highlights foreign key connections and constraint behavior:
SELECT
tc.constraint_name,
kcu.column_name AS source_column,
ccu.table_name AS referenced_table,
ccu.column_name AS referenced_column,
rc.update_rule,
rc.delete_rule
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
ON ccu.constraint_name = tc.constraint_name
JOIN information_schema.referential_constraints rc
ON rc.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_name = 'orders'
AND tc.table_schema = 'public';Common cascade behaviors
| Rule | How it behaves when a parent row is deleted |
|---|---|
CASCADE | Automatically deletes matching child rows. |
SET NULL | Sets the child row's foreign key column to NULL. |
SET DEFAULT | Sets the child row's foreign key column to its defined default value. |
RESTRICT | Blocks the delete action if child records exist. |
NO ACTION | Similar to RESTRICT, checking constraints at the very end of the transaction block. |
Index Inspection
We also show you details about your table indexes, including storage sizes and usage statistics:
SELECT
i.relname AS index_name,
ix.indisunique AS is_unique,
ix.indisprimary AS is_primary,
pg_size_pretty(pg_relation_size(i.oid)) AS index_size,
s.idx_scan AS times_used,
pg_get_indexdef(ix.indexrelid) AS definition
FROM pg_index ix
JOIN pg_class i ON i.oid = ix.indexrelid
JOIN pg_class t ON t.oid = ix.indrelid
LEFT JOIN pg_stat_user_indexes s ON s.indexrelid = ix.indexrelid
WHERE t.relname = 'orders'
AND t.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')
ORDER BY pg_relation_size(i.oid) DESC;Finding unused indexes
To optimize your database, you can search for indexes that aren't being used:
SELECT
schemaname || '.' || relname AS table_name,
indexrelname AS index_name,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
idx_scan AS times_used
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND schemaname = 'public'
AND indexrelname NOT LIKE '%_pkey'
ORDER BY pg_relation_size(indexrelid) DESC;Resetting index stats
Keep in mind that the idx_scan counter resets if your database server restarts, or if the index is dropped and recreated. Look over these stats with your server uptime in mind.
Exporting your data
You can download data directly from the Row Browser in several useful formats:
| Format | Common uses |
|---|---|
| CSV | Running spreadsheet analyses or sharing data |
| JSON | Integrating with APIs or loading database rows in apps |
| SQL | Creating manual backups or schema migration files |
Query used for CSV export
COPY (
SELECT * FROM orders WHERE status = 'paid'
) TO STDOUT WITH CSV HEADER;Query used for JSON export
SELECT json_agg(row_to_json(t))
FROM (
SELECT * FROM orders WHERE status = 'paid'
) t;Quick actions menu
From the Table Explorer, you can perform these quick tasks:
| Action | Description |
|---|---|
| View Data | Open the Row Browser for the selected table. |
| View Schema | Inspect columns, types, and constraints. |
| View Indexes | Look over table indexes and usage statistics. |
| View Relationships | See incoming and outgoing foreign keys. |
| Copy Table Name | Copy the full database table path to your clipboard. |
| Generate SELECT | Copy a pre-built SELECT query template. |
| Export | Download table data in CSV, JSON, or SQL format. |
| Open in SQL Console | Open a ready-to-run query in the SQL editor window. |
How is this guide?
Limits
Branch caps, storage quotas, compute accounting, extension mechanisms, and monitoring recommendations.
Branches
Database branches are independent copy-on-write clones of a parent PostgreSQL database. Each branch has its own connection credentials, network rules, metrics, and configurable lifespan — enabling safe schema experimentation, preview environments, and team isolation.
