@@ -358,104 +358,71 @@ compose exec -T postgres psql -U postgres -d demo_shop -c "SELECT pg_stat_statem
358358# These patterns are intentionally suboptimal to trigger index recommendations
359359# NO pg_sleep - all slowness comes from real inefficient query patterns
360360echo " Starting workload (this takes about 30-60 seconds)..."
361- compose exec -T postgres psql -U postgres -d demo_shop -v ON_ERROR_STOP=1 << 'EOWORK '
362- -- Workload simulation for pg_stat_statements
363- -- With 300K+ audit_log and 100K+ order_items, these patterns will genuinely exceed 100ms
364- -- All slowness is REAL from inefficient queries (no pg_sleep)
361+ # Generate SQL file with individual statements.
362+ # Queries inside PL/pgSQL DO blocks are NOT tracked separately by pg_stat_statements.
363+ # Each SELECT must be a standalone statement to be tracked individually.
364+ # Using generate_series() to repeat each pattern multiple times within a single query,
365+ # which pg_stat_statements will track and report the cumulative execution time.
365366
366- DO $$
367- DECLARE
368- i int;
369- result_count bigint;
370- dummy_row record;
371- BEGIN
372- RAISE NOTICE 'Starting workload simulation with realistic slow patterns...';
373-
374- -- Pattern 1: LOWER() on orders.status column prevents index use
375- -- This scans all 5000 orders and applies LOWER() to each row
376- -- Run 50 times to accumulate enough calls for the advisor
377- RAISE NOTICE 'Running LOWER(status) pattern...';
378- FOR i IN 1..50 LOOP
379- SELECT COUNT(*) INTO result_count FROM orders
380- WHERE LOWER(status) = 'delivered' AND total_amount > 100;
381- END LOOP;
382-
383- -- Pattern 2: Full table scan on audit_log (300K+ rows) without index
384- -- This genuinely takes time due to the large table size
385- RAISE NOTICE 'Running audit_log full scan pattern...';
386- FOR i IN 1..20 LOOP
387- SELECT COUNT(*) INTO result_count FROM audit_log
388- WHERE table_name = 'orders'
389- AND changed_at > NOW() - INTERVAL '90 days';
390- END LOOP;
391-
392- -- Pattern 3: Sort on audit_log without index support (300K rows)
393- -- Sorting 300K rows without index is genuinely slow
394- RAISE NOTICE 'Running audit_log sort pattern...';
395- FOR i IN 1..15 LOOP
396- SELECT * INTO dummy_row FROM audit_log
397- WHERE table_name IN ('orders', 'customers', 'products')
398- ORDER BY changed_at DESC
399- LIMIT 1000;
400- END LOOP;
401-
402- -- Pattern 4: ILIKE with leading wildcard on products (forces seq scan)
403- RAISE NOTICE 'Running ILIKE pattern...';
404- FOR i IN 1..30 LOOP
405- SELECT COUNT(*) INTO result_count FROM products
406- WHERE name ILIKE '%widget%' OR description ILIKE '%premium%';
407- END LOOP;
408-
409- -- Pattern 5: Large join between order_items (100K) and orders (5K)
410- -- Missing index on the join column makes this slow
411- RAISE NOTICE 'Running large join pattern...';
412- FOR i IN 1..15 LOOP
413- SELECT COUNT(*), SUM(oi.subtotal) INTO result_count, result_count FROM order_items oi
414- JOIN orders o ON oi.order_id = o.id
415- WHERE o.status = 'delivered';
416- END LOOP;
417-
418- -- Pattern 6: Expensive aggregation across 100K+ order_items
419- RAISE NOTICE 'Running expensive aggregation pattern...';
420- FOR i IN 1..10 LOOP
421- SELECT COUNT(*) INTO result_count FROM (
422- SELECT DATE_TRUNC('month', o.created_at) as month,
423- p.name as product_name,
424- COUNT(*) as order_count,
425- SUM(oi.subtotal) as revenue
426- FROM orders o
427- JOIN order_items oi ON o.id = oi.order_id
428- JOIN products p ON oi.product_id = p.id
429- WHERE o.status NOT IN ('cancelled', 'refunded')
430- GROUP BY DATE_TRUNC('month', o.created_at), p.id, p.name
431- ORDER BY revenue DESC
432- ) sub;
433- END LOOP;
434-
435- -- Pattern 7: Missing composite index - status + payment_status filter
436- RAISE NOTICE 'Running missing composite index pattern...';
437- FOR i IN 1..40 LOOP
438- SELECT COUNT(*) INTO result_count FROM orders
439- WHERE status = 'pending' AND payment_status = 'paid'
440- AND created_at > NOW() - INTERVAL '30 days';
441- END LOOP;
442-
443- -- Pattern 8: Correlated subquery (inefficient N+1 style)
444- RAISE NOTICE 'Running correlated subquery pattern...';
445- FOR i IN 1..20 LOOP
446- SELECT COUNT(*) INTO result_count FROM orders o
447- WHERE EXISTS (
448- SELECT 1 FROM audit_log a
449- WHERE a.record_id = o.id
450- AND a.table_name = 'orders'
451- AND a.action = 'UPDATE'
452- );
453- END LOOP;
454-
455- RAISE NOTICE 'Workload simulation completed.';
456- END $$;
367+ compose exec -T postgres psql -U postgres -d demo_shop -v ON_ERROR_STOP=1 << 'EOWORK '
368+ -- Pattern 1: LOWER() on status column defeats index (50 calls)
369+ -- Each row of generate_series triggers a separate evaluation via LATERAL join
370+ SELECT 'LOWER(status) pattern' as pattern, COUNT(*)
371+ FROM generate_series(1, 50) g,
372+ LATERAL (SELECT COUNT(*) FROM orders WHERE LOWER(status) = 'delivered' AND total_amount > 100) sub;
373+
374+ -- Pattern 2: Full table scan on audit_log 300K rows (20 calls)
375+ SELECT 'audit_log scan pattern' as pattern, COUNT(*)
376+ FROM generate_series(1, 20) g,
377+ LATERAL (SELECT COUNT(*) FROM audit_log WHERE table_name = 'orders' AND changed_at > NOW() - INTERVAL '90 days') sub;
378+
379+ -- Pattern 3: Sort on audit_log without index (15 calls)
380+ SELECT 'audit_log sort pattern' as pattern, COUNT(*)
381+ FROM generate_series(1, 15) g,
382+ LATERAL (SELECT id FROM audit_log WHERE table_name IN ('orders', 'customers', 'products') ORDER BY changed_at DESC LIMIT 1000) sub;
383+
384+ -- Pattern 4: ILIKE with leading wildcard (30 calls)
385+ SELECT 'ILIKE pattern' as pattern, COUNT(*)
386+ FROM generate_series(1, 30) g,
387+ LATERAL (SELECT COUNT(*) FROM products WHERE name ILIKE '%widget%' OR description ILIKE '%premium%') sub;
388+
389+ -- Pattern 5: Large join order_items (100K) to orders (5K) (15 calls)
390+ SELECT 'large join pattern' as pattern, COUNT(*), SUM(total)
391+ FROM generate_series(1, 15) g,
392+ LATERAL (SELECT COUNT(*) as cnt, SUM(oi.subtotal) as total FROM order_items oi JOIN orders o ON oi.order_id = o.id WHERE o.status = 'delivered') sub;
393+
394+ -- Pattern 6: Expensive aggregation with GROUP BY (10 calls)
395+ SELECT 'expensive aggregation pattern' as pattern, COUNT(*)
396+ FROM generate_series(1, 10) g,
397+ LATERAL (
398+ SELECT DATE_TRUNC('month', o.created_at), p.name, COUNT(*), SUM(oi.subtotal)
399+ FROM orders o
400+ JOIN order_items oi ON o.id = oi.order_id
401+ JOIN products p ON oi.product_id = p.id
402+ WHERE o.status NOT IN ('cancelled', 'refunded')
403+ GROUP BY 1, p.id, p.name
404+ ORDER BY 4 DESC
405+ LIMIT 100
406+ ) sub;
407+
408+ -- Pattern 7: Missing composite index on (status, payment_status) (40 calls)
409+ SELECT 'composite index pattern' as pattern, COUNT(*)
410+ FROM generate_series(1, 40) g,
411+ LATERAL (SELECT COUNT(*) FROM orders WHERE status = 'pending' AND payment_status = 'paid' AND created_at > NOW() - INTERVAL '30 days') sub;
412+
413+ -- Pattern 8: Correlated subquery - inefficient EXISTS (20 calls)
414+ SELECT 'correlated subquery pattern' as pattern, COUNT(*)
415+ FROM generate_series(1, 20) g,
416+ LATERAL (
417+ SELECT COUNT(*) FROM orders o
418+ WHERE EXISTS (SELECT 1 FROM audit_log a WHERE a.record_id = o.id AND a.table_name = 'orders' AND a.action = 'UPDATE')
419+ ) sub;
420+
421+ SELECT 'Workload patterns completed' as status;
457422EOWORK
458423
424+ echo " Workload patterns completed."
425+
459426echo " Workload simulation completed."
460427echo " Verifying pg_stat_statements data..."
461428slow_count=" $( compose exec -T postgres psql -U postgres -d demo_shop -At -c \
0 commit comments