Merged
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
10 changes: 10 additions & 0 deletions cmd/inspect.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ import (
"github.com/supabase/cli/internal/inspect/replication_slots"
"github.com/supabase/cli/internal/inspect/role_stats"
"github.com/supabase/cli/internal/inspect/table_stats"
"github.com/supabase/cli/internal/inspect/traffic_profile"
"github.com/supabase/cli/internal/inspect/vacuum_stats"
"github.com/supabase/cli/internal/utils/flags"
)
Expand DownExpand Up@@ -135,6 +136,14 @@ var (
},
}

inspectTrafficProfileCmd = &cobra.Command{
Use: "traffic-profile",
Short: "Show read/write activity ratio for tables based on block I/O operations",
RunE: func(cmd *cobra.Command, args []string) error {
return traffic_profile.Run(cmd.Context(), flags.DbConfig, afero.NewOsFs())
},
}

inspectCacheHitCmd = &cobra.Command{
Deprecated: `use "db-stats" instead.`,
Use: "cache-hit",
Expand DownExpand Up@@ -270,6 +279,7 @@ func init() {
inspectDBCmd.AddCommand(inspectBloatCmd)
inspectDBCmd.AddCommand(inspectVacuumStatsCmd)
inspectDBCmd.AddCommand(inspectTableStatsCmd)
inspectDBCmd.AddCommand(inspectTrafficProfileCmd)
inspectDBCmd.AddCommand(inspectRoleStatsCmd)
inspectDBCmd.AddCommand(inspectDBStatsCmd)
// DEPRECATED
Expand Down
24 changes: 24 additions & 0 deletions docs/supabase/inspect/db-traffic-profile.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
# db-traffic-profile

This command analyzes table I/O patterns to show read/write activity ratios based on block-level operations. It combines data from PostgreSQL's `pg_stat_user_tables` (for tuple operations) and `pg_statio_user_tables` (for block I/O) to categorize each table's workload profile.


The command classifies tables into categories:
- **Read-Heavy** - Read operations are more than 5x write operations (e.g., 1:10, 1:50)
- **Write-Heavy** - Write operations are more than 20% of read operations (e.g., 1:2, 1:4, 2:1, 10:1)
- **Balanced** - Mixed workload where writes are between 20% and 500% of reads
- **Read-Only** - Only read operations detected
- **Write-Only** - Only write operations detected

```
SCHEMA │ TABLE │ BLOCKS READ │ WRITE TUPLES │ BLOCKS WRITE │ ACTIVITY RATIO
───────┼──────────────┼─────────────┼──────────────┼──────────────┼────────────────────
public │ user_events │ 450,234 │ 9,004,680│ 23,450 │ 20:1 (Write-Heavy)
public │ users │ 89,203 │ 12,451│ 1,203 │ 7.2:1 (Read-Heavy)
public │ sessions │ 15,402 │ 14,823│ 2,341 │ ≈1:1 (Balanced)
public │ cache_data │ 123,456 │ 0│ 0 │ Read-Only
auth │ audit_logs │ 0 │ 98,234│ 12,341 │ Write-Only
```

**Note:** This command only displays tables that have had both read and write activity. Tables with no I/O operations are not shown. The classification ratio threshold (default: 5:1) determines when a table is considered "heavy" in one direction versus balanced.

49 changes: 49 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
package traffic_profile

import (
"context"
_ "embed"
"fmt"

"github.com/go-errors/errors"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/pkg/pgxv5"
)

//go:embed traffic_profile.sql
var TrafficProfileQuery string

type Result struct {
Schemaname string
Table_name string
Blocks_read int64
Write_tuples int64
Blocks_write float64
Activity_ratio string
}

func Run(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
conn, err := utils.ConnectByConfig(ctx, config, options...)
if err != nil {
return err
}
defer conn.Close(context.Background())
rows, err := conn.Query(ctx, TrafficProfileQuery)
if err != nil {
return errors.Errorf("failed to query rows: %w", err)
}
result, err := pgxv5.CollectRows[Result](rows)
if err != nil {
return err
}

table := "|Schema|Table|Blocks Read|Write Tuples|Blocks Write|Activity Ratio|\n|-|-|-|-|-|-|\n"
for _, r := range result {
table += fmt.Sprintf("|`%s`|`%s`|`%d`|`%d`|`%.1f`|`%s`|\n",
r.Schemaname, r.Table_name, r.Blocks_read, r.Write_tuples, r.Blocks_write, r.Activity_ratio)
}
return utils.RenderTable(table)
}
45 changes: 45 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile.sql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
-- Query adapted from Crunchy Data blog: "Is Postgres Read Heavy or Write Heavy? (And Why You Should Care)" by David Christensen
WITH
ratio_target AS (SELECT 5 AS ratio),
table_list AS (SELECT
s.schemaname,
s.relname AS table_name,
si.heap_blks_read + si.idx_blks_read AS blocks_read,
s.n_tup_ins + s.n_tup_upd + s.n_tup_del AS write_tuples,
relpages * (s.n_tup_ins + s.n_tup_upd + s.n_tup_del ) / (case when reltuples = 0 then 1 else reltuples end) as blocks_write
FROM
pg_stat_user_tables AS s
JOIN pg_statio_user_tables AS si ON s.relid = si.relid
JOIN pg_class c ON c.oid = s.relid
WHERE
(s.n_tup_ins + s.n_tup_upd + s.n_tup_del) > 0
AND
(si.heap_blks_read + si.idx_blks_read) > 0
)
SELECT
schemaname,
table_name,
blocks_read,
write_tuples,
blocks_write,
CASE
WHEN blocks_read = 0 and blocks_write = 0 THEN
'No Activity'
WHEN blocks_write * ratio > blocks_read THEN
CASE
WHEN blocks_read = 0 THEN 'Write-Only'
ELSE
ROUND(blocks_write :: numeric / blocks_read :: numeric, 1)::text || ':1 (Write-Heavy)'
END
WHEN blocks_read > blocks_write * ratio THEN
CASE
WHEN blocks_write = 0 THEN 'Read-Only'
ELSE
'1:' || ROUND(blocks_read::numeric / blocks_write :: numeric, 1)::text || ' (Read-Heavy)'
END
ELSE
'1:1 (Balanced)'
END AS activity_ratio
FROM table_list, ratio_target
ORDER BY
(blocks_read + blocks_write) DESC
42 changes: 42 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile_test.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
package traffic_profile

import (
"context"
"testing"

"github.com/jackc/pgconn"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/supabase/cli/pkg/pgtest"
)

var dbConfig = pgconn.Config{
Host: "127.0.0.1",
Port: 5432,
User: "admin",
Password: "password",
Database: "postgres",
}

func TestTrafficProfile(t *testing.T) {
t.Run("inspects traffic profile", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query(TrafficProfileQuery).
Reply("SELECT 6", Result{
Schemaname: "public",
Table_name: "users",
Blocks_read: 1000,
Write_tuples: 500,
Blocks_write: 250.5,
Activity_ratio: "1:4.0 (Read-Heavy)",
})
// Run test
err := Run(context.Background(), dbConfig, fsys, conn.Intercept)
// Check error
assert.NoError(t, err)
})
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
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
10 changes: 10 additions & 0 deletions cmd/inspect.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ import (
"github.com/supabase/cli/internal/inspect/replication_slots"
"github.com/supabase/cli/internal/inspect/role_stats"
"github.com/supabase/cli/internal/inspect/table_stats"
"github.com/supabase/cli/internal/inspect/traffic_profile"
"github.com/supabase/cli/internal/inspect/vacuum_stats"
"github.com/supabase/cli/internal/utils/flags"
)
Expand DownExpand Up@@ -135,6 +136,14 @@ var (
},
}

inspectTrafficProfileCmd = &cobra.Command{
Use: "traffic-profile",
Short: "Show read/write activity ratio for tables based on block I/O operations",
RunE: func(cmd *cobra.Command, args []string) error {
return traffic_profile.Run(cmd.Context(), flags.DbConfig, afero.NewOsFs())
},
}

inspectCacheHitCmd = &cobra.Command{
Deprecated: `use "db-stats" instead.`,
Use: "cache-hit",
Expand DownExpand Up@@ -270,6 +279,7 @@ func init() {
inspectDBCmd.AddCommand(inspectBloatCmd)
inspectDBCmd.AddCommand(inspectVacuumStatsCmd)
inspectDBCmd.AddCommand(inspectTableStatsCmd)
inspectDBCmd.AddCommand(inspectTrafficProfileCmd)
inspectDBCmd.AddCommand(inspectRoleStatsCmd)
inspectDBCmd.AddCommand(inspectDBStatsCmd)
// DEPRECATED
Expand Down
24 changes: 24 additions & 0 deletions docs/supabase/inspect/db-traffic-profile.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
# db-traffic-profile

This command analyzes table I/O patterns to show read/write activity ratios based on block-level operations. It combines data from PostgreSQL's `pg_stat_user_tables` (for tuple operations) and `pg_statio_user_tables` (for block I/O) to categorize each table's workload profile.


The command classifies tables into categories:
- **Read-Heavy** - Read operations are more than 5x write operations (e.g., 1:10, 1:50)
- **Write-Heavy** - Write operations are more than 20% of read operations (e.g., 1:2, 1:4, 2:1, 10:1)
- **Balanced** - Mixed workload where writes are between 20% and 500% of reads
- **Read-Only** - Only read operations detected
- **Write-Only** - Only write operations detected

```
SCHEMA │ TABLE │ BLOCKS READ │ WRITE TUPLES │ BLOCKS WRITE │ ACTIVITY RATIO
───────┼──────────────┼─────────────┼──────────────┼──────────────┼────────────────────
public │ user_events │ 450,234 │ 9,004,680│ 23,450 │ 20:1 (Write-Heavy)
public │ users │ 89,203 │ 12,451│ 1,203 │ 7.2:1 (Read-Heavy)
public │ sessions │ 15,402 │ 14,823│ 2,341 │ ≈1:1 (Balanced)
public │ cache_data │ 123,456 │ 0│ 0 │ Read-Only
auth │ audit_logs │ 0 │ 98,234│ 12,341 │ Write-Only
```

**Note:** This command only displays tables that have had both read and write activity. Tables with no I/O operations are not shown. The classification ratio threshold (default: 5:1) determines when a table is considered "heavy" in one direction versus balanced.

49 changes: 49 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
package traffic_profile

import (
"context"
_ "embed"
"fmt"

"github.com/go-errors/errors"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/pkg/pgxv5"
)

//go:embed traffic_profile.sql
var TrafficProfileQuery string

type Result struct {
Schemaname string
Table_name string
Blocks_read int64
Write_tuples int64
Blocks_write float64
Activity_ratio string
}

func Run(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
conn, err := utils.ConnectByConfig(ctx, config, options...)
if err != nil {
return err
}
defer conn.Close(context.Background())
rows, err := conn.Query(ctx, TrafficProfileQuery)
if err != nil {
return errors.Errorf("failed to query rows: %w", err)
}
result, err := pgxv5.CollectRows[Result](rows)
if err != nil {
return err
}

table := "|Schema|Table|Blocks Read|Write Tuples|Blocks Write|Activity Ratio|\n|-|-|-|-|-|-|\n"
for _, r := range result {
table += fmt.Sprintf("|`%s`|`%s`|`%d`|`%d`|`%.1f`|`%s`|\n",
r.Schemaname, r.Table_name, r.Blocks_read, r.Write_tuples, r.Blocks_write, r.Activity_ratio)
}
return utils.RenderTable(table)
}
45 changes: 45 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile.sql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
-- Query adapted from Crunchy Data blog: "Is Postgres Read Heavy or Write Heavy? (And Why You Should Care)" by David Christensen
WITH
ratio_target AS (SELECT 5 AS ratio),
table_list AS (SELECT
s.schemaname,
s.relname AS table_name,
si.heap_blks_read + si.idx_blks_read AS blocks_read,
s.n_tup_ins + s.n_tup_upd + s.n_tup_del AS write_tuples,
relpages * (s.n_tup_ins + s.n_tup_upd + s.n_tup_del ) / (case when reltuples = 0 then 1 else reltuples end) as blocks_write
FROM
pg_stat_user_tables AS s
JOIN pg_statio_user_tables AS si ON s.relid = si.relid
JOIN pg_class c ON c.oid = s.relid
WHERE
(s.n_tup_ins + s.n_tup_upd + s.n_tup_del) > 0
AND
(si.heap_blks_read + si.idx_blks_read) > 0
)
SELECT
schemaname,
table_name,
blocks_read,
write_tuples,
blocks_write,
CASE
WHEN blocks_read = 0 and blocks_write = 0 THEN
'No Activity'
WHEN blocks_write * ratio > blocks_read THEN
CASE
WHEN blocks_read = 0 THEN 'Write-Only'
ELSE
ROUND(blocks_write :: numeric / blocks_read :: numeric, 1)::text || ':1 (Write-Heavy)'
END
WHEN blocks_read > blocks_write * ratio THEN
CASE
WHEN blocks_write = 0 THEN 'Read-Only'
ELSE
'1:' || ROUND(blocks_read::numeric / blocks_write :: numeric, 1)::text || ' (Read-Heavy)'
END
ELSE
'1:1 (Balanced)'
END AS activity_ratio
FROM table_list, ratio_target
ORDER BY
(blocks_read + blocks_write) DESC
42 changes: 42 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile_test.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
package traffic_profile

import (
"context"
"testing"

"github.com/jackc/pgconn"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/supabase/cli/pkg/pgtest"
)

var dbConfig = pgconn.Config{
Host: "127.0.0.1",
Port: 5432,
User: "admin",
Password: "password",
Database: "postgres",
}

func TestTrafficProfile(t *testing.T) {
t.Run("inspects traffic profile", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query(TrafficProfileQuery).
Reply("SELECT 6", Result{
Schemaname: "public",
Table_name: "users",
Blocks_read: 1000,
Write_tuples: 500,
Blocks_write: 250.5,
Activity_ratio: "1:4.0 (Read-Heavy)",
})
// Run test
err := Run(context.Background(), dbConfig, fsys, conn.Intercept)
// Check error
assert.NoError(t, err)
})
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
10 changes: 10 additions & 0 deletions cmd/inspect.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ import (
"github.com/supabase/cli/internal/inspect/replication_slots"
"github.com/supabase/cli/internal/inspect/role_stats"
"github.com/supabase/cli/internal/inspect/table_stats"
"github.com/supabase/cli/internal/inspect/traffic_profile"
"github.com/supabase/cli/internal/inspect/vacuum_stats"
"github.com/supabase/cli/internal/utils/flags"
)
Expand DownExpand Up@@ -135,6 +136,14 @@ var (
},
}

inspectTrafficProfileCmd = &cobra.Command{
Use: "traffic-profile",
Short: "Show read/write activity ratio for tables based on block I/O operations",
RunE: func(cmd *cobra.Command, args []string) error {
return traffic_profile.Run(cmd.Context(), flags.DbConfig, afero.NewOsFs())
},
}

inspectCacheHitCmd = &cobra.Command{
Deprecated: `use "db-stats" instead.`,
Use: "cache-hit",
Expand DownExpand Up@@ -270,6 +279,7 @@ func init() {
inspectDBCmd.AddCommand(inspectBloatCmd)
inspectDBCmd.AddCommand(inspectVacuumStatsCmd)
inspectDBCmd.AddCommand(inspectTableStatsCmd)
inspectDBCmd.AddCommand(inspectTrafficProfileCmd)
inspectDBCmd.AddCommand(inspectRoleStatsCmd)
inspectDBCmd.AddCommand(inspectDBStatsCmd)
// DEPRECATED
Expand Down
24 changes: 24 additions & 0 deletions docs/supabase/inspect/db-traffic-profile.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
# db-traffic-profile

This command analyzes table I/O patterns to show read/write activity ratios based on block-level operations. It combines data from PostgreSQL's `pg_stat_user_tables` (for tuple operations) and `pg_statio_user_tables` (for block I/O) to categorize each table's workload profile.


The command classifies tables into categories:
- **Read-Heavy** - Read operations are more than 5x write operations (e.g., 1:10, 1:50)
- **Write-Heavy** - Write operations are more than 20% of read operations (e.g., 1:2, 1:4, 2:1, 10:1)
- **Balanced** - Mixed workload where writes are between 20% and 500% of reads
- **Read-Only** - Only read operations detected
- **Write-Only** - Only write operations detected

```
SCHEMA │ TABLE │ BLOCKS READ │ WRITE TUPLES │ BLOCKS WRITE │ ACTIVITY RATIO
───────┼──────────────┼─────────────┼──────────────┼──────────────┼────────────────────
public │ user_events │ 450,234 │ 9,004,680│ 23,450 │ 20:1 (Write-Heavy)
public │ users │ 89,203 │ 12,451│ 1,203 │ 7.2:1 (Read-Heavy)
public │ sessions │ 15,402 │ 14,823│ 2,341 │ ≈1:1 (Balanced)
public │ cache_data │ 123,456 │ 0│ 0 │ Read-Only
auth │ audit_logs │ 0 │ 98,234│ 12,341 │ Write-Only
```

**Note:** This command only displays tables that have had both read and write activity. Tables with no I/O operations are not shown. The classification ratio threshold (default: 5:1) determines when a table is considered "heavy" in one direction versus balanced.

49 changes: 49 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
package traffic_profile

import (
"context"
_ "embed"
"fmt"

"github.com/go-errors/errors"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/pkg/pgxv5"
)

//go:embed traffic_profile.sql
var TrafficProfileQuery string

type Result struct {
Schemaname string
Table_name string
Blocks_read int64
Write_tuples int64
Blocks_write float64
Activity_ratio string
}

func Run(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
conn, err := utils.ConnectByConfig(ctx, config, options...)
if err != nil {
return err
}
defer conn.Close(context.Background())
rows, err := conn.Query(ctx, TrafficProfileQuery)
if err != nil {
return errors.Errorf("failed to query rows: %w", err)
}
result, err := pgxv5.CollectRows[Result](rows)
if err != nil {
return err
}

table := "|Schema|Table|Blocks Read|Write Tuples|Blocks Write|Activity Ratio|\n|-|-|-|-|-|-|\n"
for _, r := range result {
table += fmt.Sprintf("|`%s`|`%s`|`%d`|`%d`|`%.1f`|`%s`|\n",
r.Schemaname, r.Table_name, r.Blocks_read, r.Write_tuples, r.Blocks_write, r.Activity_ratio)
}
return utils.RenderTable(table)
}
45 changes: 45 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile.sql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
-- Query adapted from Crunchy Data blog: "Is Postgres Read Heavy or Write Heavy? (And Why You Should Care)" by David Christensen
WITH
ratio_target AS (SELECT 5 AS ratio),
table_list AS (SELECT
s.schemaname,
s.relname AS table_name,
si.heap_blks_read + si.idx_blks_read AS blocks_read,
s.n_tup_ins + s.n_tup_upd + s.n_tup_del AS write_tuples,
relpages * (s.n_tup_ins + s.n_tup_upd + s.n_tup_del ) / (case when reltuples = 0 then 1 else reltuples end) as blocks_write
FROM
pg_stat_user_tables AS s
JOIN pg_statio_user_tables AS si ON s.relid = si.relid
JOIN pg_class c ON c.oid = s.relid
WHERE
(s.n_tup_ins + s.n_tup_upd + s.n_tup_del) > 0
AND
(si.heap_blks_read + si.idx_blks_read) > 0
)
SELECT
schemaname,
table_name,
blocks_read,
write_tuples,
blocks_write,
CASE
WHEN blocks_read = 0 and blocks_write = 0 THEN
'No Activity'
WHEN blocks_write * ratio > blocks_read THEN
CASE
WHEN blocks_read = 0 THEN 'Write-Only'
ELSE
ROUND(blocks_write :: numeric / blocks_read :: numeric, 1)::text || ':1 (Write-Heavy)'
END
WHEN blocks_read > blocks_write * ratio THEN
CASE
WHEN blocks_write = 0 THEN 'Read-Only'
ELSE
'1:' || ROUND(blocks_read::numeric / blocks_write :: numeric, 1)::text || ' (Read-Heavy)'
END
ELSE
'1:1 (Balanced)'
END AS activity_ratio
FROM table_list, ratio_target
ORDER BY
(blocks_read + blocks_write) DESC
42 changes: 42 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile_test.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
package traffic_profile

import (
"context"
"testing"

"github.com/jackc/pgconn"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/supabase/cli/pkg/pgtest"
)

var dbConfig = pgconn.Config{
Host: "127.0.0.1",
Port: 5432,
User: "admin",
Password: "password",
Database: "postgres",
}

func TestTrafficProfile(t *testing.T) {
t.Run("inspects traffic profile", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query(TrafficProfileQuery).
Reply("SELECT 6", Result{
Schemaname: "public",
Table_name: "users",
Blocks_read: 1000,
Write_tuples: 500,
Blocks_write: 250.5,
Activity_ratio: "1:4.0 (Read-Heavy)",
})
// Run test
err := Run(context.Background(), dbConfig, fsys, conn.Intercept)
// Check error
assert.NoError(t, err)
})
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
10 changes: 10 additions & 0 deletions cmd/inspect.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ import (
"github.com/supabase/cli/internal/inspect/replication_slots"
"github.com/supabase/cli/internal/inspect/role_stats"
"github.com/supabase/cli/internal/inspect/table_stats"
"github.com/supabase/cli/internal/inspect/traffic_profile"
"github.com/supabase/cli/internal/inspect/vacuum_stats"
"github.com/supabase/cli/internal/utils/flags"
)
Expand DownExpand Up@@ -135,6 +136,14 @@ var (
},
}

inspectTrafficProfileCmd = &cobra.Command{
Use: "traffic-profile",
Short: "Show read/write activity ratio for tables based on block I/O operations",
RunE: func(cmd *cobra.Command, args []string) error {
return traffic_profile.Run(cmd.Context(), flags.DbConfig, afero.NewOsFs())
},
}

inspectCacheHitCmd = &cobra.Command{
Deprecated: `use "db-stats" instead.`,
Use: "cache-hit",
Expand DownExpand Up@@ -270,6 +279,7 @@ func init() {
inspectDBCmd.AddCommand(inspectBloatCmd)
inspectDBCmd.AddCommand(inspectVacuumStatsCmd)
inspectDBCmd.AddCommand(inspectTableStatsCmd)
inspectDBCmd.AddCommand(inspectTrafficProfileCmd)
inspectDBCmd.AddCommand(inspectRoleStatsCmd)
inspectDBCmd.AddCommand(inspectDBStatsCmd)
// DEPRECATED
Expand Down
24 changes: 24 additions & 0 deletions docs/supabase/inspect/db-traffic-profile.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
# db-traffic-profile

This command analyzes table I/O patterns to show read/write activity ratios based on block-level operations. It combines data from PostgreSQL's `pg_stat_user_tables` (for tuple operations) and `pg_statio_user_tables` (for block I/O) to categorize each table's workload profile.


The command classifies tables into categories:
- **Read-Heavy** - Read operations are more than 5x write operations (e.g., 1:10, 1:50)
- **Write-Heavy** - Write operations are more than 20% of read operations (e.g., 1:2, 1:4, 2:1, 10:1)
- **Balanced** - Mixed workload where writes are between 20% and 500% of reads
- **Read-Only** - Only read operations detected
- **Write-Only** - Only write operations detected

```
SCHEMA │ TABLE │ BLOCKS READ │ WRITE TUPLES │ BLOCKS WRITE │ ACTIVITY RATIO
───────┼──────────────┼─────────────┼──────────────┼──────────────┼────────────────────
public │ user_events │ 450,234 │ 9,004,680│ 23,450 │ 20:1 (Write-Heavy)
public │ users │ 89,203 │ 12,451│ 1,203 │ 7.2:1 (Read-Heavy)
public │ sessions │ 15,402 │ 14,823│ 2,341 │ ≈1:1 (Balanced)
public │ cache_data │ 123,456 │ 0│ 0 │ Read-Only
auth │ audit_logs │ 0 │ 98,234│ 12,341 │ Write-Only
```

**Note:** This command only displays tables that have had both read and write activity. Tables with no I/O operations are not shown. The classification ratio threshold (default: 5:1) determines when a table is considered "heavy" in one direction versus balanced.

49 changes: 49 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
package traffic_profile

import (
"context"
_ "embed"
"fmt"

"github.com/go-errors/errors"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/pkg/pgxv5"
)

//go:embed traffic_profile.sql
var TrafficProfileQuery string

type Result struct {
Schemaname string
Table_name string
Blocks_read int64
Write_tuples int64
Blocks_write float64
Activity_ratio string
}

func Run(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
conn, err := utils.ConnectByConfig(ctx, config, options...)
if err != nil {
return err
}
defer conn.Close(context.Background())
rows, err := conn.Query(ctx, TrafficProfileQuery)
if err != nil {
return errors.Errorf("failed to query rows: %w", err)
}
result, err := pgxv5.CollectRows[Result](rows)
if err != nil {
return err
}

table := "|Schema|Table|Blocks Read|Write Tuples|Blocks Write|Activity Ratio|\n|-|-|-|-|-|-|\n"
for _, r := range result {
table += fmt.Sprintf("|`%s`|`%s`|`%d`|`%d`|`%.1f`|`%s`|\n",
r.Schemaname, r.Table_name, r.Blocks_read, r.Write_tuples, r.Blocks_write, r.Activity_ratio)
}
return utils.RenderTable(table)
}
45 changes: 45 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile.sql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
-- Query adapted from Crunchy Data blog: "Is Postgres Read Heavy or Write Heavy? (And Why You Should Care)" by David Christensen
WITH
ratio_target AS (SELECT 5 AS ratio),
table_list AS (SELECT
s.schemaname,
s.relname AS table_name,
si.heap_blks_read + si.idx_blks_read AS blocks_read,
s.n_tup_ins + s.n_tup_upd + s.n_tup_del AS write_tuples,
relpages * (s.n_tup_ins + s.n_tup_upd + s.n_tup_del ) / (case when reltuples = 0 then 1 else reltuples end) as blocks_write
FROM
pg_stat_user_tables AS s
JOIN pg_statio_user_tables AS si ON s.relid = si.relid
JOIN pg_class c ON c.oid = s.relid
WHERE
(s.n_tup_ins + s.n_tup_upd + s.n_tup_del) > 0
AND
(si.heap_blks_read + si.idx_blks_read) > 0
)
SELECT
schemaname,
table_name,
blocks_read,
write_tuples,
blocks_write,
CASE
WHEN blocks_read = 0 and blocks_write = 0 THEN
'No Activity'
WHEN blocks_write * ratio > blocks_read THEN
CASE
WHEN blocks_read = 0 THEN 'Write-Only'
ELSE
ROUND(blocks_write :: numeric / blocks_read :: numeric, 1)::text || ':1 (Write-Heavy)'
END
WHEN blocks_read > blocks_write * ratio THEN
CASE
WHEN blocks_write = 0 THEN 'Read-Only'
ELSE
'1:' || ROUND(blocks_read::numeric / blocks_write :: numeric, 1)::text || ' (Read-Heavy)'
END
ELSE
'1:1 (Balanced)'
END AS activity_ratio
FROM table_list, ratio_target
ORDER BY
(blocks_read + blocks_write) DESC
42 changes: 42 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile_test.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
package traffic_profile

import (
"context"
"testing"

"github.com/jackc/pgconn"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/supabase/cli/pkg/pgtest"
)

var dbConfig = pgconn.Config{
Host: "127.0.0.1",
Port: 5432,
User: "admin",
Password: "password",
Database: "postgres",
}

func TestTrafficProfile(t *testing.T) {
t.Run("inspects traffic profile", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query(TrafficProfileQuery).
Reply("SELECT 6", Result{
Schemaname: "public",
Table_name: "users",
Blocks_read: 1000,
Write_tuples: 500,
Blocks_write: 250.5,
Activity_ratio: "1:4.0 (Read-Heavy)",
})
// Run test
err := Run(context.Background(), dbConfig, fsys, conn.Intercept)
// Check error
assert.NoError(t, err)
})
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
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
10 changes: 10 additions & 0 deletions cmd/inspect.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ import (
"github.com/supabase/cli/internal/inspect/replication_slots"
"github.com/supabase/cli/internal/inspect/role_stats"
"github.com/supabase/cli/internal/inspect/table_stats"
"github.com/supabase/cli/internal/inspect/traffic_profile"
"github.com/supabase/cli/internal/inspect/vacuum_stats"
"github.com/supabase/cli/internal/utils/flags"
)
Expand DownExpand Up@@ -135,6 +136,14 @@ var (
},
}

inspectTrafficProfileCmd = &cobra.Command{
Use: "traffic-profile",
Short: "Show read/write activity ratio for tables based on block I/O operations",
RunE: func(cmd *cobra.Command, args []string) error {
return traffic_profile.Run(cmd.Context(), flags.DbConfig, afero.NewOsFs())
},
}

inspectCacheHitCmd = &cobra.Command{
Deprecated: `use "db-stats" instead.`,
Use: "cache-hit",
Expand DownExpand Up@@ -270,6 +279,7 @@ func init() {
inspectDBCmd.AddCommand(inspectBloatCmd)
inspectDBCmd.AddCommand(inspectVacuumStatsCmd)
inspectDBCmd.AddCommand(inspectTableStatsCmd)
inspectDBCmd.AddCommand(inspectTrafficProfileCmd)
inspectDBCmd.AddCommand(inspectRoleStatsCmd)
inspectDBCmd.AddCommand(inspectDBStatsCmd)
// DEPRECATED
Expand Down
24 changes: 24 additions & 0 deletions docs/supabase/inspect/db-traffic-profile.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
# db-traffic-profile

This command analyzes table I/O patterns to show read/write activity ratios based on block-level operations. It combines data from PostgreSQL's `pg_stat_user_tables` (for tuple operations) and `pg_statio_user_tables` (for block I/O) to categorize each table's workload profile.


The command classifies tables into categories:
- **Read-Heavy** - Read operations are more than 5x write operations (e.g., 1:10, 1:50)
- **Write-Heavy** - Write operations are more than 20% of read operations (e.g., 1:2, 1:4, 2:1, 10:1)
- **Balanced** - Mixed workload where writes are between 20% and 500% of reads
- **Read-Only** - Only read operations detected
- **Write-Only** - Only write operations detected

```
SCHEMA │ TABLE │ BLOCKS READ │ WRITE TUPLES │ BLOCKS WRITE │ ACTIVITY RATIO
───────┼──────────────┼─────────────┼──────────────┼──────────────┼────────────────────
public │ user_events │ 450,234 │ 9,004,680│ 23,450 │ 20:1 (Write-Heavy)
public │ users │ 89,203 │ 12,451│ 1,203 │ 7.2:1 (Read-Heavy)
public │ sessions │ 15,402 │ 14,823│ 2,341 │ ≈1:1 (Balanced)
public │ cache_data │ 123,456 │ 0│ 0 │ Read-Only
auth │ audit_logs │ 0 │ 98,234│ 12,341 │ Write-Only
```

**Note:** This command only displays tables that have had both read and write activity. Tables with no I/O operations are not shown. The classification ratio threshold (default: 5:1) determines when a table is considered "heavy" in one direction versus balanced.

49 changes: 49 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
package traffic_profile

import (
"context"
_ "embed"
"fmt"

"github.com/go-errors/errors"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/pkg/pgxv5"
)

//go:embed traffic_profile.sql
var TrafficProfileQuery string

type Result struct {
Schemaname string
Table_name string
Blocks_read int64
Write_tuples int64
Blocks_write float64
Activity_ratio string
}

func Run(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
conn, err := utils.ConnectByConfig(ctx, config, options...)
if err != nil {
return err
}
defer conn.Close(context.Background())
rows, err := conn.Query(ctx, TrafficProfileQuery)
if err != nil {
return errors.Errorf("failed to query rows: %w", err)
}
result, err := pgxv5.CollectRows[Result](rows)
if err != nil {
return err
}

table := "|Schema|Table|Blocks Read|Write Tuples|Blocks Write|Activity Ratio|\n|-|-|-|-|-|-|\n"
for _, r := range result {
table += fmt.Sprintf("|`%s`|`%s`|`%d`|`%d`|`%.1f`|`%s`|\n",
r.Schemaname, r.Table_name, r.Blocks_read, r.Write_tuples, r.Blocks_write, r.Activity_ratio)
}
return utils.RenderTable(table)
}
45 changes: 45 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile.sql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
-- Query adapted from Crunchy Data blog: "Is Postgres Read Heavy or Write Heavy? (And Why You Should Care)" by David Christensen
WITH
ratio_target AS (SELECT 5 AS ratio),
table_list AS (SELECT
s.schemaname,
s.relname AS table_name,
si.heap_blks_read + si.idx_blks_read AS blocks_read,
s.n_tup_ins + s.n_tup_upd + s.n_tup_del AS write_tuples,
relpages * (s.n_tup_ins + s.n_tup_upd + s.n_tup_del ) / (case when reltuples = 0 then 1 else reltuples end) as blocks_write
FROM
pg_stat_user_tables AS s
JOIN pg_statio_user_tables AS si ON s.relid = si.relid
JOIN pg_class c ON c.oid = s.relid
WHERE
(s.n_tup_ins + s.n_tup_upd + s.n_tup_del) > 0
AND
(si.heap_blks_read + si.idx_blks_read) > 0
)
SELECT
schemaname,
table_name,
blocks_read,
write_tuples,
blocks_write,
CASE
WHEN blocks_read = 0 and blocks_write = 0 THEN
'No Activity'
WHEN blocks_write * ratio > blocks_read THEN
CASE
WHEN blocks_read = 0 THEN 'Write-Only'
ELSE
ROUND(blocks_write :: numeric / blocks_read :: numeric, 1)::text || ':1 (Write-Heavy)'
END
WHEN blocks_read > blocks_write * ratio THEN
CASE
WHEN blocks_write = 0 THEN 'Read-Only'
ELSE
'1:' || ROUND(blocks_read::numeric / blocks_write :: numeric, 1)::text || ' (Read-Heavy)'
END
ELSE
'1:1 (Balanced)'
END AS activity_ratio
FROM table_list, ratio_target
ORDER BY
(blocks_read + blocks_write) DESC
42 changes: 42 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile_test.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
package traffic_profile

import (
"context"
"testing"

"github.com/jackc/pgconn"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/supabase/cli/pkg/pgtest"
)

var dbConfig = pgconn.Config{
Host: "127.0.0.1",
Port: 5432,
User: "admin",
Password: "password",
Database: "postgres",
}

func TestTrafficProfile(t *testing.T) {
t.Run("inspects traffic profile", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query(TrafficProfileQuery).
Reply("SELECT 6", Result{
Schemaname: "public",
Table_name: "users",
Blocks_read: 1000,
Write_tuples: 500,
Blocks_write: 250.5,
Activity_ratio: "1:4.0 (Read-Heavy)",
})
// Run test
err := Run(context.Background(), dbConfig, fsys, conn.Intercept)
// Check error
assert.NoError(t, err)
})
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
10 changes: 10 additions & 0 deletions cmd/inspect.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ import (
"github.com/supabase/cli/internal/inspect/replication_slots"
"github.com/supabase/cli/internal/inspect/role_stats"
"github.com/supabase/cli/internal/inspect/table_stats"
"github.com/supabase/cli/internal/inspect/traffic_profile"
"github.com/supabase/cli/internal/inspect/vacuum_stats"
"github.com/supabase/cli/internal/utils/flags"
)
Expand DownExpand Up@@ -135,6 +136,14 @@ var (
},
}

inspectTrafficProfileCmd = &cobra.Command{
Use: "traffic-profile",
Short: "Show read/write activity ratio for tables based on block I/O operations",
RunE: func(cmd *cobra.Command, args []string) error {
return traffic_profile.Run(cmd.Context(), flags.DbConfig, afero.NewOsFs())
},
}

inspectCacheHitCmd = &cobra.Command{
Deprecated: `use "db-stats" instead.`,
Use: "cache-hit",
Expand DownExpand Up@@ -270,6 +279,7 @@ func init() {
inspectDBCmd.AddCommand(inspectBloatCmd)
inspectDBCmd.AddCommand(inspectVacuumStatsCmd)
inspectDBCmd.AddCommand(inspectTableStatsCmd)
inspectDBCmd.AddCommand(inspectTrafficProfileCmd)
inspectDBCmd.AddCommand(inspectRoleStatsCmd)
inspectDBCmd.AddCommand(inspectDBStatsCmd)
// DEPRECATED
Expand Down
24 changes: 24 additions & 0 deletions docs/supabase/inspect/db-traffic-profile.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
# db-traffic-profile

This command analyzes table I/O patterns to show read/write activity ratios based on block-level operations. It combines data from PostgreSQL's `pg_stat_user_tables` (for tuple operations) and `pg_statio_user_tables` (for block I/O) to categorize each table's workload profile.


The command classifies tables into categories:
- **Read-Heavy** - Read operations are more than 5x write operations (e.g., 1:10, 1:50)
- **Write-Heavy** - Write operations are more than 20% of read operations (e.g., 1:2, 1:4, 2:1, 10:1)
- **Balanced** - Mixed workload where writes are between 20% and 500% of reads
- **Read-Only** - Only read operations detected
- **Write-Only** - Only write operations detected

```
SCHEMA │ TABLE │ BLOCKS READ │ WRITE TUPLES │ BLOCKS WRITE │ ACTIVITY RATIO
───────┼──────────────┼─────────────┼──────────────┼──────────────┼────────────────────
public │ user_events │ 450,234 │ 9,004,680│ 23,450 │ 20:1 (Write-Heavy)
public │ users │ 89,203 │ 12,451│ 1,203 │ 7.2:1 (Read-Heavy)
public │ sessions │ 15,402 │ 14,823│ 2,341 │ ≈1:1 (Balanced)
public │ cache_data │ 123,456 │ 0│ 0 │ Read-Only
auth │ audit_logs │ 0 │ 98,234│ 12,341 │ Write-Only
```

**Note:** This command only displays tables that have had both read and write activity. Tables with no I/O operations are not shown. The classification ratio threshold (default: 5:1) determines when a table is considered "heavy" in one direction versus balanced.

49 changes: 49 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
package traffic_profile

import (
"context"
_ "embed"
"fmt"

"github.com/go-errors/errors"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/pkg/pgxv5"
)

//go:embed traffic_profile.sql
var TrafficProfileQuery string

type Result struct {
Schemaname string
Table_name string
Blocks_read int64
Write_tuples int64
Blocks_write float64
Activity_ratio string
}

func Run(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
conn, err := utils.ConnectByConfig(ctx, config, options...)
if err != nil {
return err
}
defer conn.Close(context.Background())
rows, err := conn.Query(ctx, TrafficProfileQuery)
if err != nil {
return errors.Errorf("failed to query rows: %w", err)
}
result, err := pgxv5.CollectRows[Result](rows)
if err != nil {
return err
}

table := "|Schema|Table|Blocks Read|Write Tuples|Blocks Write|Activity Ratio|\n|-|-|-|-|-|-|\n"
for _, r := range result {
table += fmt.Sprintf("|`%s`|`%s`|`%d`|`%d`|`%.1f`|`%s`|\n",
r.Schemaname, r.Table_name, r.Blocks_read, r.Write_tuples, r.Blocks_write, r.Activity_ratio)
}
return utils.RenderTable(table)
}
45 changes: 45 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile.sql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
-- Query adapted from Crunchy Data blog: "Is Postgres Read Heavy or Write Heavy? (And Why You Should Care)" by David Christensen
WITH
ratio_target AS (SELECT 5 AS ratio),
table_list AS (SELECT
s.schemaname,
s.relname AS table_name,
si.heap_blks_read + si.idx_blks_read AS blocks_read,
s.n_tup_ins + s.n_tup_upd + s.n_tup_del AS write_tuples,
relpages * (s.n_tup_ins + s.n_tup_upd + s.n_tup_del ) / (case when reltuples = 0 then 1 else reltuples end) as blocks_write
FROM
pg_stat_user_tables AS s
JOIN pg_statio_user_tables AS si ON s.relid = si.relid
JOIN pg_class c ON c.oid = s.relid
WHERE
(s.n_tup_ins + s.n_tup_upd + s.n_tup_del) > 0
AND
(si.heap_blks_read + si.idx_blks_read) > 0
)
SELECT
schemaname,
table_name,
blocks_read,
write_tuples,
blocks_write,
CASE
WHEN blocks_read = 0 and blocks_write = 0 THEN
'No Activity'
WHEN blocks_write * ratio > blocks_read THEN
CASE
WHEN blocks_read = 0 THEN 'Write-Only'
ELSE
ROUND(blocks_write :: numeric / blocks_read :: numeric, 1)::text || ':1 (Write-Heavy)'
END
WHEN blocks_read > blocks_write * ratio THEN
CASE
WHEN blocks_write = 0 THEN 'Read-Only'
ELSE
'1:' || ROUND(blocks_read::numeric / blocks_write :: numeric, 1)::text || ' (Read-Heavy)'
END
ELSE
'1:1 (Balanced)'
END AS activity_ratio
FROM table_list, ratio_target
ORDER BY
(blocks_read + blocks_write) DESC
42 changes: 42 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile_test.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
package traffic_profile

import (
"context"
"testing"

"github.com/jackc/pgconn"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/supabase/cli/pkg/pgtest"
)

var dbConfig = pgconn.Config{
Host: "127.0.0.1",
Port: 5432,
User: "admin",
Password: "password",
Database: "postgres",
}

func TestTrafficProfile(t *testing.T) {
t.Run("inspects traffic profile", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query(TrafficProfileQuery).
Reply("SELECT 6", Result{
Schemaname: "public",
Table_name: "users",
Blocks_read: 1000,
Write_tuples: 500,
Blocks_write: 250.5,
Activity_ratio: "1:4.0 (Read-Heavy)",
})
// Run test
err := Run(context.Background(), dbConfig, fsys, conn.Intercept)
// Check error
assert.NoError(t, err)
})
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
10 changes: 10 additions & 0 deletions cmd/inspect.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ import (
"github.com/supabase/cli/internal/inspect/replication_slots"
"github.com/supabase/cli/internal/inspect/role_stats"
"github.com/supabase/cli/internal/inspect/table_stats"
"github.com/supabase/cli/internal/inspect/traffic_profile"
"github.com/supabase/cli/internal/inspect/vacuum_stats"
"github.com/supabase/cli/internal/utils/flags"
)
Expand DownExpand Up@@ -135,6 +136,14 @@ var (
},
}

inspectTrafficProfileCmd = &cobra.Command{
Use: "traffic-profile",
Short: "Show read/write activity ratio for tables based on block I/O operations",
RunE: func(cmd *cobra.Command, args []string) error {
return traffic_profile.Run(cmd.Context(), flags.DbConfig, afero.NewOsFs())
},
}

inspectCacheHitCmd = &cobra.Command{
Deprecated: `use "db-stats" instead.`,
Use: "cache-hit",
Expand DownExpand Up@@ -270,6 +279,7 @@ func init() {
inspectDBCmd.AddCommand(inspectBloatCmd)
inspectDBCmd.AddCommand(inspectVacuumStatsCmd)
inspectDBCmd.AddCommand(inspectTableStatsCmd)
inspectDBCmd.AddCommand(inspectTrafficProfileCmd)
inspectDBCmd.AddCommand(inspectRoleStatsCmd)
inspectDBCmd.AddCommand(inspectDBStatsCmd)
// DEPRECATED
Expand Down
24 changes: 24 additions & 0 deletions docs/supabase/inspect/db-traffic-profile.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
# db-traffic-profile

This command analyzes table I/O patterns to show read/write activity ratios based on block-level operations. It combines data from PostgreSQL's `pg_stat_user_tables` (for tuple operations) and `pg_statio_user_tables` (for block I/O) to categorize each table's workload profile.


The command classifies tables into categories:
- **Read-Heavy** - Read operations are more than 5x write operations (e.g., 1:10, 1:50)
- **Write-Heavy** - Write operations are more than 20% of read operations (e.g., 1:2, 1:4, 2:1, 10:1)
- **Balanced** - Mixed workload where writes are between 20% and 500% of reads
- **Read-Only** - Only read operations detected
- **Write-Only** - Only write operations detected

```
SCHEMA │ TABLE │ BLOCKS READ │ WRITE TUPLES │ BLOCKS WRITE │ ACTIVITY RATIO
───────┼──────────────┼─────────────┼──────────────┼──────────────┼────────────────────
public │ user_events │ 450,234 │ 9,004,680│ 23,450 │ 20:1 (Write-Heavy)
public │ users │ 89,203 │ 12,451│ 1,203 │ 7.2:1 (Read-Heavy)
public │ sessions │ 15,402 │ 14,823│ 2,341 │ ≈1:1 (Balanced)
public │ cache_data │ 123,456 │ 0│ 0 │ Read-Only
auth │ audit_logs │ 0 │ 98,234│ 12,341 │ Write-Only
```

**Note:** This command only displays tables that have had both read and write activity. Tables with no I/O operations are not shown. The classification ratio threshold (default: 5:1) determines when a table is considered "heavy" in one direction versus balanced.

49 changes: 49 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
package traffic_profile

import (
"context"
_ "embed"
"fmt"

"github.com/go-errors/errors"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/pkg/pgxv5"
)

//go:embed traffic_profile.sql
var TrafficProfileQuery string

type Result struct {
Schemaname string
Table_name string
Blocks_read int64
Write_tuples int64
Blocks_write float64
Activity_ratio string
}

func Run(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
conn, err := utils.ConnectByConfig(ctx, config, options...)
if err != nil {
return err
}
defer conn.Close(context.Background())
rows, err := conn.Query(ctx, TrafficProfileQuery)
if err != nil {
return errors.Errorf("failed to query rows: %w", err)
}
result, err := pgxv5.CollectRows[Result](rows)
if err != nil {
return err
}

table := "|Schema|Table|Blocks Read|Write Tuples|Blocks Write|Activity Ratio|\n|-|-|-|-|-|-|\n"
for _, r := range result {
table += fmt.Sprintf("|`%s`|`%s`|`%d`|`%d`|`%.1f`|`%s`|\n",
r.Schemaname, r.Table_name, r.Blocks_read, r.Write_tuples, r.Blocks_write, r.Activity_ratio)
}
return utils.RenderTable(table)
}
45 changes: 45 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile.sql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
-- Query adapted from Crunchy Data blog: "Is Postgres Read Heavy or Write Heavy? (And Why You Should Care)" by David Christensen
WITH
ratio_target AS (SELECT 5 AS ratio),
table_list AS (SELECT
s.schemaname,
s.relname AS table_name,
si.heap_blks_read + si.idx_blks_read AS blocks_read,
s.n_tup_ins + s.n_tup_upd + s.n_tup_del AS write_tuples,
relpages * (s.n_tup_ins + s.n_tup_upd + s.n_tup_del ) / (case when reltuples = 0 then 1 else reltuples end) as blocks_write
FROM
pg_stat_user_tables AS s
JOIN pg_statio_user_tables AS si ON s.relid = si.relid
JOIN pg_class c ON c.oid = s.relid
WHERE
(s.n_tup_ins + s.n_tup_upd + s.n_tup_del) > 0
AND
(si.heap_blks_read + si.idx_blks_read) > 0
)
SELECT
schemaname,
table_name,
blocks_read,
write_tuples,
blocks_write,
CASE
WHEN blocks_read = 0 and blocks_write = 0 THEN
'No Activity'
WHEN blocks_write * ratio > blocks_read THEN
CASE
WHEN blocks_read = 0 THEN 'Write-Only'
ELSE
ROUND(blocks_write :: numeric / blocks_read :: numeric, 1)::text || ':1 (Write-Heavy)'
END
WHEN blocks_read > blocks_write * ratio THEN
CASE
WHEN blocks_write = 0 THEN 'Read-Only'
ELSE
'1:' || ROUND(blocks_read::numeric / blocks_write :: numeric, 1)::text || ' (Read-Heavy)'
END
ELSE
'1:1 (Balanced)'
END AS activity_ratio
FROM table_list, ratio_target
ORDER BY
(blocks_read + blocks_write) DESC
42 changes: 42 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile_test.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
package traffic_profile

import (
"context"
"testing"

"github.com/jackc/pgconn"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/supabase/cli/pkg/pgtest"
)

var dbConfig = pgconn.Config{
Host: "127.0.0.1",
Port: 5432,
User: "admin",
Password: "password",
Database: "postgres",
}

func TestTrafficProfile(t *testing.T) {
t.Run("inspects traffic profile", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query(TrafficProfileQuery).
Reply("SELECT 6", Result{
Schemaname: "public",
Table_name: "users",
Blocks_read: 1000,
Write_tuples: 500,
Blocks_write: 250.5,
Activity_ratio: "1:4.0 (Read-Heavy)",
})
// Run test
err := Run(context.Background(), dbConfig, fsys, conn.Intercept)
// Check error
assert.NoError(t, err)
})
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
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
10 changes: 10 additions & 0 deletions cmd/inspect.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ import (
"github.com/supabase/cli/internal/inspect/replication_slots"
"github.com/supabase/cli/internal/inspect/role_stats"
"github.com/supabase/cli/internal/inspect/table_stats"
"github.com/supabase/cli/internal/inspect/traffic_profile"
"github.com/supabase/cli/internal/inspect/vacuum_stats"
"github.com/supabase/cli/internal/utils/flags"
)
Expand DownExpand Up@@ -135,6 +136,14 @@ var (
},
}

inspectTrafficProfileCmd = &cobra.Command{
Use: "traffic-profile",
Short: "Show read/write activity ratio for tables based on block I/O operations",
RunE: func(cmd *cobra.Command, args []string) error {
return traffic_profile.Run(cmd.Context(), flags.DbConfig, afero.NewOsFs())
},
}

inspectCacheHitCmd = &cobra.Command{
Deprecated: `use "db-stats" instead.`,
Use: "cache-hit",
Expand DownExpand Up@@ -270,6 +279,7 @@ func init() {
inspectDBCmd.AddCommand(inspectBloatCmd)
inspectDBCmd.AddCommand(inspectVacuumStatsCmd)
inspectDBCmd.AddCommand(inspectTableStatsCmd)
inspectDBCmd.AddCommand(inspectTrafficProfileCmd)
inspectDBCmd.AddCommand(inspectRoleStatsCmd)
inspectDBCmd.AddCommand(inspectDBStatsCmd)
// DEPRECATED
Expand Down
24 changes: 24 additions & 0 deletions docs/supabase/inspect/db-traffic-profile.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
# db-traffic-profile

This command analyzes table I/O patterns to show read/write activity ratios based on block-level operations. It combines data from PostgreSQL's `pg_stat_user_tables` (for tuple operations) and `pg_statio_user_tables` (for block I/O) to categorize each table's workload profile.


The command classifies tables into categories:
- **Read-Heavy** - Read operations are more than 5x write operations (e.g., 1:10, 1:50)
- **Write-Heavy** - Write operations are more than 20% of read operations (e.g., 1:2, 1:4, 2:1, 10:1)
- **Balanced** - Mixed workload where writes are between 20% and 500% of reads
- **Read-Only** - Only read operations detected
- **Write-Only** - Only write operations detected

```
SCHEMA │ TABLE │ BLOCKS READ │ WRITE TUPLES │ BLOCKS WRITE │ ACTIVITY RATIO
───────┼──────────────┼─────────────┼──────────────┼──────────────┼────────────────────
public │ user_events │ 450,234 │ 9,004,680│ 23,450 │ 20:1 (Write-Heavy)
public │ users │ 89,203 │ 12,451│ 1,203 │ 7.2:1 (Read-Heavy)
public │ sessions │ 15,402 │ 14,823│ 2,341 │ ≈1:1 (Balanced)
public │ cache_data │ 123,456 │ 0│ 0 │ Read-Only
auth │ audit_logs │ 0 │ 98,234│ 12,341 │ Write-Only
```

**Note:** This command only displays tables that have had both read and write activity. Tables with no I/O operations are not shown. The classification ratio threshold (default: 5:1) determines when a table is considered "heavy" in one direction versus balanced.

49 changes: 49 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
package traffic_profile

import (
"context"
_ "embed"
"fmt"

"github.com/go-errors/errors"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/pkg/pgxv5"
)

//go:embed traffic_profile.sql
var TrafficProfileQuery string

type Result struct {
Schemaname string
Table_name string
Blocks_read int64
Write_tuples int64
Blocks_write float64
Activity_ratio string
}

func Run(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
conn, err := utils.ConnectByConfig(ctx, config, options...)
if err != nil {
return err
}
defer conn.Close(context.Background())
rows, err := conn.Query(ctx, TrafficProfileQuery)
if err != nil {
return errors.Errorf("failed to query rows: %w", err)
}
result, err := pgxv5.CollectRows[Result](rows)
if err != nil {
return err
}

table := "|Schema|Table|Blocks Read|Write Tuples|Blocks Write|Activity Ratio|\n|-|-|-|-|-|-|\n"
for _, r := range result {
table += fmt.Sprintf("|`%s`|`%s`|`%d`|`%d`|`%.1f`|`%s`|\n",
r.Schemaname, r.Table_name, r.Blocks_read, r.Write_tuples, r.Blocks_write, r.Activity_ratio)
}
return utils.RenderTable(table)
}
45 changes: 45 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile.sql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
-- Query adapted from Crunchy Data blog: "Is Postgres Read Heavy or Write Heavy? (And Why You Should Care)" by David Christensen
WITH
ratio_target AS (SELECT 5 AS ratio),
table_list AS (SELECT
s.schemaname,
s.relname AS table_name,
si.heap_blks_read + si.idx_blks_read AS blocks_read,
s.n_tup_ins + s.n_tup_upd + s.n_tup_del AS write_tuples,
relpages * (s.n_tup_ins + s.n_tup_upd + s.n_tup_del ) / (case when reltuples = 0 then 1 else reltuples end) as blocks_write
FROM
pg_stat_user_tables AS s
JOIN pg_statio_user_tables AS si ON s.relid = si.relid
JOIN pg_class c ON c.oid = s.relid
WHERE
(s.n_tup_ins + s.n_tup_upd + s.n_tup_del) > 0
AND
(si.heap_blks_read + si.idx_blks_read) > 0
)
SELECT
schemaname,
table_name,
blocks_read,
write_tuples,
blocks_write,
CASE
WHEN blocks_read = 0 and blocks_write = 0 THEN
'No Activity'
WHEN blocks_write * ratio > blocks_read THEN
CASE
WHEN blocks_read = 0 THEN 'Write-Only'
ELSE
ROUND(blocks_write :: numeric / blocks_read :: numeric, 1)::text || ':1 (Write-Heavy)'
END
WHEN blocks_read > blocks_write * ratio THEN
CASE
WHEN blocks_write = 0 THEN 'Read-Only'
ELSE
'1:' || ROUND(blocks_read::numeric / blocks_write :: numeric, 1)::text || ' (Read-Heavy)'
END
ELSE
'1:1 (Balanced)'
END AS activity_ratio
FROM table_list, ratio_target
ORDER BY
(blocks_read + blocks_write) DESC
42 changes: 42 additions & 0 deletions internal/inspect/traffic_profile/traffic_profile_test.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
package traffic_profile

import (
"context"
"testing"

"github.com/jackc/pgconn"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/supabase/cli/pkg/pgtest"
)

var dbConfig = pgconn.Config{
Host: "127.0.0.1",
Port: 5432,
User: "admin",
Password: "password",
Database: "postgres",
}

func TestTrafficProfile(t *testing.T) {
t.Run("inspects traffic profile", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query(TrafficProfileQuery).
Reply("SELECT 6", Result{
Schemaname: "public",
Table_name: "users",
Blocks_read: 1000,
Write_tuples: 500,
Blocks_write: 250.5,
Activity_ratio: "1:4.0 (Read-Heavy)",
})
// Run test
err := Run(context.Background(), dbConfig, fsys, conn.Intercept)
// Check error
assert.NoError(t, err)
})
}
Loading