-- Migration 003: Add composite and covering indexes for performance -- Date: 2025-12-14 -- Description: Create optimized indexes for frequent queries -- ===================================================== -- 1. User-Pack relationship indexes -- ===================================================== -- Composite index for "get user packs" query CREATE INDEX IF NOT EXISTS idx_user_packs_composite ON user_packs(user_id, pack_id) INCLUDE (granted_at, grant_type); -- Reverse index for "which users have this pack" CREATE INDEX IF NOT EXISTS idx_user_packs_pack_composite ON user_packs(pack_id, user_id) WHERE grant_type != 'admin'; -- ===================================================== -- 2. Card-Pack relationship indexes -- ===================================================== -- Composite index for "get pack cards ordered" CREATE INDEX IF NOT EXISTS idx_card_pack_cards_ordered ON card_pack_cards(pack_id, "order") INCLUDE (card_id); -- Index for cards CREATE INDEX IF NOT EXISTS idx_game_cards_pack ON game_cards(pack_id) INCLUDE (original, translation) WHERE is_deleted = false; -- ===================================================== -- 3. User subscriptions indexes -- ===================================================== -- Active subscriptions (most common query) CREATE INDEX IF NOT EXISTS idx_user_subscriptions_active ON user_subscriptions(user_id, finish) WHERE finish > NOW(); -- Index for subscription expiration checks CREATE INDEX IF NOT EXISTS idx_user_subscriptions_expiring ON user_subscriptions(finish) WHERE finish BETWEEN NOW() AND NOW() + INTERVAL '7 days'; -- ===================================================== -- 4. Payments indexes -- ===================================================== -- Covering index for user payments list CREATE INDEX IF NOT EXISTS idx_payments_user_covering ON payments(user_id, date DESC) INCLUDE (amount, currency, status, payment_system); -- Index for pending/processing payments (for cron jobs) CREATE INDEX IF NOT EXISTS idx_payments_pending ON payments(status, date DESC) WHERE status IN ('pending', 'processing', 'created'); -- Index for successful payments analytics CREATE INDEX IF NOT EXISTS idx_payments_succeeded ON payments(date DESC) WHERE status = 'succeeded'; -- ===================================================== -- 5. Study sessions indexes -- ===================================================== -- User sessions ordered by time CREATE INDEX IF NOT EXISTS idx_study_sessions_user_time ON study_sessions(user_id, start_time DESC) INCLUDE (words_learned, tests_completed, accuracy); -- Active sessions (no end_time yet) CREATE INDEX IF NOT EXISTS idx_study_sessions_active ON study_sessions(user_id, session_id) WHERE end_time IS NULL; -- ===================================================== -- 6. Authentication indexes -- ===================================================== -- Covering index for token lookup CREATE INDEX IF NOT EXISTS idx_tokens_token_covering ON tokens(token) INCLUDE (user_id, expires, external_user_id); -- Valid tokens only CREATE INDEX IF NOT EXISTS idx_tokens_valid ON tokens(user_id, expires DESC) WHERE expires > NOW(); -- Refresh tokens covering index CREATE INDEX IF NOT EXISTS idx_refresh_tokens_jti_covering ON refresh_tokens(jti) INCLUDE (user_id, expires_at, is_blacklisted); -- Active refresh tokens CREATE INDEX IF NOT EXISTS idx_refresh_tokens_active ON refresh_tokens(user_id, expires_at DESC) WHERE is_blacklisted = false AND expires_at > NOW(); -- ===================================================== -- 7. Users indexes -- ===================================================== -- Email lookup with common fields CREATE INDEX IF NOT EXISTS idx_users_email_covering ON users(email) INCLUDE (id, name, admin, external_user_id) WHERE email IS NOT NULL AND is_deleted = false; -- External user ID lookup CREATE INDEX IF NOT EXISTS idx_users_external_covering ON users(external_user_id) INCLUDE (id, name, email, admin); -- ===================================================== -- 8. Promo codes indexes -- ===================================================== -- Code lookup CREATE INDEX IF NOT EXISTS idx_promo_codes_code_covering ON promo_codes(code) INCLUDE (campaign_id, user_id, is_used) WHERE is_used = false; -- Campaign promo codes CREATE INDEX IF NOT EXISTS idx_promo_codes_campaign ON promo_codes(campaign_id, is_used); -- ===================================================== -- 9. Tests indexes -- ===================================================== -- Test pack relation CREATE INDEX IF NOT EXISTS idx_test_pack_relations_pack ON test_pack_relations(pack_id) INCLUDE (test_id); -- Test questions ordered CREATE INDEX IF NOT EXISTS idx_test_questions_test ON test_questions(test_id, "order"); -- ===================================================== -- Analyze tables after creating indexes -- ===================================================== ANALYZE users; ANALYZE user_packs; ANALYZE card_pack_cards; ANALYZE game_cards; ANALYZE payments; ANALYZE user_subscriptions; ANALYZE study_sessions; ANALYZE tokens; ANALYZE refresh_tokens; ANALYZE promo_codes; ANALYZE tests; -- ===================================================== -- Проверка созданных индексов -- ===================================================== SELECT schemaname, tablename, indexname, indexdef FROM pg_indexes WHERE schemaname = 'public' AND indexname LIKE 'idx_%composite%' OR indexname LIKE 'idx_%covering%' ORDER BY tablename, indexname; -- ===================================================== -- Оценка размера индексов -- ===================================================== SELECT schemaname, tablename, indexname, pg_size_pretty(pg_relation_size(indexrelid)) as index_size FROM pg_stat_user_indexes WHERE schemaname = 'public' ORDER BY pg_relation_size(indexrelid) DESC LIMIT 20; PRINT 'Migration 003 completed successfully!';