| | 465 | |
| | 466 | |
| | 467 | == Scenario 3 - Annual genre popularity and engagement trend |
| | 468 | === Without index analysis |
| | 469 | {{{ |
| | 470 | EXPLAIN ANALYZE |
| | 471 | WITH genre_annual AS ( |
| | 472 | SELECT |
| | 473 | DATE_TRUNC('year', s.story_created_at) AS year, |
| | 474 | g.genre_id, |
| | 475 | g.genre_name, |
| | 476 | COUNT(DISTINCT s.story_id) AS total_stories, |
| | 477 | COUNT(DISTINCT w.user_id) AS total_writers, |
| | 478 | COALESCE(SUM(ch.view_count), 0) AS total_views, |
| | 479 | COALESCE(SUM(ch.word_count), 0) AS total_words, |
| | 480 | COUNT(DISTINCT l.user_id) AS total_likes, |
| | 481 | COUNT(DISTINCT c.comment_id) AS total_comments, |
| | 482 | ROUND(AVG(ch.rating), 2) AS avg_rating |
| | 483 | FROM genre g |
| | 484 | JOIN has_genre hg ON g.genre_id = hg.genre_id |
| | 485 | JOIN story s ON hg.story_id = s.story_id |
| | 486 | JOIN writer w ON s.user_id = w.user_id |
| | 487 | JOIN status st ON s.story_id = st.story_id AND st.status = 'published' |
| | 488 | LEFT JOIN chapter ch ON s.story_id = ch.story_id |
| | 489 | LEFT JOIN likes l ON s.story_id = l.story_id |
| | 490 | LEFT JOIN comment c ON s.story_id = c.story_id |
| | 491 | GROUP BY DATE_TRUNC('year', s.story_created_at), g.genre_id, g.genre_name |
| | 492 | ), |
| | 493 | with_metrics AS ( |
| | 494 | SELECT *, |
| | 495 | ROUND((total_likes + total_comments)::DECIMAL / NULLIF(total_views, 0) * 100, 2) AS engagement_rate, |
| | 496 | ROUND(total_views::DECIMAL / NULLIF(total_stories, 0), 2) AS avg_views_per_story, |
| | 497 | LAG(total_views) OVER (PARTITION BY genre_id ORDER BY year) AS prev_year_views, |
| | 498 | LAG(total_stories) OVER (PARTITION BY genre_id ORDER BY year) AS prev_year_stories |
| | 499 | FROM genre_annual |
| | 500 | ) |
| | 501 | SELECT |
| | 502 | TO_CHAR(year, 'YYYY') AS year, |
| | 503 | genre_name, |
| | 504 | total_stories, total_writers, total_views, avg_views_per_story, |
| | 505 | total_likes, total_comments, |
| | 506 | COALESCE(avg_rating, 0) AS avg_rating, |
| | 507 | COALESCE(engagement_rate, 0) AS engagement_rate, |
| | 508 | ROUND((total_views - prev_year_views)::DECIMAL / NULLIF(prev_year_views, 0) * 100, 2) AS yoy_views_growth_pct, |
| | 509 | ROUND((total_stories - prev_year_stories)::DECIMAL / NULLIF(prev_year_stories, 0) * 100, 2) AS yoy_stories_growth_pct, |
| | 510 | RANK() OVER (PARTITION BY year ORDER BY total_views DESC) AS popularity_rank, |
| | 511 | RANK() OVER (PARTITION BY year ORDER BY engagement_rate DESC) AS engagement_rank |
| | 512 | FROM with_metrics |
| | 513 | ORDER BY year DESC, popularity_rank; |
| | 514 | }}} |
| | 515 | {{{ |
| | 516 | |QUERY PLAN | |
| | 517 | |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |
| | 518 | |Sort (cost=190.58..191.10 rows=207 width=506) (actual time=0.963..0.969 rows=10 loops=1) | |
| | 519 | | Sort Key: (to_char(with_metrics.year, 'YYYY'::text)) DESC, (rank() OVER (?)) | |
| | 520 | | Sort Method: quicksort Memory: 26kB | |
| | 521 | | -> WindowAgg (cost=158.05..182.62 rows=207 width=506) (actual time=0.919..0.942 rows=10 loops=1) | |
| | 522 | | -> Incremental Sort (cost=158.05..171.23 rows=207 width=386) (actual time=0.910..0.916 rows=10 loops=1) | |
| | 523 | | Sort Key: with_metrics.year, with_metrics.total_views DESC | |
| | 524 | | Presorted Key: with_metrics.year | |
| | 525 | | Full-sort Groups: 1 Sort Method: quicksort Average Memory: 26kB Peak Memory: 26kB | |
| | 526 | | -> WindowAgg (cost=158.02..162.16 rows=207 width=386) (actual time=0.888..0.903 rows=10 loops=1) | |
| | 527 | | -> Sort (cost=158.02..158.54 rows=207 width=378) (actual time=0.885..0.890 rows=10 loops=1) | |
| | 528 | | Sort Key: with_metrics.year, with_metrics.engagement_rate DESC | |
| | 529 | | Sort Method: quicksort Memory: 26kB | |
| | 530 | | -> Subquery Scan on with_metrics (cost=137.12..150.06 rows=207 width=378) (actual time=0.849..0.879 rows=10 loops=1) | |
| | 531 | | -> WindowAgg (cost=137.12..147.99 rows=207 width=390) (actual time=0.848..0.876 rows=10 loops=1) | |
| | 532 | | -> Sort (cost=137.12..137.64 rows=207 width=302) (actual time=0.838..0.842 rows=10 loops=1) | |
| | 533 | | Sort Key: genre_annual.genre_id, genre_annual.year | |
| | 534 | | Sort Method: quicksort Memory: 25kB | |
| | 535 | | -> Subquery Scan on genre_annual (cost=118.81..129.16 rows=207 width=302) (actual time=0.644..0.833 rows=10 loops=1) | |
| | 536 | | -> GroupAggregate (cost=118.81..127.09 rows=207 width=310) (actual time=0.644..0.830 rows=10 loops=1) | |
| | 537 | | Group Key: (date_trunc('year'::text, s.story_created_at)), g.genre_id | |
| | 538 | | -> Sort (cost=118.81..119.33 rows=207 width=257) (actual time=0.579..0.603 rows=354 loops=1) | |
| | 539 | | Sort Key: (date_trunc('year'::text, s.story_created_at)), g.genre_id, s.story_id | |
| | 540 | | Sort Method: quicksort Memory: 48kB | |
| | 541 | | -> Merge Left Join (cost=1.15..110.85 rows=207 width=257) (actual time=0.093..0.405 rows=354 loops=1) | |
| | 542 | | Merge Cond: (s.story_id = l.story_id) | |
| | 543 | | -> Merge Left Join (cost=1.01..94.70 rows=57 width=253) (actual time=0.079..0.200 rows=78 loops=1) | |
| | 544 | | Merge Cond: (s.story_id = c.story_id) | |
| | 545 | | -> Merge Left Join (cost=0.88..81.48 rows=26 width=249) (actual time=0.070..0.162 rows=30 loops=1) | |
| | 546 | | Merge Cond: (s.story_id = ch.story_id) | |
| | 547 | | -> Nested Loop (cost=0.74..67.76 rows=12 width=238) (actual time=0.055..0.125 rows=12 loops=1) | |
| | 548 | | -> Nested Loop (cost=0.57..48.82 rows=12 width=238) (actual time=0.046..0.104 rows=12 loops=1) | |
| | 549 | | -> Nested Loop (cost=0.43..43.70 rows=12 width=230) (actual time=0.038..0.081 rows=12 loops=1) | |
| | 550 | | -> Nested Loop (cost=0.28..17.58 rows=12 width=12) (actual time=0.028..0.052 rows=12 loops=1) | |
| | 551 | | -> Index Only Scan using has_genre_pk on has_genre hg (cost=0.14..12.36 rows=15 width=8) (actual time=0.012..0.019 rows=15 loops=1) | |
| | 552 | | Heap Fetches: 15 | |
| | 553 | | -> Memoize (cost=0.14..0.96 rows=1 width=4) (actual time=0.002..0.002 rows=1 loops=15) | |
| | 554 | | Cache Key: hg.story_id | |
| | 555 | | Cache Mode: logical | |
| | 556 | | Hits: 10 Misses: 5 Evictions: 0 Overflows: 0 Memory Usage: 1kB | |
| | 557 | | -> Index Only Scan using idx_status_published on status st (cost=0.13..0.95 rows=1 width=4) (actual time=0.003..0.003 rows=1 loops=5)| |
| | 558 | | Index Cond: (story_id = hg.story_id) | |
| | 559 | | Heap Fetches: 4 | |
| | 560 | | -> Memoize (cost=0.16..3.11 rows=1 width=222) (actual time=0.002..0.002 rows=1 loops=12) | |
| | 561 | | Cache Key: hg.genre_id | |
| | 562 | | Cache Mode: logical | |
| | 563 | | Hits: 5 Misses: 7 Evictions: 0 Overflows: 0 Memory Usage: 1kB | |
| | 564 | | -> Index Scan using genre_pkey on genre g (cost=0.15..3.10 rows=1 width=222) (actual time=0.002..0.002 rows=1 loops=7) | |
| | 565 | | Index Cond: (genre_id = hg.genre_id) | |
| | 566 | | -> Memoize (cost=0.14..0.96 rows=1 width=16) (actual time=0.001..0.001 rows=1 loops=12) | |
| | 567 | | Cache Key: hg.story_id | |
| | 568 | | Cache Mode: logical | |
| | 569 | | Hits: 8 Misses: 4 Evictions: 0 Overflows: 0 Memory Usage: 1kB | |
| | 570 | | -> Index Scan using story_pkey on story s (cost=0.13..0.95 rows=1 width=16) (actual time=0.003..0.003 rows=1 loops=4) | |
| | 571 | | Index Cond: (story_id = hg.story_id) | |
| | 572 | | -> Memoize (cost=0.17..4.98 rows=1 width=4) (actual time=0.001..0.001 rows=1 loops=12) | |
| | 573 | | Cache Key: s.user_id | |
| | 574 | | Cache Mode: logical | |
| | 575 | | Hits: 9 Misses: 3 Evictions: 0 Overflows: 0 Memory Usage: 1kB | |
| | 576 | | -> Index Only Scan using writer_pkey on writer w (cost=0.15..4.97 rows=1 width=4) (actual time=0.003..0.003 rows=1 loops=3) | |
| | 577 | | Index Cond: (user_id = s.user_id) | |
| | 578 | | Heap Fetches: 3 | |
| | 579 | | -> Materialize (cost=0.14..13.33 rows=11 width=15) (actual time=0.011..0.022 rows=31 loops=1) | |
| | 580 | | -> Index Scan using idx_chapter_story_id on chapter ch (cost=0.14..13.30 rows=11 width=15) (actual time=0.008..0.013 rows=11 loops=1) | |
| | 581 | | -> Materialize (cost=0.14..12.33 rows=11 width=8) (actual time=0.008..0.017 rows=79 loops=1) | |
| | 582 | | -> Index Scan using idx_comment_story_id on comment c (cost=0.14..12.30 rows=11 width=8) (actual time=0.006..0.009 rows=11 loops=1) | |
| | 583 | | -> Materialize (cost=0.14..12.45 rows=18 width=8) (actual time=0.007..0.032 rows=355 loops=1) | |
| | 584 | | -> Index Scan using idx_likes_story_id on likes l (cost=0.14..12.41 rows=18 width=8) (actual time=0.006..0.010 rows=18 loops=1) | |
| | 585 | |Planning Time: 4.355 ms | |
| | 586 | |Execution Time: 1.161 ms |
| | 587 | }}} |
| | 588 | Average time without indexes is: 1.179 ms |
| | 589 | === Indexes for this queries |
| | 590 | {{{ |
| | 591 | CREATE INDEX idx_has_genre_genre_id ON has_genre(genre_id); |
| | 592 | CREATE INDEX idx_has_genre_story_id ON has_genre(story_id); |
| | 593 | CREATE INDEX idx_status_story_published ON status(story_id) WHERE status = 'published'; |
| | 594 | |
| | 595 | ANALYZE genre; |
| | 596 | ANALYZE has_genre; |
| | 597 | ANALYZE status; |
| | 598 | ANALYZE story; |
| | 599 | ANALYZE chapter; |
| | 600 | ANALYZE likes; |
| | 601 | ANALYZE comment; |
| | 602 | }}} |
| | 603 | After analysis we get: |
| | 604 | {{{ |
| | 605 | |QUERY PLAN | |
| | 606 | |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |
| | 607 | |Sort (cost=119.78..119.91 rows=50 width=298) (actual time=0.999..1.005 rows=10 loops=1) | |
| | 608 | | Sort Key: (to_char(with_metrics.year, 'YYYY'::text)) DESC, (rank() OVER (?)) | |
| | 609 | | Sort Method: quicksort Memory: 26kB | |
| | 610 | | -> WindowAgg (cost=112.40..118.37 rows=50 width=298) (actual time=0.957..0.980 rows=10 loops=1) | |
| | 611 | | -> Incremental Sort (cost=112.40..115.62 rows=50 width=178) (actual time=0.949..0.954 rows=10 loops=1) | |
| | 612 | | Sort Key: with_metrics.year, with_metrics.total_views DESC | |
| | 613 | | Presorted Key: with_metrics.year | |
| | 614 | | Full-sort Groups: 1 Sort Method: quicksort Average Memory: 26kB Peak Memory: 26kB | |
| | 615 | | -> WindowAgg (cost=112.37..113.37 rows=50 width=178) (actual time=0.926..0.942 rows=10 loops=1) | |
| | 616 | | -> Sort (cost=112.37..112.49 rows=50 width=170) (actual time=0.923..0.928 rows=10 loops=1) | |
| | 617 | | Sort Key: with_metrics.year, with_metrics.engagement_rate DESC | |
| | 618 | | Sort Method: quicksort Memory: 26kB | |
| | 619 | | -> Subquery Scan on with_metrics (cost=107.83..110.96 rows=50 width=170) (actual time=0.888..0.917 rows=10 loops=1) | |
| | 620 | | -> WindowAgg (cost=107.83..110.46 rows=50 width=182) (actual time=0.887..0.914 rows=10 loops=1) | |
| | 621 | | -> Sort (cost=107.83..107.96 rows=50 width=94) (actual time=0.876..0.881 rows=10 loops=1) | |
| | 622 | | Sort Key: genre_annual.genre_id, genre_annual.year | |
| | 623 | | Sort Method: quicksort Memory: 25kB | |
| | 624 | | -> Subquery Scan on genre_annual (cost=100.39..106.42 rows=50 width=94) (actual time=0.684..0.871 rows=10 loops=1) | |
| | 625 | | -> GroupAggregate (cost=100.39..105.92 rows=50 width=102) (actual time=0.683..0.869 rows=10 loops=1) | |
| | 626 | | Group Key: (date_trunc('year'::text, s.story_created_at)), g.genre_id | |
| | 627 | | -> Sort (cost=100.39..100.91 rows=207 width=49) (actual time=0.618..0.641 rows=354 loops=1) | |
| | 628 | | Sort Key: (date_trunc('year'::text, s.story_created_at)), g.genre_id, s.story_id | |
| | 629 | | Sort Method: quicksort Memory: 48kB | |
| | 630 | | -> Merge Left Join (cost=1.14..92.43 rows=207 width=49) (actual time=0.101..0.453 rows=354 loops=1) | |
| | 631 | | Merge Cond: (s.story_id = l.story_id) | |
| | 632 | | -> Merge Left Join (cost=1.00..76.29 rows=57 width=45) (actual time=0.075..0.215 rows=78 loops=1) | |
| | 633 | | Merge Cond: (s.story_id = c.story_id) | |
| | 634 | | -> Merge Left Join (cost=0.86..63.07 rows=26 width=41) (actual time=0.067..0.177 rows=30 loops=1) | |
| | 635 | | Merge Cond: (s.story_id = ch.story_id) | |
| | 636 | | -> Nested Loop (cost=0.73..49.35 rows=12 width=30) (actual time=0.053..0.138 rows=12 loops=1) | |
| | 637 | | -> Nested Loop (cost=0.56..30.40 rows=12 width=30) (actual time=0.044..0.117 rows=12 loops=1) | |
| | 638 | | -> Nested Loop (cost=0.42..25.28 rows=12 width=22) (actual time=0.036..0.092 rows=12 loops=1) | |
| | 639 | | -> Nested Loop (cost=0.28..20.06 rows=15 width=18) (actual time=0.026..0.061 rows=15 loops=1) | |
| | 640 | | -> Index Only Scan using has_genre_pk on has_genre hg (cost=0.14..12.36 rows=15 width=8) (actual time=0.011..0.019 rows=15 loops=1) | |
| | 641 | | Heap Fetches: 15 | |
| | 642 | | -> Memoize (cost=0.15..0.96 rows=1 width=14) (actual time=0.002..0.002 rows=1 loops=15) | |
| | 643 | | Cache Key: hg.genre_id | |
| | 644 | | Cache Mode: logical | |
| | 645 | | Hits: 7 Misses: 8 Evictions: 0 Overflows: 0 Memory Usage: 1kB | |
| | 646 | | -> Index Scan using genre_pkey on genre g (cost=0.14..0.95 rows=1 width=14) (actual time=0.002..0.002 rows=1 loops=8) | |
| | 647 | | Index Cond: (genre_id = hg.genre_id) | |
| | 648 | | -> Memoize (cost=0.14..0.96 rows=1 width=4) (actual time=0.002..0.002 rows=1 loops=15) | |
| | 649 | | Cache Key: hg.story_id | |
| | 650 | | Cache Mode: logical | |
| | 651 | | Hits: 10 Misses: 5 Evictions: 0 Overflows: 0 Memory Usage: 1kB | |
| | 652 | | -> Index Only Scan using idx_status_story_published on status st (cost=0.13..0.95 rows=1 width=4) (actual time=0.003..0.003 rows=1 loops=5)| |
| | 653 | | Index Cond: (story_id = hg.story_id) | |
| | 654 | | Heap Fetches: 4 | |
| | 655 | | -> Memoize (cost=0.14..0.96 rows=1 width=16) (actual time=0.002..0.002 rows=1 loops=12) | |
| | 656 | | Cache Key: hg.story_id | |
| | 657 | | Cache Mode: logical | |
| | 658 | | Hits: 8 Misses: 4 Evictions: 0 Overflows: 0 Memory Usage: 1kB | |
| | 659 | | -> Index Scan using story_pkey on story s (cost=0.13..0.95 rows=1 width=16) (actual time=0.003..0.003 rows=1 loops=4) | |
| | 660 | | Index Cond: (story_id = hg.story_id) | |
| | 661 | | -> Memoize (cost=0.17..4.98 rows=1 width=4) (actual time=0.001..0.001 rows=1 loops=12) | |
| | 662 | | Cache Key: s.user_id | |
| | 663 | | Cache Mode: logical | |
| | 664 | | Hits: 9 Misses: 3 Evictions: 0 Overflows: 0 Memory Usage: 1kB | |
| | 665 | | -> Index Only Scan using writer_pkey on writer w (cost=0.15..4.97 rows=1 width=4) (actual time=0.003..0.003 rows=1 loops=3) | |
| | 666 | | Index Cond: (user_id = s.user_id) | |
| | 667 | | Heap Fetches: 3 | |
| | 668 | | -> Materialize (cost=0.14..13.33 rows=11 width=15) (actual time=0.010..0.023 rows=31 loops=1) | |
| | 669 | | -> Index Scan using idx_chapter_story_id on chapter ch (cost=0.14..13.30 rows=11 width=15) (actual time=0.008..0.013 rows=11 loops=1) | |
| | 670 | | -> Materialize (cost=0.14..12.33 rows=11 width=8) (actual time=0.007..0.017 rows=79 loops=1) | |
| | 671 | | -> Index Scan using idx_comment_story_id on comment c (cost=0.14..12.30 rows=11 width=8) (actual time=0.006..0.009 rows=11 loops=1) | |
| | 672 | | -> Materialize (cost=0.14..12.45 rows=18 width=8) (actual time=0.007..0.033 rows=355 loops=1) | |
| | 673 | | -> Index Scan using idx_likes_story_id on likes l (cost=0.14..12.41 rows=18 width=8) (actual time=0.006..0.011 rows=18 loops=1) | |
| | 674 | |Planning Time: 4.782 ms | |
| | 675 | |Execution Time: 1.200 ms |
| | 676 | }}} |
| | 677 | Average time: 1.181 ms |
| | 678 | |
| | 679 | The indexes added for this query provided no meaningful improvement, with average execution time remaining virtually unchanged from 1.179 ms to 1.181 ms (~0%), which is within normal measurement variance. The indexes are not kept. |