diff --git a/database/README.md b/database/README.md new file mode 100644 index 00000000..b9270520 --- /dev/null +++ b/database/README.md @@ -0,0 +1,29 @@ +# Database + +Off-chain PostgreSQL layer for indexing Soroban contract events to power the analytics dashboard. + +## Structure + +``` +database/ +├── migrations/ +│ └── 001_create_events_table.sql # events table + analytics indexes +└── benchmarks/ + └── explain_analyze_indexes.sql # EXPLAIN ANALYZE before/after comparison +``` + +## Applying the migration + +```bash +psql -U -d -f database/migrations/001_create_events_table.sql +``` + +## Running benchmarks + +Seed the table first (see the seed helper comment in the benchmark file), then: + +```bash +psql -U -d -f database/benchmarks/explain_analyze_indexes.sql +``` + +Compare `Planning Time` and `Execution Time` in the output before and after the migration. diff --git a/database/benchmarks/explain_analyze_indexes.sql b/database/benchmarks/explain_analyze_indexes.sql new file mode 100644 index 00000000..f99f6bbd --- /dev/null +++ b/database/benchmarks/explain_analyze_indexes.sql @@ -0,0 +1,70 @@ +-- explain_analyze_indexes.sql +-- Measures query execution time before and after adding the analytics indexes. +-- +-- Usage: +-- 1. Run the BEFORE blocks BEFORE applying the migration (drop indexes if needed). +-- 2. Apply 001_create_events_table.sql +-- 3. Run the AFTER blocks and compare Planning/Execution times. +-- +-- Requires: a populated `events` table with representative data. +-- Tip: seed with at least 100k rows for meaningful results. + +-- ─── Seed helper (optional) ─────────────────────────────────────────────────── +-- INSERT INTO events (group_id, member_address, event_type, amount, cycle, +-- ledger_sequence, transaction_hash, created_at) +-- SELECT +-- (random() * 999 + 1)::BIGINT, +-- 'G' || substr(md5(random()::text), 1, 55), +-- (ARRAY['ContributionMade','PayoutExecuted','MemberJoined','GroupCreated'])[ceil(random()*4)::int], +-- (random() * 10000)::NUMERIC(20,7), +-- (random() * 11 + 1)::INTEGER, +-- (random() * 1000000)::BIGINT, +-- md5(random()::text), +-- NOW() - (random() * INTERVAL '365 days') +-- FROM generate_series(1, 100000); + +-- ───────────────────────────────────────────────────────────────────────────── +-- QUERY 1: Cycle analytics (group_id, event_type, created_at) +-- ───────────────────────────────────────────────────────────────────────────── + +-- BEFORE (sequential scan expected without index) +EXPLAIN ANALYZE +SELECT group_id, event_type, created_at, amount, cycle +FROM events +WHERE group_id = 42 + AND event_type = 'ContributionMade' + AND created_at >= NOW() - INTERVAL '30 days' +ORDER BY created_at DESC; + +-- AFTER (index scan expected on idx_events_cycle_analytics) +-- Run the same query again after migration — output should show: +-- "Index Scan using idx_events_cycle_analytics on events" +EXPLAIN ANALYZE +SELECT group_id, event_type, created_at, amount, cycle +FROM events +WHERE group_id = 42 + AND event_type = 'ContributionMade' + AND created_at >= NOW() - INTERVAL '30 days' +ORDER BY created_at DESC; + +-- ───────────────────────────────────────────────────────────────────────────── +-- QUERY 2: Member history (member_address, event_type) +-- ───────────────────────────────────────────────────────────────────────────── + +-- BEFORE (sequential scan expected without index) +EXPLAIN ANALYZE +SELECT member_address, event_type, created_at, amount, group_id +FROM events +WHERE member_address = 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5' + AND event_type = 'PayoutExecuted' +ORDER BY created_at DESC; + +-- AFTER (index scan expected on idx_events_member_history) +-- Run the same query again after migration — output should show: +-- "Index Scan using idx_events_member_history on events" +EXPLAIN ANALYZE +SELECT member_address, event_type, created_at, amount, group_id +FROM events +WHERE member_address = 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5' + AND event_type = 'PayoutExecuted' +ORDER BY created_at DESC; diff --git a/database/migrations/001_create_events_table.sql b/database/migrations/001_create_events_table.sql new file mode 100644 index 00000000..419c356f --- /dev/null +++ b/database/migrations/001_create_events_table.sql @@ -0,0 +1,36 @@ +-- Migration: 001_create_events_table.sql +-- Creates the events table for off-chain indexing of Soroban contract events. +-- This table powers the analytics dashboard and leaderboard queries. + +CREATE TABLE IF NOT EXISTS events ( + id BIGSERIAL PRIMARY KEY, + group_id BIGINT NOT NULL, + member_address VARCHAR(56) NOT NULL, -- Stellar address (G... format, max 56 chars) + event_type VARCHAR(64) NOT NULL, -- e.g. 'ContributionMade', 'PayoutExecuted' + amount NUMERIC(20,7), -- XLM amount in stroops / 10^7 + cycle INTEGER, + ledger_sequence BIGINT NOT NULL, + transaction_hash VARCHAR(64) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + raw_payload JSONB -- full event payload for flexibility +); + +-- ─── Indexes ────────────────────────────────────────────────────────────────── + +-- Index 1: Cycle analytics queries +-- Supports dashboard queries that filter by group, event type, and time range. +-- e.g. "show all ContributionMade events for group 42 in the last 30 days" +CREATE INDEX IF NOT EXISTS idx_events_cycle_analytics + ON events (group_id, event_type, created_at DESC); + +-- Index 2: Member history queries +-- Supports per-member history lookups filtered by event type. +-- e.g. "show all PayoutExecuted events for member G...XYZ" +CREATE INDEX IF NOT EXISTS idx_events_member_history + ON events (member_address, event_type); + +-- ─── Comments ───────────────────────────────────────────────────────────────── + +COMMENT ON TABLE events IS 'Off-chain index of Soroban contract events for analytics.'; +COMMENT ON INDEX idx_events_cycle_analytics IS 'Speeds up cycle analytics dashboard queries (group_id, event_type, created_at).'; +COMMENT ON INDEX idx_events_member_history IS 'Speeds up member history queries (member_address, event_type).';